Motivation

  1. We should never roll our own crypto. This means we should not come up with our own signature schemes, validation schemes, etc. Previously, we were using our own signature scheme in
  2. EIP-712 is the standard for readable signatures in the EVM ecosystem: https://eips.ethereum.org/EIPS/eip-712 This allows users who are signing stuff in Create3 to use their EOAs and to read what they are signing instead of seeing a bunch of meaningless bytes

Background

If you don't know what ecrecover is(like me before starting this fix), you should read this:
https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7

EIP-712 doesn't provide replay protection, as there is no nonce inbuilt into the EIP-712. This guide explains how to implement nonces: https://medium.com/immunefi/intro-to-cryptography-and-signatures-in-ethereum-2025b6a4a33d

This change was mostly guided from this EIP-712 step by step guide: https://gist.github.com/markodayan/e05f524b915f129c4f8500df816a369b

1. Design your Data structures

We need to secure these four things:

  • addDeployer
    • โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹  struct AddDeployer {
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹      address deployer;
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹      uint256 nonce;
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹  }
      
      โ€‹โ€‹โ€‹โ€‹โ€‹โ€‹  
      

2. Design your domain separator

{
    name: "FunWallet.Create3Deployer",
    version: "1",
    chainId: block.chainId,
    verifyingContract: address.this,
    salt: "0x43efba6b4cc18b6faa2625fe562bdd9a23260359"
}

3. Write Signing code for your dApp

Offchain stuff, do this later
https://gist.github.com/markodayan/e05f524b915f129c4f8500df816a369b

4. Write Authentication code for verifying contract

The purpose of the below code is: given a list of v,r,s signatures from some offchain source, determine whether the v,r,s signatures are valid deployers who signed the specific messages to call addDeployer(), removeDeployer(), setThreshold(), or callChild()

Domain Separator

This is to adhere to the Eip712 standard

const name = "FunWallet.Create3Deployer"
const version = "1"
const salt = 0x43efba6b4cc18b6faa2625fe562bdd9a23260359;
string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)";
bytes32 private constant DOMAIN_SEPARATOR = keccak256(abi.encode(
    EIP712_DOMAIN_TYPEHASH,
    keccak256(name),
    keccak256(version),
    block.chainid,
    address(this),
    salt
));

addDeployer

TYPEHASH:
string private constant ADD_DEPLOYER_TYPEHASH = "AddDeployer(address deployer, uint256 nonce)"
Hash function:

function hashAddDeployer(AddDeployer addDeployer) private pure returns(bytes32){
    return keccak256(abi.encode(
        ADD_DEPLOYER_TYPEHASH,
        addDeployer.deployer,
        addDeployer.nonce
    ));
} 

Verify Function:

function numValidAddDeployerHashes(AddDeployer addDeployer, uint8[] memory v, bytes32[] memory r, bytes32[] memory s) public view returns(uint256 numValidHashes){
    for(uint256 i; i < v.length; i++){
        address signer = ecrecover(hashAddDeployer(addDeployer), v[i], r[i], s[i]);
        numValidHashes += validDeployer[signer] ? 1:0;
    }
}

removeDeployer

TYPEHASH:
string private constant REMOVE_DEPLOYER_TYPEHASH = "RemoveDeployer(address deployer, uint256 nonce)"
Hash function:

function hashRemoveDeployer(RemoveDeployer removeDeployer) private pure returns(bytes32){
    return keccak256(abi.encode(
        REMOVE_DEPLOYER_TYPEHASH,
        removeDeployer.deployer,
        removeDeployer.nonce
    ));
} 

Verify Function:

function numValidRemoveDeployerHashes(RemoveDeployer removeDeployer, uint8[] memory v, bytes32[] memory r, bytes32[] memory s) public view returns(uint256 numValidHashes){
    for(uint256 i; i < v.length; i++){
        address signer = ecrecover(hashRemoveDeployer(removeDeployer), v[i], r[i], s[i]);
        numValidHashes += validDeployer[signer] ? 1:0;
    }
}

setThreshold

TYPEHASH:
string private constant SET_THRESHOLD_TYPEHASH = "SetThreshold(uint256 threshold, uint256 nonce)"
Hash function:

function hashSetThreshold(SetThreshold setThreshold) private pure returns(bytes32){
    return keccak256(abi.encode(
        SET_THRESHOLD_TYPEHASH,
        setThreshold.threshold,
        setThreshold.nonce
    ));
} 

Verify Function:

function numValidSetThresholdHashes(SetThreshold setThreshold, uint8[] memory v, bytes32[] memory r, bytes32[] memory s) public view returns(uint256 numValidHashes){
    for(uint256 i; i < v.length; i++){
        address signer = ecrecover(hashSetThreshold(setThreshold), v[i], r[i], s[i]);
        numValidHashes += validDeployer[signer] ? 1:0;
    }
}

callChild

TYPEHASH:
string private constant CALL_CHILD_TYPEHASH = "CallChild(address child, bytes data, uint256 nonce)"
Hash function:

function hashCallChild(CallChild callChild) private pure returns(bytes32){
    return keccak256(abi.encode(
        CALL_CHILD_TYPEHASH,
        callChild.child,
        callChild.data,
        callChild.nonce
    ));
} 

Verify Function:

function numValidCallChildHashes(CallChild callChild, uint8[] memory v, bytes32[] memory r, bytes32[] memory s) public view returns(uint256 numValidHashes){
    for(uint256 i; i < v.length; i++){
        address signer = ecrecover(hashCallChild(callChild), v[i], r[i], s[i]);
        numValidHashes += validDeployer[signer] ? 1:0;
    }
}

Resources