Skip to content

Commit 849e186

Browse files
nkrishangKrishang Nadgauda
and
Krishang Nadgauda
authored
Gasless ContractPublisher without chainid in typehash (#235)
* Forwarder with no chainid in typehash * replace repo forwarder with prod forwarder code * Add EIP712ChainlessDomain to set of modified oz presets * replace ForwarderNoChainid -> ForwarderChainlessDomain * move forwarder related code to dedicated directory * fix import paths in tests * update namehash in forwarder tets * run prettier * docs update * move ForwarderEOAOnly to dedicated forwarder dir Co-authored-by: Krishang Nadgauda <nkrishang@Krishangs-MacBook-Pro.local>
1 parent 9eb1a83 commit 849e186

14 files changed

+531
-24
lines changed

contracts/Forwarder.sol

Lines changed: 0 additions & 12 deletions
This file was deleted.

contracts/forwarder/Forwarder.sol

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6+
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
7+
8+
/*
9+
* @dev Minimal forwarder for GSNv2
10+
*/
11+
contract Forwarder is EIP712 {
12+
using ECDSA for bytes32;
13+
14+
struct ForwardRequest {
15+
address from;
16+
address to;
17+
uint256 value;
18+
uint256 gas;
19+
uint256 nonce;
20+
bytes data;
21+
}
22+
23+
bytes32 private constant TYPEHASH =
24+
keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)");
25+
26+
mapping(address => uint256) private _nonces;
27+
28+
constructor() EIP712("GSNv2 Forwarder", "0.0.1") {}
29+
30+
function getNonce(address from) public view returns (uint256) {
31+
return _nonces[from];
32+
}
33+
34+
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
35+
address signer = _hashTypedDataV4(
36+
keccak256(abi.encode(TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data)))
37+
).recover(signature);
38+
39+
return _nonces[req.from] == req.nonce && signer == req.from;
40+
}
41+
42+
function execute(ForwardRequest calldata req, bytes calldata signature)
43+
public
44+
payable
45+
returns (bool, bytes memory)
46+
{
47+
require(verify(req, signature), "MinimalForwarder: signature does not match request");
48+
_nonces[req.from] = req.nonce + 1;
49+
50+
// solhint-disable-next-line avoid-low-level-calls
51+
(bool success, bytes memory result) = req.to.call{ gas: req.gas, value: req.value }(
52+
abi.encodePacked(req.data, req.from)
53+
);
54+
55+
if (!success) {
56+
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
57+
if (result.length < 68) revert("Transaction reverted silently");
58+
assembly {
59+
result := add(result, 0x04)
60+
}
61+
revert(abi.decode(result, (string)));
62+
}
63+
// Check gas: https://ronan.eth.link/blog/ethereum-gas-dangers/
64+
assert(gasleft() > req.gas / 63);
65+
return (success, result);
66+
}
67+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v4.5.0) (metatx/MinimalForwarder.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
7+
import "../openzeppelin-presets/cryptography/EIP712ChainlessDomain.sol";
8+
9+
/**
10+
* @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}.
11+
*/
12+
contract ForwarderChainlessDomain is EIP712ChainlessDomain {
13+
using ECDSA for bytes32;
14+
15+
struct ForwardRequest {
16+
address from;
17+
address to;
18+
uint256 value;
19+
uint256 gas;
20+
uint256 nonce;
21+
bytes data;
22+
uint256 chainid;
23+
}
24+
25+
bytes32 private constant _TYPEHASH =
26+
keccak256(
27+
"ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 chainid)"
28+
);
29+
30+
mapping(address => uint256) private _nonces;
31+
32+
constructor() EIP712ChainlessDomain("GSNv2 Forwarder", "0.0.1") {}
33+
34+
function getNonce(address from) public view returns (uint256) {
35+
return _nonces[from];
36+
}
37+
38+
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
39+
address signer = _hashTypedDataV4(
40+
keccak256(
41+
abi.encode(
42+
_TYPEHASH,
43+
req.from,
44+
req.to,
45+
req.value,
46+
req.gas,
47+
req.nonce,
48+
keccak256(req.data),
49+
block.chainid
50+
)
51+
)
52+
).recover(signature);
53+
return _nonces[req.from] == req.nonce && signer == req.from;
54+
}
55+
56+
function execute(ForwardRequest calldata req, bytes calldata signature)
57+
public
58+
payable
59+
returns (bool, bytes memory)
60+
{
61+
// require(req.chainid == block.chainid, "MinimalForwarder: invalid chainId");
62+
require(verify(req, signature), "MinimalForwarder: signature does not match request");
63+
_nonces[req.from] = req.nonce + 1;
64+
65+
(bool success, bytes memory returndata) = req.to.call{ gas: req.gas, value: req.value }(
66+
abi.encodePacked(req.data, req.from)
67+
);
68+
69+
// Validate that the relayer has sent enough gas for the call.
70+
// See https://ronan.eth.link/blog/ethereum-gas-dangers/
71+
if (gasleft() <= req.gas / 63) {
72+
// We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since
73+
// neither revert or assert consume all gas since Solidity 0.8.0
74+
// https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require
75+
assembly {
76+
invalid()
77+
}
78+
}
79+
80+
return (success, returndata);
81+
}
82+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.11;
3+
4+
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
5+
6+
contract ForwarderConsumer is ERC2771Context {
7+
address public caller;
8+
9+
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
10+
11+
function setCaller() external {
12+
caller = _msgSender();
13+
}
14+
}

contracts/ForwarderEOAOnly.sol renamed to contracts/forwarder/ForwarderEOAOnly.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity ^0.8.11;
33

4-
import "./openzeppelin-presets/metatx/MinimalForwarderEOAOnly.sol";
4+
import "../openzeppelin-presets/metatx/MinimalForwarderEOAOnly.sol";
55

66
/*
77
* @dev Minimal forwarder for GSNv2
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts v4.4.1 (utils/cryptography/draft-EIP712.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
7+
8+
/**
9+
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
10+
*
11+
* The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
12+
* thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
13+
* they need in their contracts using a combination of `abi.encode` and `keccak256`.
14+
*
15+
* This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
16+
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
17+
* ({_hashTypedDataV4}).
18+
*
19+
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
20+
* the chain id to protect against replay attacks on an eventual fork of the chain.
21+
*
22+
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
23+
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
24+
*
25+
* _Available since v3.4._
26+
*/
27+
abstract contract EIP712ChainlessDomain {
28+
/* solhint-disable var-name-mixedcase */
29+
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
30+
// invalidate the cached domain separator if the chain id changes.
31+
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
32+
address private immutable _CACHED_THIS;
33+
34+
bytes32 private immutable _HASHED_NAME;
35+
bytes32 private immutable _HASHED_VERSION;
36+
bytes32 private immutable _TYPE_HASH;
37+
38+
/* solhint-enable var-name-mixedcase */
39+
40+
/**
41+
* @dev Initializes the domain separator and parameter caches.
42+
*
43+
* The meaning of `name` and `version` is specified in
44+
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
45+
*
46+
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
47+
* - `version`: the current major version of the signing domain.
48+
*
49+
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
50+
* contract upgrade].
51+
*/
52+
constructor(string memory name, string memory version) {
53+
bytes32 hashedName = keccak256(bytes(name));
54+
bytes32 hashedVersion = keccak256(bytes(version));
55+
bytes32 typeHash = keccak256("EIP712Domain(string name,string version,address verifyingContract)");
56+
_HASHED_NAME = hashedName;
57+
_HASHED_VERSION = hashedVersion;
58+
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
59+
_CACHED_THIS = address(this);
60+
_TYPE_HASH = typeHash;
61+
}
62+
63+
/**
64+
* @dev Returns the domain separator for the current chain.
65+
*/
66+
function _domainSeparatorV4() internal view returns (bytes32) {
67+
if (address(this) == _CACHED_THIS) {
68+
return _CACHED_DOMAIN_SEPARATOR;
69+
} else {
70+
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
71+
}
72+
}
73+
74+
function _buildDomainSeparator(
75+
bytes32 typeHash,
76+
bytes32 nameHash,
77+
bytes32 versionHash
78+
) private view returns (bytes32) {
79+
return keccak256(abi.encode(typeHash, nameHash, versionHash, address(this)));
80+
}
81+
82+
/**
83+
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
84+
* function returns the hash of the fully encoded EIP712 message for this domain.
85+
*
86+
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
87+
*
88+
* ```solidity
89+
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
90+
* keccak256("Mail(address to,string contents)"),
91+
* mailTo,
92+
* keccak256(bytes(mailContents))
93+
* )));
94+
* address signer = ECDSA.recover(digest, signature);
95+
* ```
96+
*/
97+
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
98+
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
99+
}
100+
}

