From 3fb8271305581ca8f83d9f636f731c68b1f36f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20M=C3=A4kel=C3=A4?= Date: Wed, 6 Sep 2023 14:07:05 +0300 Subject: [PATCH 1/6] adding test --- test/unit/Relay.test.js | 145 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 test/unit/Relay.test.js diff --git a/test/unit/Relay.test.js b/test/unit/Relay.test.js new file mode 100644 index 00000000..a9039da9 --- /dev/null +++ b/test/unit/Relay.test.js @@ -0,0 +1,145 @@ +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') +const Web3HttpProvider = require('web3-providers-http') + +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 + let etherProvider + // let web3provider + const value = 'https://nevermined.io/did/nevermined/test-attr-example.txt' + const nftMetadataURL = 'https://nevermined.io/metadata.json' + let account + let forwarder + 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") + + + 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() + console.log('Make DID') + account = accounts[4] + const signer = await ethers.provider.getSigner(4) + const did = await didRegistry.hashDID(didSeed, account) + const checksum = testUtils.generateId() + + console.log('Registering DID') + + /* + await didRegistry['registerMintableDID(bytes32,address,bytes32,address[],string,uint256,uint256,bytes32,string,string)']( + didSeed, nft.address, checksum, [], value, 20, 0, constants.activities.GENERATED, nftMetadataURL, '', { from: account, gasLimit: 5000000 }) + */ + 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, '' + ]) + } + console.log('Req made', req) + + let data = await forwarder._getEncoded(req, "0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4", []) + console.log('Data', data) + + let domain = { + name: "Nevermined", + version: "1", + chainId: await web3.eth.getChainId(), + verifyingContract: forwarder.address, + } + console.log('Domain', domain) + + 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" }, + ], + } + + // let sig = await web3.eth.sign(data, account) + let sig = await signer._signTypedData(domain, types, req) + console.log('Signature', sig) + + await forwarder.execute( + req, + "0xc3f2eba0fc9b898fe103e579c225132f6b31c351c0e50ddc84c09ee64e8c5f73", + "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) + */ + }) + }) +}) From 3eb2de3209f76ee6d72ec44306591e42db03b7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20M=C3=A4kel=C3=A4?= Date: Wed, 6 Sep 2023 14:08:34 +0300 Subject: [PATCH 2/6] seems to work --- test/unit/Relay.test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/Relay.test.js b/test/unit/Relay.test.js index a9039da9..756e7f30 100644 --- a/test/unit/Relay.test.js +++ b/test/unit/Relay.test.js @@ -1,9 +1,8 @@ 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') -const Web3HttpProvider = require('web3-providers-http') +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 }) From be5d699639710fa00ceed57949d575c23c5f429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20M=C3=A4kel=C3=A4?= Date: Wed, 6 Sep 2023 14:38:13 +0300 Subject: [PATCH 3/6] more mangling --- test/unit/Relay.test.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/unit/Relay.test.js b/test/unit/Relay.test.js index 756e7f30..abecbd3f 100644 --- a/test/unit/Relay.test.js +++ b/test/unit/Relay.test.js @@ -23,6 +23,7 @@ describe('using ethers with OpenGSN forwarder', () => { 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 @@ -33,6 +34,20 @@ describe('using ethers with OpenGSN forwarder', () => { await forwarder.registerDomainSeparator("Nevermined", "1") + let keccak = a => ethers.utils.solidityKeccak256(['bytes'], [a]) + + let 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 + ]) + + console.log('pake', pake.length, pake) + separator = ethers.utils.keccak256(pake) + console.log('separator', separator, ) accounts = await web3.eth.getAccounts() const owner = accounts[0] @@ -110,7 +125,7 @@ describe('using ethers with OpenGSN forwarder', () => { await forwarder.execute( req, - "0xc3f2eba0fc9b898fe103e579c225132f6b31c351c0e50ddc84c09ee64e8c5f73", + separator, "0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4", [], sig From 4dabc7661dd6c46ddb3bf192e65efcd1c067a55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20M=C3=A4kel=C3=A4?= Date: Wed, 6 Sep 2023 14:44:59 +0300 Subject: [PATCH 4/6] lint --- test/unit/Relay.test.js | 59 ++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/test/unit/Relay.test.js b/test/unit/Relay.test.js index abecbd3f..3c904fd0 100644 --- a/test/unit/Relay.test.js +++ b/test/unit/Relay.test.js @@ -17,8 +17,6 @@ async function deployContract(contract, deployer, libraries, args) { describe('using ethers with OpenGSN forwarder', () => { let didRegistry, nft let accounts - let etherProvider - // let web3provider const value = 'https://nevermined.io/did/nevermined/test-attr-example.txt' const nftMetadataURL = 'https://nevermined.io/metadata.json' let account @@ -27,16 +25,16 @@ describe('using ethers with OpenGSN forwarder', () => { before(async () => { const deployer = await ethers.provider.getSigner(8) const deploymentProvider = ethers.provider - const Forwarder = await ethers.getContractFactory("Forwarder") + const Forwarder = await ethers.getContractFactory('Forwarder') const signer = Forwarder.connect(deployer) forwarder = await signer.deploy() await forwarder.deployed() - await forwarder.registerDomainSeparator("Nevermined", "1") + await forwarder.registerDomainSeparator('Nevermined', '1') - let keccak = a => ethers.utils.solidityKeccak256(['bytes'], [a]) + const keccak = a => ethers.utils.solidityKeccak256(['bytes'], [a]) - let pake = ethers.utils.defaultAbiCoder.encode(["bytes32", "bytes32", "bytes32", "uint256", "address"], + 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')), @@ -44,10 +42,8 @@ describe('using ethers with OpenGSN forwarder', () => { await web3.eth.getChainId(), forwarder.address ]) - - console.log('pake', pake.length, pake) + separator = ethers.utils.keccak256(pake) - console.log('separator', separator, ) accounts = await web3.eth.getAccounts() const owner = accounts[0] @@ -65,28 +61,20 @@ describe('using ethers with OpenGSN forwarder', () => { ) 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() - console.log('Make DID') account = accounts[4] const signer = await ethers.provider.getSigner(4) const did = await didRegistry.hashDID(didSeed, account) const checksum = testUtils.generateId() - console.log('Registering DID') - - /* - await didRegistry['registerMintableDID(bytes32,address,bytes32,address[],string,uint256,uint256,bytes32,string,string)']( - didSeed, nft.address, checksum, [], value, 20, 0, constants.activities.GENERATED, nftMetadataURL, '', { from: account, gasLimit: 5000000 }) - */ const req = { from: account, to: didRegistry.address, - value: "0", + value: '0', nonce: 0, validUntil: 0, gas: 5000000, @@ -94,39 +82,32 @@ describe('using ethers with OpenGSN forwarder', () => { didSeed, nft.address, checksum, [], value, 20, 0, constants.activities.GENERATED, nftMetadataURL, '' ]) } - console.log('Req made', req) - - let data = await forwarder._getEncoded(req, "0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4", []) - console.log('Data', data) - let domain = { - name: "Nevermined", - version: "1", + const domain = { + name: 'Nevermined', + version: '1', chainId: await web3.eth.getChainId(), - verifyingContract: forwarder.address, + verifyingContract: forwarder.address } - console.log('Domain', domain) 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" }, - ], + { 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' } + ] } - // let sig = await web3.eth.sign(data, account) - let sig = await signer._signTypedData(domain, types, req) - console.log('Signature', sig) + const sig = await signer._signTypedData(domain, types, req) await forwarder.execute( req, separator, - "0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4", + '0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4', [], sig ) From ff65bdf8ef6a497404ad56c3e2bc7edc0cc28791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20M=C3=A4kel=C3=A4?= Date: Wed, 6 Sep 2023 14:49:21 +0300 Subject: [PATCH 5/6] cleanup --- test/unit/Relay.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Relay.test.js b/test/unit/Relay.test.js index 3c904fd0..a410d690 100644 --- a/test/unit/Relay.test.js +++ b/test/unit/Relay.test.js @@ -104,7 +104,7 @@ describe('using ethers with OpenGSN forwarder', () => { const sig = await signer._signTypedData(domain, types, req) - await forwarder.execute( + await forwarder.connect(ethers.provider.getSigner(5)).execute( req, separator, '0x2510fc5e187085770200b027d9f2cc4b930768f3b2bd81daafb71ffeb53d21c4', From b1e1c71c7c92965f55a76d68d014e2d486865460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20M=C3=A4kel=C3=A4?= Date: Wed, 6 Sep 2023 14:52:11 +0300 Subject: [PATCH 6/6] forwarder contract from opengsn --- contracts/test/Forwarder.sol | 243 +++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 contracts/test/Forwarder.sol diff --git a/contracts/test/Forwarder.sol b/contracts/test/Forwarder.sol new file mode 100644 index 00000000..3a26ee01 --- /dev/null +++ b/contracts/test/Forwarder.sol @@ -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 + ); + } +} \ No newline at end of file