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

Add entrypoints and 1271 support #11

Merged
merged 19 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules/
.DS_Store
cache/
artifacts/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v16.14.2
79 changes: 43 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# EIP 712 Codegen
This module aims to automate the hard parts of using EIP-712

[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.

[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.
[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.

[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.


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.

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

### As a CLI tool:

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.
`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:

`npx eip712-codegen -i ./yourTypes.js >> TypesFile.sol`

Examples:
These are the command line options:

```
Options:
--version Show version number [boolean]
-i, --input Input file path [string] [required]
-e, --entryPoints Type names to be used as entry points [array] [required]
-l, --log Enable logging [boolean]
-h, --help Show help [boolean]
```

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.

More examples:

input:
```sh
npx eip712-codegen --input <input-file-path> --entryPoints <entry-point-1> <entry-point-2> ... --log
```

Example:
```sh
npx eip712-codegen ./sampleTypes.js > YourTypesFile.sol
npx eip712-codegen --input sampleTypes.js --entryPoints Type1 Type2 > YourTypesFile.sol
```

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`:
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`:

```sh
npx eip712-codegen ./sampleTypes.js log > YourTypesFile.sol
npx eip712-codegen --input sampleTypes.js --entryPoints Type1 Type2 --log > YourTypesFile.sol
```

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

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:
### Entrypoints

```solidity
function verifyDelegationSignature (SignedDelegation memory signedDelegation) public view returns (address) {
// Break out the struct that was signed:
Delegation memory delegation = signedDelegation.delegation;
// Get the top-level hash of that struct, as defined just below:
bytes32 sigHash = getDelegationTypedDataHash(delegation);
// The `recover` method comes from the codegen, and will be able to recover from this:
address recoveredSignatureSigner = recover(sigHash, signedDelegation.signature);
return recoveredSignatureSigner;
}
function getDelegationTypedDataHash(Delegation memory delegation) public view returns (bytes32) {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
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);`

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

// This last part is calling one of the generated methods.
// It must match the name of the struct that is the `primaryType` of this signature.
GET_DELEGATION_PACKETHASH(delegation)
));
return digest;
}
The `Signed{Type}` struct format looks like this:
```solidity
{
bytes signature;
address signer;
YourType message;
}
```

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.

For regular EOA signatures, the signer should be set to the zero address (`0x0000000000000000000000000000000000000000`).
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.
32 changes: 28 additions & 4 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
#!/usr/bin/env node

const yargs = require('yargs');
const typesToCode = require('./index');
const path = require('path');

const targetPath = path.join(process.cwd(), process.argv[2]);
const types = require(targetPath);
const shouldLog = process.argv[3] === 'log';
const argv = yargs
.option('input', {
alias: 'i',
describe: 'Input file path',
demandOption: true,
type: 'string',
})
.option('entryPoints', {
alias: 'e',
describe: 'Type names to be used as entry points',
demandOption: true,
array: true,
type: 'string',
})
.option('log', {
alias: 'l',
describe: 'Enable logging',
type: 'boolean',
})
.help()
.alias('help', 'h')
.argv;

console.log(typesToCode.generateSolidity(types, shouldLog));
const targetPath = argv.input;
const types = require(targetPath);
const entryPoints = argv.entryPoints;
const shouldLog = argv.log;

console.log(typesToCode.generateSolidity(types, shouldLog, entryPoints));
43 changes: 43 additions & 0 deletions contracts/ECRecovery.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pragma solidity 0.8.19;

// SPDX-License-Identifier: MIT

contract ECRecovery {
/**
* @dev Recover signer address from a message by using their signature
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
* @param sig bytes signature, the signature is generated using web3.eth.sign()
*/
function recover(bytes32 hash, bytes memory sig)
internal
pure
returns (address)
{
bytes32 r;
bytes32 s;
uint8 v;

//Check the signature length
if (sig.length != 65) {
return (address(0));
}

// Divide the signature in r, s and v variables
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
// Version of signature should be 27 or 28, but 0 and 1 are also possible versions
if (v < 27) {
v += 27;
}

// If the version is correct return the signer address
if (v != 27 && v != 28) {
return (address(0));
} else {
return ecrecover(hash, v, r, s);
}
}
}
31 changes: 31 additions & 0 deletions contracts/EIP1271.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pragma solidity 0.8.19;
import "./ECRecovery.sol";

//SPDX-License-Identifier: MIT

contract EIP1271 is ECRecovery {
mapping(address => bool) isOwner;

constructor() {
isOwner[msg.sender] = true;
}

function addOwner(address _owner) public {
isOwner[_owner] = true;
}

/**
* @notice Verifies that the signer is the owner of the signing contract.
*/
function isValidSignature(bytes32 _hash, bytes calldata _signature)
external
view
returns (bytes4)
{
if (isOwner[recover(_hash, _signature)]) {
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
}
144 changes: 144 additions & 0 deletions contracts/EIP712Decoder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
pragma solidity ^0.8.13;
// SPDX-License-Identifier: MIT


abstract contract ERC1271Contract {
/**
* @dev Should return whether the signature provided is valid for the provided hash
* @param _hash Hash of the data to be signed
* @param _signature Signature byte array associated with _hash
*
* MUST return the bytes4 magic value 0x1626ba7e when function passes.
* MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
* MUST allow external calls
*/
function isValidSignature(
bytes32 _hash,
bytes memory _signature)
public
view
virtual
returns (bytes4 magicValue);
}

abstract contract EIP712Decoder {
function getDomainHash () public view virtual returns (bytes32);

struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}

bytes32 constant public EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");


struct SignedPerson {
bytes signature;
address signer;
Person message;
}


struct Person {
string name;
uint256 age;
}

bytes32 constant public PERSON_TYPEHASH = keccak256("Person(string name,uint256 age)");


/**
* @dev Recover signer address from a message by using their signature
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
* @param sig bytes signature, the signature is generated using web3.eth.sign()
*/
function recover(bytes32 hash, bytes memory sig) internal pure returns (address) {
bytes32 r;
bytes32 s;
uint8 v;

// Check the signature length
if (sig.length != 65) {
return (address(0));
}

// Divide the signature in r, s and v variables
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
// Version of signature should be 27 or 28, but 0 and 1 are also possible versions
if (v < 27) {
v += 27;
}

// If the version is correct return the signer address
if (v != 27 && v != 28) {
return (address(0));
} else {
return ecrecover(hash, v, r, s);
}
}

function GET_EIP712DOMAIN_PACKETHASH (EIP712Domain memory _input) public pure returns (bytes32) {
bytes memory encoded = GET_EIP712DOMAIN_PACKET(_input);
return keccak256(encoded);
}

function GET_EIP712DOMAIN_PACKET (EIP712Domain memory _input) public pure returns (bytes memory) {
bytes memory encoded = abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(_input.name)),
keccak256(bytes(_input.version)),
_input.chainId,
_input.verifyingContract
);
return encoded;
}


function GET_PERSON_PACKETHASH (Person memory _input) public pure returns (bytes32) {
bytes memory encoded = GET_PERSON_PACKET(_input);
return keccak256(encoded);
}

function GET_PERSON_PACKET (Person memory _input) public pure returns (bytes memory) {
bytes memory encoded = abi.encode(
PERSON_TYPEHASH,
keccak256(bytes(_input.name)),
_input.age
);
return encoded;
}


function verifySignedPerson(SignedPerson memory _input) public view returns (address) {
bytes32 packetHash = GET_PERSON_PACKETHASH(_input.message);
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
getDomainHash(),
packetHash
)
);

if (_input.signer == 0x0000000000000000000000000000000000000000) {
address recoveredSigner = recover(
digest,
_input.signature
);
return recoveredSigner;
} else {
// EIP-1271 signature verification
bytes4 result = ERC1271Contract(_input.signer).isValidSignature(digest, _input.signature);
require(result == 0x1626ba7e, "INVALID_SIGNATURE");
return _input.signer;
}
}


}

21 changes: 21 additions & 0 deletions contracts/MockEIP712Decoder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "./EIP712Decoder.sol";

contract MockEIP712Decoder is EIP712Decoder {
bytes32 domainHash;
constructor(uint256 chainId) {
domainHash = keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes("MockEIP712Decoder")),
keccak256(bytes("1")),
chainId,
address(this)
));
}

function getDomainHash () public view override returns (bytes32) {
return domainHash;
}
}
Loading