docs/EIP712ChainlessDomain.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# EIP712ChainlessDomain
2+
3+
4+
5+
6+
7+
8+
9+
*https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in their contracts using a combination of `abi.encode` and `keccak256`. This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA ({_hashTypedDataV4}). The implementation of the domain separator was designed to be as efficient as possible while still properly updating the chain id to protect against replay attacks on an eventual fork of the chain. NOTE: This contract implements the version of the encoding known as &quot;v4&quot;, as implemented by the JSON RPC method https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. _Available since v3.4._*
10+
11+
12+

docs/Forwarder.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
### execute
1414

1515
```solidity
16-
function execute(MinimalForwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes)
16+
function execute(Forwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes)
1717
```
1818

1919

@@ -24,7 +24,7 @@ function execute(MinimalForwarder.ForwardRequest req, bytes signature) external
2424

2525
| Name | Type | Description |
2626
|---|---|---|
27-
| req | MinimalForwarder.ForwardRequest | undefined |
27+
| req | Forwarder.ForwardRequest | undefined |
2828
| signature | bytes | undefined |
2929

3030
#### Returns
@@ -59,7 +59,7 @@ function getNonce(address from) external view returns (uint256)
5959
### verify
6060

6161
```solidity
62-
function verify(MinimalForwarder.ForwardRequest req, bytes signature) external view returns (bool)
62+
function verify(Forwarder.ForwardRequest req, bytes signature) external view returns (bool)
6363
```
6464

