-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #498 from nevermined-io/feat/opengsn-relay
Opengsn forwarding sample
- Loading branch information
Showing
2 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
pragma solidity ^0.8.0; | ||
pragma abicoder v2; | ||
|
||
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; | ||
import 'hardhat/console.sol'; | ||
|
||
interface IForwarder { | ||
|
||
struct ForwardRequest { | ||
address from; | ||
address to; | ||
uint256 value; | ||
uint256 gas; | ||
uint256 nonce; | ||
bytes data; | ||
uint256 validUntil; | ||
} | ||
|
||
event DomainRegistered(bytes32 indexed domainSeparator, bytes domainValue); | ||
|
||
event RequestTypeRegistered(bytes32 indexed typeHash, string typeStr); | ||
|
||
function getNonce(address from) | ||
external view | ||
returns(uint256); | ||
|
||
/** | ||
* verify the transaction would execute. | ||
* validate the signature and the nonce of the request. | ||
* revert if either signature or nonce are incorrect. | ||
* also revert if domainSeparator or requestTypeHash are not registered. | ||
*/ | ||
function verify( | ||
ForwardRequest calldata forwardRequest, | ||
bytes32 domainSeparator, | ||
bytes32 requestTypeHash, | ||
bytes calldata suffixData, | ||
bytes calldata signature | ||
) external view; | ||
|
||
/** | ||
* execute a transaction | ||
* @param forwardRequest - all transaction parameters | ||
* @param domainSeparator - domain used when signing this request | ||
* @param requestTypeHash - request type used when signing this request. | ||
* @param suffixData - the extension data used when signing this request. | ||
* @param signature - signature to validate. | ||
* | ||
* the transaction is verified, and then executed. | ||
* the success and ret of "call" are returned. | ||
* This method would revert only verification errors. target errors | ||
* are reported using the returned "success" and ret string | ||
*/ | ||
function execute( | ||
ForwardRequest calldata forwardRequest, | ||
bytes32 domainSeparator, | ||
bytes32 requestTypeHash, | ||
bytes calldata suffixData, | ||
bytes calldata signature | ||
) | ||
external payable | ||
returns (bool success, bytes memory ret); | ||
|
||
/** | ||
* Register a new Request typehash. | ||
* @param typeName - the name of the request type. | ||
* @param typeSuffix - any extra data after the generic params. | ||
* (must add at least one param. The generic ForwardRequest type is always registered by the constructor) | ||
*/ | ||
function registerRequestType(string calldata typeName, string calldata typeSuffix) external; | ||
|
||
/** | ||
* Register a new domain separator. | ||
* The domain separator must have the following fields: name,version,chainId, verifyingContract. | ||
* the chainId is the current network's chainId, and the verifyingContract is this forwarder. | ||
* This method is given the domain name and version to create and register the domain separator value. | ||
* @param name the domain's display name | ||
* @param version the domain/protocol version | ||
*/ | ||
function registerDomainSeparator(string calldata name, string calldata version) external; | ||
} | ||
|
||
contract Forwarder is IForwarder { | ||
using ECDSA for bytes32; | ||
|
||
string public constant GENERIC_PARAMS = 'address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 validUntil'; | ||
|
||
string public constant EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; | ||
|
||
mapping(bytes32 => bool) public typeHashes; | ||
mapping(bytes32 => bool) public domains; | ||
|
||
// Nonces of senders, used to prevent replay attacks | ||
mapping(address => uint256) private nonces; | ||
|
||
// solhint-disable-next-line no-empty-blocks | ||
receive() external payable {} | ||
|
||
function getNonce(address from) | ||
public view override | ||
returns (uint256) { | ||
return nonces[from]; | ||
} | ||
|
||
constructor() { | ||
|
||
string memory requestType = string(abi.encodePacked('ForwardRequest(', GENERIC_PARAMS, ')')); | ||
registerRequestTypeInternal(requestType); | ||
} | ||
|
||
function verify( | ||
ForwardRequest calldata req, | ||
bytes32 domainSeparator, | ||
bytes32 requestTypeHash, | ||
bytes calldata suffixData, | ||
bytes calldata sig) | ||
external override view { | ||
|
||
_verifyNonce(req); | ||
_verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); | ||
} | ||
|
||
function execute( | ||
ForwardRequest calldata req, | ||
bytes32 domainSeparator, | ||
bytes32 requestTypeHash, | ||
bytes calldata suffixData, | ||
bytes calldata sig | ||
) | ||
external payable | ||
override | ||
returns (bool success, bytes memory ret) { | ||
_verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); | ||
_verifyAndUpdateNonce(req); | ||
|
||
require(req.validUntil == 0 || req.validUntil > block.number, 'FWD: request expired'); | ||
|
||
uint gasForTransfer = 0; | ||
if ( req.value != 0 ) { | ||
gasForTransfer = 40000; //buffer in case we need to move eth after the transaction. | ||
} | ||
bytes memory callData = abi.encodePacked(req.data, req.from); | ||
require(gasleft()*63/64 >= req.gas + gasForTransfer, 'FWD: insufficient gas'); | ||
// solhint-disable-next-line avoid-low-level-calls | ||
(success,ret) = req.to.call{gas : req.gas, value : req.value}(callData); | ||
if ( req.value != 0 && address(this).balance>0 ) { | ||
// can't fail: req.from signed (off-chain) the request, so it must be an EOA... | ||
payable(req.from).transfer(address(this).balance); | ||
} | ||
|
||
return (success,ret); | ||
} | ||
|
||
|
||
function _verifyNonce(ForwardRequest calldata req) internal view { | ||
require(nonces[req.from] == req.nonce, 'FWD: nonce mismatch'); | ||
} | ||
|
||
function _verifyAndUpdateNonce(ForwardRequest calldata req) internal { | ||
require(nonces[req.from]++ == req.nonce, 'FWD: nonce mismatch'); | ||
} | ||
|
||
function registerRequestType(string calldata typeName, string calldata typeSuffix) external override { | ||
|
||
for (uint i = 0; i < bytes(typeName).length; i++) { | ||
bytes1 c = bytes(typeName)[i]; | ||
require(c != '(' && c != ')', 'FWD: invalid typename'); | ||
} | ||
|
||
string memory requestType = string(abi.encodePacked(typeName, '(', GENERIC_PARAMS, ',', typeSuffix)); | ||
registerRequestTypeInternal(requestType); | ||
} | ||
|
||
function registerDomainSeparator(string calldata name, string calldata version) external override { | ||
uint256 chainId; | ||
/* solhint-disable-next-line no-inline-assembly */ | ||
assembly { chainId := chainid() } | ||
|
||
bytes memory domainValue = abi.encode( | ||
keccak256(bytes(EIP712_DOMAIN_TYPE)), | ||
keccak256(bytes(name)), | ||
keccak256(bytes(version)), | ||
chainId, | ||
address(this)); | ||
|
||
bytes32 domainHash = keccak256(domainValue); | ||
|
||
domains[domainHash] = true; | ||
emit DomainRegistered(domainHash, domainValue); | ||
} | ||
|
||
function registerRequestTypeInternal(string memory requestType) internal { | ||
|
||
bytes32 requestTypehash = keccak256(bytes(requestType)); | ||
typeHashes[requestTypehash] = true; | ||
emit RequestTypeRegistered(requestTypehash, requestType); | ||
} | ||
|
||
function _verifySig( | ||
ForwardRequest calldata req, | ||
bytes32 domainSeparator, | ||
bytes32 requestTypeHash, | ||
bytes calldata suffixData, | ||
bytes calldata sig) | ||
internal | ||
view | ||
{ | ||
require(domains[domainSeparator], 'FWD: unregistered domain sep.'); | ||
require(typeHashes[requestTypeHash], 'FWD: unregistered typehash'); | ||
bytes32 digest = keccak256(abi.encodePacked( | ||
'\x19\x01', domainSeparator, | ||
keccak256(_getEncoded(req, requestTypeHash, suffixData)) | ||
)); | ||
require(digest.recover(sig) == req.from, 'FWD: signature mismatch'); | ||
} | ||
|
||
function _getEncoded( | ||
ForwardRequest calldata req, | ||
bytes32 requestTypeHash, | ||
bytes calldata suffixData | ||
) | ||
public | ||
view | ||
returns ( | ||
bytes memory | ||
) { | ||
// we use encodePacked since we append suffixData as-is, not as dynamic param. | ||
// still, we must make sure all first params are encoded as abi.encode() | ||
// would encode them - as 256-bit-wide params. | ||
return abi.encodePacked( | ||
requestTypeHash, | ||
uint256(uint160(req.from)), | ||
uint256(uint160(req.to)), | ||
req.value, | ||
req.gas, | ||
req.nonce, | ||
keccak256(req.data), | ||
req.validUntil, | ||
suffixData | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
const { ethers, web3 } = require('hardhat') | ||
const { it, describe, before } = require('mocha') | ||
const { assert } = require('chai') | ||
const constants = require('../helpers/constants.js') | ||
const testUtils = require('../helpers/utils.js') | ||
|
||
async function deployContract(contract, deployer, libraries, args) { | ||
const C = await ethers.getContractFactory(contract, { libraries }) | ||
const signer = C.connect(deployer) | ||
const c = await signer.deploy() | ||
await c.deployed() | ||
const tx = await c.initialize(...args) | ||
await tx.wait() | ||
return c | ||
} | ||
|
||
describe('using ethers with OpenGSN forwarder', () => { | ||
let didRegistry, nft | ||
let accounts | ||
const value = 'https://nevermined.io/did/nevermined/test-attr-example.txt' | ||
const nftMetadataURL = 'https://nevermined.io/metadata.json' | ||
let account | ||
let forwarder | ||
let separator | ||
before(async () => { | ||
const deployer = await ethers.provider.getSigner(8) | ||
const deploymentProvider = ethers.provider | ||
const Forwarder = await ethers.getContractFactory('Forwarder') | ||
const signer = Forwarder.connect(deployer) | ||
forwarder = await signer.deploy() | ||
await forwarder.deployed() | ||
|
||
await forwarder.registerDomainSeparator('Nevermined', '1') | ||
|
||
const keccak = a => ethers.utils.solidityKeccak256(['bytes'], [a]) | ||
|
||
const pake = ethers.utils.defaultAbiCoder.encode(['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], | ||
[ | ||
keccak(Buffer.from('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')), | ||
keccak(Buffer.from('Nevermined')), | ||
keccak(Buffer.from('1')), | ||
await web3.eth.getChainId(), | ||
forwarder.address | ||
]) | ||
|
||
separator = ethers.utils.keccak256(pake) | ||
|
||
accounts = await web3.eth.getAccounts() | ||
const owner = accounts[0] | ||
const governor = accounts[1] | ||
|
||
const nvmConfig = await deployContract('NeverminedConfig', deployer, {}, [owner, governor, false]) | ||
await nvmConfig.connect( | ||
await deploymentProvider.getSigner(governor)).setTrustedForwarder(forwarder.address) | ||
|
||
didRegistry = await deployContract( | ||
'DIDRegistry', | ||
deployer, | ||
{}, | ||
[owner, constants.address.zero, constants.address.zero, nvmConfig.address, constants.address.zero] | ||
) | ||
nft = await deployContract('NFT1155Upgradeable', deployer, {}, [owner, didRegistry.address, '', '', '']) | ||
nft.connect(await deploymentProvider.getSigner(owner)).setNvmConfigAddress(nvmConfig.address) | ||
}) | ||
|
||
describe('Register an Asset with a DID', () => { | ||
it('Should mint and burn NFTs after initialization', async () => { | ||
const didSeed = testUtils.generateId() | ||
account = accounts[4] | ||
const signer = await ethers.provider.getSigner(4) | ||
const did = await didRegistry.hashDID(didSeed, account) | ||
const checksum = testUtils.generateId() | ||
|
||
const req = { | ||
from: account, | ||
to: didRegistry.address, | ||
value: '0', | ||
nonce: 0, | ||
validUntil: 0, | ||
gas: 5000000, | ||
data: didRegistry.interface.encodeFunctionData('registerMintableDID(bytes32,address,bytes32,address[],string,uint256,uint256,bytes32,string,string)', [ | ||
didSeed, nft.address, checksum, [], value, 20, 0, constants.activities.GENERATED, nftMetadataURL, '' | ||
]) | ||
} | ||
|
||
const domain = { | ||
name: 'Nevermined', | ||
version: '1', | ||
chainId: await web3.eth.getChainId(), | ||
verifyingContract: forwarder.address | ||
} | ||
|
||
const types = { | ||
ForwardRequest: [ | ||
{ name: 'from', type: 'address' }, | ||
{ name: 'to', type: 'address' }, | ||
{ name: 'value', type: 'uint256' }, | ||
{ name: 'gas', type: 'uint256' }, | ||
{ name: 'nonce', type: 'uint256' }, | ||
{ name: 'data', type: 'bytes' }, | ||
{ name: 'validUntil', type: 'uint256' } | ||
] | ||
} | ||
|
||
const sig = await signer._signTypedData(domain, types, req) | ||
|
||
await forwarder.connect(ethers.provider.getSigner(5)).execute( | ||
req, | ||
separator, | ||
'0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4', | ||
[], | ||
sig | ||
) | ||
|
||
const didEntry = await didRegistry.getDIDRegister(did) | ||
assert.strictEqual(account, didEntry.owner) | ||
|
||
const nftAttr = await nft.getNFTAttributes(did) | ||
assert.strictEqual(nftMetadataURL, nftAttr.nftURI) | ||
|
||
/* | ||
console.log('Minting') | ||
await nft['mint(uint256,uint256)'](did, 20, { from: account, gasLimit: 1000000 }) | ||
console.log('Balance') | ||
let balance = await nft.balanceOf(account, did) | ||
assert.strictEqual(20, balance.toNumber()) | ||
console.log('Burn') | ||
await nft['burn(uint256,uint256)'](did, 5, { from: account, gasLimit: 1000000 }) | ||
balance = await nft.balanceOf(account, did) | ||
assert.strictEqual(15, balance.toNumber()) | ||
const _nftURI = await nft.uri(did) | ||
assert.strictEqual(nftMetadataURL, _nftURI) | ||
*/ | ||
}) | ||
}) | ||
}) |