Skip to content

Commit

Permalink
Merge pull request #498 from nevermined-io/feat/opengsn-relay
Browse files Browse the repository at this point in the history
Opengsn forwarding sample
  • Loading branch information
aaitor authored Sep 18, 2023
2 parents a19adbd + b1e1c71 commit 2e87707
Show file tree
Hide file tree
Showing 2 changed files with 383 additions and 0 deletions.
243 changes: 243 additions & 0 deletions contracts/test/Forwarder.sol
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
);
}
}
140 changes: 140 additions & 0 deletions test/unit/Relay.test.js
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)
*/
})
})
})

0 comments on commit 2e87707

Please sign in to comment.