6565

@@ -70,7 +70,7 @@ function verify(MinimalForwarder.ForwardRequest req, bytes signature) external v
7070

7171
| Name | Type | Description |
7272
|---|---|---|
73-
| req | MinimalForwarder.ForwardRequest | undefined |
73+
| req | Forwarder.ForwardRequest | undefined |
7474
| signature | bytes | undefined |
7575

7676
#### Returns

docs/MinimalForwarder.md renamed to docs/ForwarderChainlessDomain.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# MinimalForwarder
1+
# ForwarderChainlessDomain
22

33

44

@@ -13,7 +13,7 @@
1313
### execute
1414

1515
```solidity
16-
function execute(MinimalForwarder.ForwardRequest req, bytes signature) external payable returns (bool, bytes)
16+
function execute(ForwarderChainlessDomain.ForwardRequest req, bytes signature) external payable returns (bool, bytes)
1717
```
1818

1919

@@ -24,7 +24,7 @@ function execute(MinimalForwarder.ForwardRequest req, bytes signature) external
2424

2525
| Name | Type | Description |
2626
|---|---|---|
27-
| req | MinimalForwarder.ForwardRequest | undefined |
27+
| req | ForwarderChainlessDomain.ForwardRequest | undefined |
2828
| signature | bytes | undefined |
2929

3030
#### Returns
@@ -59,7 +59,7 @@ function getNonce(address from) external view returns (uint256)
5959
### verify
6060

6161
```solidity
62-
function verify(MinimalForwarder.ForwardRequest req, bytes signature) external view returns (bool)
62+
function verify(ForwarderChainlessDomain.ForwardRequest req, bytes signature) external view returns (bool)
6363
```
6464

6565

@@ -70,7 +70,7 @@ function verify(MinimalForwarder.ForwardRequest req, bytes signature) external v
7070

7171
| Name | Type | Description |
7272
|---|---|---|
73-
| req | MinimalForwarder.ForwardRequest | undefined |
73+
| req | ForwarderChainlessDomain.ForwardRequest | undefined |
7474
| signature | bytes | undefined |
7575

7676
#### Returns

0 commit comments

Comments
 (0)