-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Minimal support for ERC2771 (GSNv2) #2508
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
Merged
Merged
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 0fef50d
Merge branch 'master' into feature/GSNv2
Amxx b1823d1
fix erc712 hash computation
Amxx a0efed6
add testing for gsnv2
Amxx 15839a7
changelog entry
Amxx b7183a4
fix lint
Amxx a74c5c7
Apply suggestions from code review
Amxx 7731f16
Merge branch 'master' into feature/GSNv2
Amxx 138fbd1
use ERC2770 for gsnv2 forwarder
Amxx 6a02d75
rename gsnv2 into metatx
Amxx 50c2fa4
change event signature and fix import path
Amxx 435324f
fix lint
Amxx 0abb253
provide 2 version of the forwarder
Amxx 3a1fed9
remove gsnv2 mention from minimalforwarder domain
Amxx df6d730
make _typehashes & _domains private
Amxx 1ad190a
Remove ERC2770 forwarder for now
Amxx eebff1b
improve coverage of metatx
Amxx ef33ec7
Merge branch 'master' into feature/GSNv2
Amxx c9a9c2d
Update contracts/metatx/MinimalForwarder.sol
Amxx c3b180e
refactor EIP721 draft to use ECDSA tooling
Amxx aa344d6
Merge branch 'feature/GSNv2' of github.com:Amxx/openzeppelin-contract…
Amxx e88b839
remove mention to gsn in changelog entry
Amxx 63341e9
improve inline documentation
Amxx 3d2e907
Merge branch 'master' into feature/GSNv2
Amxx 312bad8
Merge branch 'master' into feature/GSNv2
Amxx a3870ff
fix import path
Amxx 338e640
rename BaseRelayRecipient → ERC2771Context
Amxx 99914ef
rename mock file
frangio fd309c3
fix naming in changelog
frangio d06cd39
improve docs
frangio 915db92
Merge branch 'master' into feature/GSNv2
frangio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,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(); | ||
| } | ||
| } | ||
| } |
This file contains hidden or 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,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); | ||
|
|
||
| return (success, returndata); | ||
| } | ||
| } | ||
This file contains hidden or 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,12 @@ | ||
| = Meta Transactions | ||
|
|
||
| [.readme-notice] | ||
| NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/math | ||
|
|
||
| == Core | ||
|
|
||
| {{ERC2771Context}} | ||
|
|
||
| == Utils | ||
|
|
||
| {{MinimalForwarder}} |
This file contains hidden or 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,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(); | ||
| } | ||
| } |
This file contains hidden or 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,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 }); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.