Skip to content

Commit 4ded485

Browse files
authored
Merge pull request #11 from danfinlay/add-entrypoints
Add entrypoints and 1271 support
2 parents 337b2f8 + 9e9d3f5 commit 4ded485

File tree

14 files changed

+34719
-732
lines changed

14 files changed

+34719
-732
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
node_modules/
22
.DS_Store
3+
cache/
4+
artifacts/

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v16.14.2

README.md

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# EIP 712 Codegen
2+
This module aims to automate the hard parts of using EIP-712
23

3-
[EIP 712: Sign Typed Data](https://eips.ethereum.org/EIPS/eip-712) as of 2022 is the most human-readable way of getting signatures from user that are easily parsed into solidity structs.
44

5-
[The documentation](https://docs.metamask.io/guide/signing-data.html#sign-typed-data-v4) has not always been the greatest, and in particular I think this method has failed to catch on because writing the verification code is a huge pain.
5+
[EIP 712: Sign Typed Data](https://eips.ethereum.org/EIPS/eip-712) as of 2023 is the most human-readable way of getting signatures from user that are easily parsed into solidity structs.
6+
7+
[The documentation](https://docs.metamask.io/guide/signing-data.html#sign-typed-data-v4) is quite dense, and can be hard to get started with.
8+
69

710
Well, no more. This module will generate basically all the solidity you need to let users basically sign structs and then mostly just think about the structs you have signed in your code. This should really level up your ability to keep more user actions off-chain and gas-free.
811

@@ -20,18 +23,39 @@ As a module, we are exporting typescript definition files, which can help you ge
2023

2124
### As a CLI tool:
2225

23-
You point it at a typeDef file (defined as a CommonJS module, [as seen in sampleTypes.js](./sampleTypes.js)), and it then prints out some solidity to the console. You can then pipe it into a file.
26+
`npm i -g eip712-codegen` or `yarn add -g eip712-codegen` to globally install, and then you can run the command line and pipe the output into a solidity file like so:
27+
28+
`npx eip712-codegen -i ./yourTypes.js >> TypesFile.sol`
2429

25-
Examples:
30+
These are the command line options:
31+
32+
```
33+
Options:
34+
--version Show version number [boolean]
35+
-i, --input Input file path [string] [required]
36+
-e, --entryPoints Type names to be used as entry points [array] [required]
37+
-l, --log Enable logging [boolean]
38+
-h, --help Show help [boolean]
39+
```
40+
41+
The `input` file is a typeDef file (defined as a CommonJS module, [as seen in sampleTypes.js](./sampleTypes.js)), and it then prints out some solidity to the console. You can then pipe it into a file. The same typedef format is used by signing code for EIP-712, like when suggesting a signature to MetaMask, so this allows you to define these types once and reuse them on the front and backend.
42+
43+
More examples:
44+
45+
input:
46+
```sh
47+
npx eip712-codegen --input <input-file-path> --entryPoints <entry-point-1> <entry-point-2> ... --log
48+
```
2649

50+
Example:
2751
```sh
28-
npx eip712-codegen ./sampleTypes.js > YourTypesFile.sol
52+
npx eip712-codegen --input sampleTypes.js --entryPoints Type1 Type2 > YourTypesFile.sol
2953
```
3054

31-
If you're using [hardhat](hardhat.org/) and their [console.log](https://hardhat.org/hardhat-network/#console-log) feature, you can generate a logged version by adding `log`:
55+
If you're using [hardhat](hardhat.org/) and their [console.log](https://hardhat.org/hardhat-network/#console-log) feature, you can generate a logged version by adding `--log`:
3256

3357
```sh
34-
npx eip712-codegen ./sampleTypes.js log > YourTypesFile.sol
58+
npx eip712-codegen --input sampleTypes.js --entryPoints Type1 Type2 --log > YourTypesFile.sol
3559
```
3660

3761
You'll then need to import this typefile into your contract, and inherit from `EIP712Decoder`.
@@ -68,36 +92,19 @@ You'll also need to include this one method that defines your DomainHash, which
6892
}
6993
```
7094

71-
There's one more thing you have to do, this part will require the most thinking. You'll have to write the method that verifies the top-level signatures. I have not written codegen for this yet, because I don't know which types you want to use as your entry points, and there are some design decisions that are up to you here (in particular, *your entrypoint types are your user-facing security enforcement*, but here is a sample method for verifying a `SignedDelegation` as defined in our [sampleTypes.js](./sampleTypes) file:
95+
### Entrypoints
7296

73-
```solidity
74-
function verifyDelegationSignature (SignedDelegation memory signedDelegation) public view returns (address) {
75-
76-
// Break out the struct that was signed:
77-
Delegation memory delegation = signedDelegation.delegation;
78-
79-
// Get the top-level hash of that struct, as defined just below:
80-
bytes32 sigHash = getDelegationTypedDataHash(delegation);
81-
82-
// The `recover` method comes from the codegen, and will be able to recover from this:
83-
address recoveredSignatureSigner = recover(sigHash, signedDelegation.signature);
84-
return recoveredSignatureSigner;
85-
}
86-
87-
function getDelegationTypedDataHash(Delegation memory delegation) public view returns (bytes32) {
88-
bytes32 digest = keccak256(abi.encodePacked(
89-
"\x19\x01",
97+
The `--entryPoints` flag generates signature verification code for the specified types (which must also be included in the input file). These verification methods will be of the form `verifySigned${YourType}(Signed${YourType} input) returns (address);`. So if you are signing a struct called `Bid` it will generate a method called `verifySignedBid(SignedBid input) returns (address);`
9098

91-
// The domainHash is derived from your contract name and address above:
92-
domainHash,
99+
Returns an `address` of the account that signed this struct.
93100

94-
// This last part is calling one of the generated methods.
95-
// It must match the name of the struct that is the `primaryType` of this signature.
96-
GET_DELEGATION_PACKETHASH(delegation)
97-
));
98-
return digest;
99-
}
101+
The `Signed{Type}` struct format looks like this:
102+
```solidity
103+
{
104+
bytes signature;
105+
address signer;
106+
YourType message;
107+
}
100108
```
101-
102-
From there, you should be good! This library is tested to work with `eth_signTypedData_v4` as implemented in MetaMask. I have not yet tested it with ethers.js or other wallets, but there's a good chance it works for simple types, and a chance it works for arrays and structs as well.
103-
109+
For regular EOA signatures, the signer should be set to the zero address (`0x0000000000000000000000000000000000000000`).
110+
If the `signer` value is set to anything other than the zero address, rather than recover a signature normally, the contract will execute [EIP-1271 style signature recovery](https://eips.ethereum.org/EIPS/eip-1271) which allows contract accounts to perform custom verification logic allowing them to effectively "sign" messages like an EOA does.

cli.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
#!/usr/bin/env node
22

3+
const yargs = require('yargs');
34
const typesToCode = require('./index');
45
const path = require('path');
56

6-
const targetPath = path.join(process.cwd(), process.argv[2]);
7-
const types = require(targetPath);
8-
const shouldLog = process.argv[3] === 'log';
7+
const argv = yargs
8+
.option('input', {
9+
alias: 'i',
10+
describe: 'Input file path',
11+
demandOption: true,
12+
type: 'string',
13+
})
14+
.option('entryPoints', {
15+
alias: 'e',
16+
describe: 'Type names to be used as entry points',
17+
demandOption: true,
18+
array: true,
19+
type: 'string',
20+
})
21+
.option('log', {
22+
alias: 'l',
23+
describe: 'Enable logging',
24+
type: 'boolean',
25+
})
26+
.help()
27+
.alias('help', 'h')
28+
.argv;
929

10-
console.log(typesToCode.generateSolidity(types, shouldLog));
30+
const targetPath = argv.input;
31+
const types = require(targetPath);
32+
const entryPoints = argv.entryPoints;
33+
const shouldLog = argv.log;
1134

35+
console.log(typesToCode.generateSolidity(types, shouldLog, entryPoints));

contracts/ECRecovery.sol

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
pragma solidity 0.8.19;
2+
3+
// SPDX-License-Identifier: MIT
4+
5+
contract ECRecovery {
6+
/**
7+
* @dev Recover signer address from a message by using their signature
8+
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
9+
* @param sig bytes signature, the signature is generated using web3.eth.sign()
10+
*/
11+
function recover(bytes32 hash, bytes memory sig)
12+
internal
13+
pure
14+
returns (address)
15+
{
16+
bytes32 r;
17+
bytes32 s;
18+
uint8 v;
19+
20+
//Check the signature length
21+
if (sig.length != 65) {
22+
return (address(0));
23+
}
24+
25+
// Divide the signature in r, s and v variables
26+
assembly {
27+
r := mload(add(sig, 32))
28+
s := mload(add(sig, 64))
29+
v := byte(0, mload(add(sig, 96)))
30+
}
31+
// Version of signature should be 27 or 28, but 0 and 1 are also possible versions
32+
if (v < 27) {
33+
v += 27;
34+
}
35+
36+
// If the version is correct return the signer address
37+
if (v != 27 && v != 28) {
38+
return (address(0));
39+
} else {
40+
return ecrecover(hash, v, r, s);
41+
}
42+
}
43+
}

contracts/EIP1271.sol

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
pragma solidity 0.8.19;
2+
import "./ECRecovery.sol";
3+
4+
//SPDX-License-Identifier: MIT
5+
6+
contract EIP1271 is ECRecovery {
7+
mapping(address => bool) isOwner;
8+
9+
constructor() {
10+
isOwner[msg.sender] = true;
11+
}
12+
13+
function addOwner(address _owner) public {
14+
isOwner[_owner] = true;
15+
}
16+
17+
/**
18+
* @notice Verifies that the signer is the owner of the signing contract.
19+
*/
20+
function isValidSignature(bytes32 _hash, bytes calldata _signature)
21+
external
22+
view
23+
returns (bytes4)
24+
{
25+
if (isOwner[recover(_hash, _signature)]) {
26+
return 0x1626ba7e;
27+
} else {
28+
return 0xffffffff;
29+
}
30+
}
31+
}

contracts/EIP712Decoder.sol

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
pragma solidity ^0.8.13;
2+
// SPDX-License-Identifier: MIT
3+
4+
5+
abstract contract ERC1271Contract {
6+
/**
7+
* @dev Should return whether the signature provided is valid for the provided hash
8+
* @param _hash Hash of the data to be signed
9+
* @param _signature Signature byte array associated with _hash
10+
*
11+
* MUST return the bytes4 magic value 0x1626ba7e when function passes.
12+
* MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
13+
* MUST allow external calls
14+
*/
15+
function isValidSignature(
16+
bytes32 _hash,
17+
bytes memory _signature)
18+
public
19+
view
20+
virtual
21+
returns (bytes4 magicValue);
22+
}
23+
24+
abstract contract EIP712Decoder {
25+
function getDomainHash () public view virtual returns (bytes32);
26+
27+
struct EIP712Domain {
28+
string name;
29+
string version;
30+
uint256 chainId;
31+
address verifyingContract;
32+
}
33+
34+
bytes32 constant public EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
35+
36+
37+
struct SignedPerson {
38+
bytes signature;
39+
address signer;
40+
Person message;
41+
}
42+
43+
44+
struct Person {
45+
string name;
46+
uint256 age;
47+
}
48+
49+
bytes32 constant public PERSON_TYPEHASH = keccak256("Person(string name,uint256 age)");
50+
51+
52+
/**
53+
* @dev Recover signer address from a message by using their signature
54+
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
55+
* @param sig bytes signature, the signature is generated using web3.eth.sign()
56+
*/
57+
function recover(bytes32 hash, bytes memory sig) internal pure returns (address) {
58+
bytes32 r;
59+
bytes32 s;
60+
uint8 v;
61+
62+
// Check the signature length
63+
if (sig.length != 65) {
64+
return (address(0));
65+
}
66+
67+
// Divide the signature in r, s and v variables
68+
assembly {
69+
r := mload(add(sig, 32))
70+
s := mload(add(sig, 64))
71+
v := byte(0, mload(add(sig, 96)))
72+
}
73+
// Version of signature should be 27 or 28, but 0 and 1 are also possible versions
74+
if (v < 27) {
75+
v += 27;
76+
}
77+
78+
// If the version is correct return the signer address
79+
if (v != 27 && v != 28) {
80+
return (address(0));
81+
} else {
82+
return ecrecover(hash, v, r, s);
83+
}
84+
}
85+
86+
function GET_EIP712DOMAIN_PACKETHASH (EIP712Domain memory _input) public pure returns (bytes32) {
87+
bytes memory encoded = GET_EIP712DOMAIN_PACKET(_input);
88+
return keccak256(encoded);
89+
}
90+
91+
function GET_EIP712DOMAIN_PACKET (EIP712Domain memory _input) public pure returns (bytes memory) {
92+
bytes memory encoded = abi.encode(
93+
EIP712DOMAIN_TYPEHASH,
94+
keccak256(bytes(_input.name)),
95+
keccak256(bytes(_input.version)),
96+
_input.chainId,
97+
_input.verifyingContract
98+
);
99+
return encoded;
100+
}
101+
102+
103+
function GET_PERSON_PACKETHASH (Person memory _input) public pure returns (bytes32) {
104+
bytes memory encoded = GET_PERSON_PACKET(_input);
105+
return keccak256(encoded);
106+
}
107+
108+
function GET_PERSON_PACKET (Person memory _input) public pure returns (bytes memory) {
109+
bytes memory encoded = abi.encode(
110+
PERSON_TYPEHASH,
111+
keccak256(bytes(_input.name)),
112+
_input.age
113+
);
114+
return encoded;
115+
}
116+
117+
118+
function verifySignedPerson(SignedPerson memory _input) public view returns (address) {
119+
bytes32 packetHash = GET_PERSON_PACKETHASH(_input.message);
120+
bytes32 digest = keccak256(
121+
abi.encodePacked(
122+
"\x19\x01",
123+
getDomainHash(),
124+
packetHash
125+
)
126+
);
127+
128+
if (_input.signer == 0x0000000000000000000000000000000000000000) {
129+
address recoveredSigner = recover(
130+
digest,
131+
_input.signature
132+
);
133+
return recoveredSigner;
134+
} else {
135+
// EIP-1271 signature verification
136+
bytes4 result = ERC1271Contract(_input.signer).isValidSignature(digest, _input.signature);
137+
require(result == 0x1626ba7e, "INVALID_SIGNATURE");
138+
return _input.signer;
139+
}
140+
}
141+
142+
143+
}
144+

contracts/MockEIP712Decoder.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import "./EIP712Decoder.sol";
5+
6+
contract MockEIP712Decoder is EIP712Decoder {
7+
bytes32 domainHash;
8+
constructor(uint256 chainId) {
9+
domainHash = keccak256(abi.encode(
10+
EIP712DOMAIN_TYPEHASH,
11+
keccak256(bytes("MockEIP712Decoder")),
12+
keccak256(bytes("1")),
13+
chainId,
14+
address(this)
15+
));
16+
}
17+
18+
function getDomainHash () public view override returns (bytes32) {
19+
return domainHash;
20+
}
21+
}

0 commit comments

Comments
 (0)