-
Notifications
You must be signed in to change notification settings - Fork 11.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Minimal support for ERC2771 (GSNv2) (#2508)
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
- Loading branch information
Showing
9 changed files
with
425 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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 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 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 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 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 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 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 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.