EVM-compatible chains obey the same rules for cryptographic computations, such as hash functions and digital signatures. The EIP-712 message is a standard for signing typed data, which is widely used in the Ethereum ecosystem. This article will introduce the usage of EIP-712 message for Orderly Network.

EIP-712 Types Definition

Unlike the transaction signatures, which are verified onchain to validate the transaction, the EIP-712 typed data is used to sign offchain messages. The typed data is a JSON object that describes the data structure to be signed. The typed formatted message is signed by the user’s private key, and the signature is then verified either by the contract or the offchain component to authenticate the signer’s identity.

{
    "EIP712Domain": [
            { name: "name", type: "string" },
            { name: "version", type: "string" },
            { name: "chainId", type: "uint256" },
            { name: "verifyingContract", type: "address" },
        ],
    "Registration": [
            {name: "brokerId", type: "string"},
            {name: "chainId", type: "uint256"},
            {name: "timestamp", type: "uint64"},
            {name: "registrationNonce", type: "uint256"}
        ],
}

Shown above is an example of the EIP-712 types definition. The EIP712Domain is a common structure that is shared by all EIP-712 typed data structure.

  • name is the name of the domain, such as Orderly Network.
  • version is the version of the typed data structure, such as 1.0.
  • chainId is the chain id of linked network, such as 1 for Ethereum mainnet.
  • verifyingContract is the address of the contract that will verify the signature, if the signature is verified offchain, the default value is 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC.

The Registration is a specific message type that is used to register an account under a broker on the Orderly Network.

  • brokerId is used to identify the broker.
  • chainId is the chain id of the linked network.
  • timestamp is used to detect an expired registration message by the verifying service.
  • registrationNonce is used to prevent DDoS attack, working like an access token.

EIP-712 Message Structure

Before signing the message, the message should be formatted according to the defined EIP-712 type message. A complete EIP-712 typed message is a JSON object that contains the following fields:

const eip712Message = {
        "domain": {
            "name": "Orderly Network",
            "version": "1.0",
            "chainId": 4460,   // the chainId of the connected network by Metamask
            "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"  // to indicate the signanature is verified offchain
        },
        "message":{
            "brokerId": "woofi_pro",
            "chainId": 4460,
            "timestamp": 1685973017064,
            "registrationNonce": "983740230482834645859323"
        }
        "primaryType": "Registration",
        "types": {
            "EIP712Domain": [
                { name: "name", type: "string" },
                { name: "version", type: "string" },
                { name: "chainId", type: "uint256" },
                { name: "verifyingContract", type: "address" },
            ],
            "Registration": [
                {name: "brokerId", type: "string"},
                {name: "chainId", type: "uint256"},
                {name: "timestamp", type: "uint64"},
                {name: "registrationNonce", type: "uint256"}
            ]
        }
    }    
  • The field types is the core of the EIP-712 message, which defines the types of the message.
  • The primaryType is the name of the message type, which is used to identify the message type in the signature.
  • domain and message are the fields to contain the data for EIP712Domain type and defined primaryType type, here is the Registration type.

EIP-712 Message Signing

The message is signed by the user’s private key, and the signature is then verified by the contract or the offchain component to authenticate the signer’s identity. There are several tools to provide the signing function for EIP-712 message, such as ethers.js and web3.js. Here is an example of using ethers.js to sign the message:

const wallet = new ethers.Wallet(privateKey);

const domain = eip712Message['domain'];
const message = eip712Message['message'];
const types = {
    [eip712Message["primaryType"]]: eip712Message['types'][eip712Message["primaryType"]]
};
// the above types is equivalent to the following content
// const types = {
//   Registration: [
//     { name: 'brokerId', type: 'string' },
//     { name: 'chainId', type: 'uint256' },
//     { name: 'timestamp', type: 'uint64' },
//     { name: 'registrationNonce', type: 'uint256' }
//   ]
// }

const signature = await wallet.signTypedData(domain, types, message);

Of course, injected wallets like Metamask also provide the signing function for EIP-712 message. The user can sign the message with the private key of the connected account.

img

const method = 'eth_signTypedData_v4';
const params = [window.userWalletAddress, JSON.stringify(eip712Message)];
const signature = await window.ethereum.request({ method, params });

EIP-712 Message Verification

The signature of the EIP-712 message can be verified by the contract or the offchain component. If the signature is verified offchain, the verifyingContract field in the domain object should be set to 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC. The offchain component can use the ethers.js library to verify the signature.

const domain = eip712Message['domain'];
const message = eip712Message['message'];
const types = {
    [eip712Message["primaryType"]]: eip712Message['types'][eip712Message["primaryType"]]
};
const recoveredAddress = ethers.verifyTypedData(domain, types, message, signature);

If the signature is supposed to be verified onchain, the verifyingContract field in the domain object should be set to the address of the contract that will execute the verification. The contract can use the ecrecover function to verify the signature.

struct RegistrationData {
    string brokerId;
    uint256 chainId;
    uint64 timestamp;
    uint256 registrationNonce;
    uint8 v;
    bytes32 r;
    bytes32 s;
    address signer;
}

function verifyRegitraion(RegistrationData memory data) public view return (bool) {
    bytes32 typeDomainHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 eip712DomainHash = keccak256(abi.encode(typeHash, keccak256(bytes("Orderly Network")), keccak256(bytes("1.0")), block.chainId, address(this)));
    bytes32 typeStructHash = keccak256(
                abi.encode(
                    keccak256("Registration(string brokerId,uint256 chainId,uint64 timestamp,uint256 registrationNonce)"),
                    keccak256(abi.encodePacked(data.brokerId)),
                    data.chainId,
                    data.timestamp,
                    data.registrationNonce
                )
            );
    bytes32 hash = keccak256(abi.encodePacked("\x19\x01", eip712DomainHash, typeStructHash));
    return ECDSA.recover(hash, data.v, data.r, data.s) == signer;
}

Conclusion

In this article, the usage of EIP-712 message is explained by giveing an example of registering an account on Orderly Network. The EIP-712 message is a standard for signing typed data, which is widely used in the Ethereum ecosystem. The EIP-712 message is used to sign offchain messages, and the signature is then verified either by the contract or the offchain component to authenticate the signer’s identity. The EIP-712 message is a powerful tool for the offchain component to verify the user’s identity and the integrity of the message.