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 all 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 @@ -12,6 +12,7 @@
* `Initializable`: Make initializer check stricter during construction. ([#2531](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2531))
* `ERC721`: remove enumerability of tokens from the base implementation. This feature is now provided separately through the `ERC721Enumerable` extension. ([#2511](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2511))
* `AccessControl`: removed enumerability by default for a more lightweight contract. It is now opt-in through `AccessControlEnumerable`. ([#2512](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2512))
* Meta Transactions: add `ERC2771Context` and a `MinimalForwarder` for meta-transactions. ([#2508](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2508))

## 3.4.0 (2021-02-02)

Expand Down
19 changes: 16 additions & 3 deletions contracts/cryptography/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ library ECDSA {

/**
* @dev Returns an Ethereum Signed Message, created from a `hash`. This
* replicates the behavior of the
* https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`]
* JSON-RPC method.
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
Expand All @@ -83,4 +83,17 @@ library ECDSA {
// enforced by the type signature above
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}

/**
* @dev Returns an Ethereum Signed Typed Data, created from a
* `domainSeparator` and a `structHash`. This produces hash corresponding
* to the one signed with the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`]
* JSON-RPC method as part of EIP-712.
*
* See {recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
}
4 changes: 3 additions & 1 deletion contracts/drafts/EIP712.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

pragma solidity ^0.8.0;

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

/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
*
Expand Down Expand Up @@ -95,6 +97,6 @@ abstract contract EIP712 {
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
}
}
37 changes: 37 additions & 0 deletions contracts/metatx/ERC2771Context.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 ERC2771Context is Context {
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();
}
}
}
58 changes: 58 additions & 0 deletions contracts/metatx/MinimalForwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

/*
* @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}.
*/
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));
// Validate that the relayer has sent enough gas for the call.
// See https://ronan.eth.link/blog/ethereum-gas-dangers/
assert(gasleft() > req.gas / 63);
Amxx marked this conversation as resolved.
Show resolved Hide resolved

return (success, returndata);
}
}
12 changes: 12 additions & 0 deletions contracts/metatx/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
= Meta Transactions

[.readme-notice]
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/math

== Core

{{ERC2771Context}}

== Utils

{{MinimalForwarder}}
19 changes: 19 additions & 0 deletions contracts/mocks/ERC2771ContextMock.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/ERC2771Context.sol";

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

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

function _msgData() internal override(Context, ERC2771Context) view virtual returns (bytes calldata) {
return ERC2771Context._msgData();
}
}
113 changes: 113 additions & 0 deletions test/metatx/ERC2771Context.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 ERC2771ContextMock = artifacts.require('ERC2771ContextMock');
const MinimalForwarder = artifacts.require('MinimalForwarder');
const ContextMockCaller = artifacts.require('ContextMockCaller');

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

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

contract('ERC2771Context', function (accounts) {
beforeEach(async function () {
this.forwarder = await MinimalForwarder.new();
this.recipient = await ERC2771ContextMock.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, ERC2771ContextMock, '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, ERC2771ContextMock, 'Data', { data, integerValue, stringValue });
});
});
});
});
Loading