Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal support for ERC2771 (GSNv2) #2508

Merged
merged 31 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eea140d
Add code for a minimal GSNv2 compliant forwarder and recipient
Amxx Feb 2, 2021
0fef50d
Merge branch 'master' into feature/GSNv2
Amxx Feb 4, 2021
b1823d1
fix erc712 hash computation
Amxx Feb 8, 2021
a0efed6
add testing for gsnv2
Amxx Feb 8, 2021
15839a7
changelog entry
Amxx Feb 8, 2021
b7183a4
fix lint
Amxx Feb 8, 2021
a74c5c7
Apply suggestions from code review
Amxx Feb 8, 2021
7731f16
Merge branch 'master' into feature/GSNv2
Amxx Feb 9, 2021
138fbd1
use ERC2770 for gsnv2 forwarder
Amxx Feb 9, 2021
6a02d75
rename gsnv2 into metatx
Amxx Feb 9, 2021
50c2fa4
change event signature and fix import path
Amxx Feb 9, 2021
435324f
fix lint
Amxx Feb 9, 2021
0abb253
provide 2 version of the forwarder
Amxx Feb 9, 2021
3a1fed9
remove gsnv2 mention from minimalforwarder domain
Amxx Feb 11, 2021
df6d730
make _typehashes & _domains private
Amxx Feb 12, 2021
1ad190a
Remove ERC2770 forwarder for now
Amxx Feb 15, 2021
eebff1b
improve coverage of metatx
Amxx Feb 17, 2021
ef33ec7
Merge branch 'master' into feature/GSNv2
Amxx Feb 17, 2021
c9a9c2d
Update contracts/metatx/MinimalForwarder.sol
Amxx Feb 18, 2021
c3b180e
refactor EIP721 draft to use ECDSA tooling
Amxx Feb 18, 2021
aa344d6
Merge branch 'feature/GSNv2' of github.com:Amxx/openzeppelin-contract…
Amxx Feb 18, 2021
e88b839
remove mention to gsn in changelog entry
Amxx Feb 18, 2021
63341e9
improve inline documentation
Amxx Feb 18, 2021
3d2e907
Merge branch 'master' into feature/GSNv2
Amxx Feb 18, 2021
312bad8
Merge branch 'master' into feature/GSNv2
Amxx Feb 19, 2021
a3870ff
fix import path
Amxx Feb 19, 2021
338e640
rename BaseRelayRecipient → ERC2771Context
Amxx Feb 19, 2021
99914ef
rename mock file
frangio Feb 19, 2021
fd309c3
fix naming in changelog
frangio Feb 19, 2021
d06cd39
improve docs
frangio Feb 19, 2021
915db92
Merge branch 'master' into feature/GSNv2
frangio Feb 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* `Context`: making `_msgData` return `bytes calldata` instead of `bytes memory` ([#2492](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2492))
* `ERC20`: Removed the `_setDecimals` function and the storage slot associated to decimals. ([#2502](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2502))
* `Strings`: addition of a `toHexString` function. ([#2504](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2504))
* `GSNv2`: add a `BaseRelayRecipient` and a `MinimalForwarder` for experimenting with GSNv2 ([#2508](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2508))

## 3.4.0 (2021-02-02)

Expand Down
9 changes: 9 additions & 0 deletions contracts/cryptography/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,13 @@ library ECDSA {
// enforced by the type signature above
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}

/**
* @dev TODO.
*
* See {recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
}
37 changes: 37 additions & 0 deletions contracts/metatx/BaseRelayRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/*
* @dev Context variant with ERC2771 support.
*/
abstract contract BaseRelayRecipient is Context {
frangio marked this conversation as resolved.
Show resolved Hide resolved
address immutable _trustedForwarder;

constructor(address trustedForwarder) {
_trustedForwarder = trustedForwarder;
}

function isTrustedForwarder(address forwarder) public view virtual returns(bool) {
return forwarder == _trustedForwarder;
}

function _msgSender() internal view virtual override returns (address sender) {
if (isTrustedForwarder(msg.sender)) {
// The assembly code is more direct than the Solidity version using `abi.decode`.
assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) }
} else {
return super._msgSender();
}
}

function _msgData() internal view virtual override returns (bytes calldata) {
if (isTrustedForwarder(msg.sender)) {
return msg.data[:msg.data.length-20];
} else {
return super._msgData();
}
}
}
57 changes: 57 additions & 0 deletions contracts/metatx/MinimalForwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../cryptography/ECDSA.sol";
import "../drafts/EIP712.sol";

/*
* @dev Minimal forwarder for GSNv2
*/
contract MinimalForwarder is EIP712 {
using ECDSA for bytes32;

struct ForwardRequest {
address from;
address to;
uint256 value;
uint256 gas;
uint256 nonce;
bytes data;
}

bytes32 private constant TYPEHASH = keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)");

mapping(address => uint256) private _nonces;

constructor() EIP712("MinimalForwarder", "0.0.1") {}

function getNonce(address from) public view returns (uint256) {
return _nonces[from];
}

function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
address signer = _hashTypedDataV4(keccak256(abi.encode(
TYPEHASH,
req.from,
req.to,
req.value,
req.gas,
req.nonce,
keccak256(req.data)
))).recover(signature);
return _nonces[req.from] == req.nonce && signer == req.from;
}

function execute(ForwardRequest calldata req, bytes calldata signature) public payable returns (bool, bytes memory) {
require(verify(req, signature), "MinimalForwarder: signature does not match request");
_nonces[req.from] = req.nonce + 1;

// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}(abi.encodePacked(req.data, req.from));
// Check gas: https://ronan.eth.link/blog/ethereum-gas-dangers/
Amxx marked this conversation as resolved.
Show resolved Hide resolved
assert(gasleft() > req.gas / 63);
Amxx marked this conversation as resolved.
Show resolved Hide resolved

return (success, returndata);
}
}
19 changes: 19 additions & 0 deletions contracts/mocks/BaseRelayRecipientMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./ContextMock.sol";
import "../metatx/BaseRelayRecipient.sol";

// By inheriting from BaseRelayRecipient, Context's internal functions are overridden automatically
contract BaseRelayRecipientMock is ContextMock, BaseRelayRecipient {
constructor(address trustedForwarder) BaseRelayRecipient(trustedForwarder) {}

function _msgSender() internal override(Context, BaseRelayRecipient) view virtual returns (address) {
return BaseRelayRecipient._msgSender();
}

function _msgData() internal override(Context, BaseRelayRecipient) view virtual returns (bytes calldata) {
return BaseRelayRecipient._msgData();
}
}
113 changes: 113 additions & 0 deletions test/metatx/BaseRelayRecipient.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const { EIP712Domain } = require('../helpers/eip712');

const { expectEvent } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const BaseRelayRecipientMock = artifacts.require('BaseRelayRecipientMock');
const MinimalForwarder = artifacts.require('MinimalForwarder');
const ContextMockCaller = artifacts.require('ContextMockCaller');

const { shouldBehaveLikeRegularContext } = require('../GSN/Context.behavior');

const name = 'MinimalForwarder';
const version = '0.0.1';

contract('BaseRelayRecipient', function (accounts) {
beforeEach(async function () {
this.forwarder = await MinimalForwarder.new();
this.recipient = await BaseRelayRecipientMock.new(this.forwarder.address);

this.domain = {
name,
version,
chainId: await web3.eth.getChainId(),
verifyingContract: this.forwarder.address,
};
this.types = {
EIP712Domain,
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' },
],
};
});

it('recognize trusted forwarder', async function () {
expect(await this.recipient.isTrustedForwarder(this.forwarder.address));
});

context('when called directly', function () {
beforeEach(async function () {
this.context = this.recipient; // The Context behavior expects the contract in this.context
this.caller = await ContextMockCaller.new();
});

shouldBehaveLikeRegularContext(...accounts);
});

context('when receiving a relayed call', function () {
beforeEach(async function () {
this.wallet = Wallet.generate();
this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString());
this.data = {
types: this.types,
domain: this.domain,
primaryType: 'ForwardRequest',
};
});

describe('msgSender', function () {
it('returns the relayed transaction original sender', async function () {
const data = this.recipient.contract.methods.msgSender().encodeABI();

const req = {
from: this.sender,
to: this.recipient.address,
value: '0',
gas: '100000',
nonce: (await this.forwarder.getNonce(this.sender)).toString(),
data,
};

const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } });

// rejected by lint :/
// expect(await this.forwarder.verify(req, sign)).to.be.true;

const { tx } = await this.forwarder.execute(req, sign);
await expectEvent.inTransaction(tx, BaseRelayRecipientMock, 'Sender', { sender: this.sender });
});
});

describe('msgData', function () {
it('returns the relayed transaction original data', async function () {
const integerValue = '42';
const stringValue = 'OpenZeppelin';
const data = this.recipient.contract.methods.msgData(integerValue, stringValue).encodeABI();

const req = {
from: this.sender,
to: this.recipient.address,
value: '0',
gas: '100000',
nonce: (await this.forwarder.getNonce(this.sender)).toString(),
data,
};

const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } });

// rejected by lint :/
// expect(await this.forwarder.verify(req, sign)).to.be.true;

const { tx } = await this.forwarder.execute(req, sign);
await expectEvent.inTransaction(tx, BaseRelayRecipientMock, 'Data', { data, integerValue, stringValue });
});
});
});
});
Loading