EIP-712 V4: How to make a smart contract secure through Verifying Solidity Signatures

Aristoteles Joel Nici
carpediem-tech
Published in
5 min readMay 9, 2023
black shield and money. blockchain

Thanks to its decentralized nature, Ethereum offers many opportunities to develop secure and reliable applications. However, security remains a crucial issue, as any vulnerability or error could put users’ assets at risk.

EIP-712 is just one of many tools that developers can use to improve the security of their decentralized applications.

What is EIP-712?

EIP-712, or Ethereum Improvement Proposal 712, is a proposed standard for message-based digital signatures in Ethereum. The main goal of EIP-712 is to ensure the integrity and authenticity of messages exchanged between two parties.

In practice, EIP-712 is used to generate digital signatures of structured messages, where the format and content of the message itself are defined through a type schema. This way, the user who wishes to sign the message can precisely control what they are signing, avoiding possible phishing or spoofing attacks.

By using the digital signature, senders can ensure that the messages they send are not modified or falsified during transmission, and that only authorized recipients can read and understand the content of the messages.

EIP-712 V4

The latest version of the protocol, EIP-712 V4, has introduced a more complex structure for calculating the domain separator, improving the security of digital signatures on messages.

The domain separator is a unique value that is used to create a hash of the message that needs to be signed. This way, the generated digital signature can be securely verified, ensuring that the authorized sender created the message.

Version 4 of the EIP-712 protocol has introduced a more complex structure for calculating the domain separator, in order to prevent collision attacks. The structure includes several fields, including a list of message fields and their types.

To use the EIP-712 V4 protocol, smart contracts must define a constant _domainSeparatorV4 that includes the necessary parameters for calculating the domain separator. These parameters include the contract name, the EIP-712 protocol version, the chain ID, the contract address, and other surrounding factors.

How does EIP-712 work?

To use EIP-712, it is necessary to create a type schema for the message to be signed, where the fields and data types of the message are defined.

Next, the message is encoded in a standard format, so that the recipient can verify its integrity and authenticity.

Finally, the signature is generated using a private key, and is included in the message along with the necessary information to verify its authenticity. A significant advantage of EIP-712 is that it allows for the separation of the signature generation process from the message creation, thus enabling the creation of secure and decentralized applications.

In addition, EIP-712 uses a standardized format for message creation, simplifying the implementation of this functionality in applications.

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import "hardhat/console.sol";
contract MyContract is EIP712 {
mapping(address => uint256) private _nonces;
struct MyMessage {
address sender;
address receiver;
uint256 amount;
uint256 nonce;
bytes signature;
}
bytes32 private constant _MESSAGE_TYPEHASH = keccak256("Message(address sender,address receiver,uint256 amount,uint256 chainId,uint256 nonce)");

constructor( ) EIP712("MyContract","1") { }
function transfer(MyMessage calldata message) public {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(message.signature);
bytes32 structHash = keccak256(abi.encode(_MESSAGE_TYPEHASH,message.sender,message.receiver,message.amount,block.chainid,message.nonce));
address signer = ECDSA.recover(_hashTypedDataV4(structHash), v, r, s);
require(signer == message.sender, "invalid signature");
_nonces[msg.sender] += 1;
//Transfer logic
}

function getNonce() public view returns (uint256) {
return _nonces[msg.sender];
}
function splitSignature(bytes memory signature) internal virtual returns (bytes32 r, bytes32 s, uint8 v) {
require(signature.length == 65, "invalid signature length");
assembly {
// first 32 bytes, after the length prefix
r := mload(add(signature, 32))
// second 32 bytes
s := mload(add(signature, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(signature, 96)))
}
}
}

In this example, the smart contract uses the Ethereum development framework and the OpenZeppelin library for managing counters and defining EIP-712 standards. The constant _MESSAGE_TYPEHASHis a constant that represents the type of message used for generating the digital signature. This hash is later used to generate the digital signature of the message and verify the authenticity of the message through the ECDSA.recover() function.

In the constructor, we pass two parameters which are “My Contract” and “1”. These values are used by the EIP-712 abstract contract to generate a typeHash used later by the _hashTypeDataV4() function to compute the message hash.

The contract uses the splitSignature() function to extract the values of r, s, and v from the message signature. These values are used to verify the authenticity of the signature and to identify the sender of the message.

The _hashTypeDataV4() function is used to compute the message hash, while the ECDSA library’s recover function is used to verify the digital signature.

If the signature is valid, the transaction is executed.

Attention

The signature must be a “one-time-use” value. That is, the same signature cannot be used more than once.

If someone intercepts a user’s signature, even without possessing the privateKey, they can still execute operations on behalf of the victim.

Therefore, it is very important to include a value among the types that makes each signature truly unique and usable only once. In the previous example, this was the “nonce” value. A value that changes constantly. In fact, it is incremented every time the user uses the transfer() function.

In this smart contract, we used the gameId() value.

It may also be useful to include a deadline among the types, which makes the signature valid only for a certain period of time. After which the signature is no longer valid.

Another precaution that can save you a lot of time is how the following line is written:

bytes32 private constant _MESSAGE_TYPEHASH = keccak256("Message(address sender,address receiver,uint256 amount,uint256 chainId,uint256 nonce)");

I lost a lot of time because I had inserted a space between a comma and “uint256”. I had written:

bytes32 private constant _MESSAGE_TYPEHASH = keccak256("Message(address sender,address receiver,uint256 amount,uint256 chainId, uint256 nonce)");

This small detail made me waste a lot of time.

In addition, particular attention must be paid to the order in which the parameters are passed in this line:

bytes32 structHash = keccak256(abi.encode(_MESSAGE_TYPEHASH,message.sender,message.receiver,message.amount,block.chainid,message.nonce));

be the same as bytes32 private constant

_MESSAGE_TYPEHASH = keccak256("Message(address sender,address receiver,uint256 amount,uint256 chainId,uint256 nonce)");

It is also recommended to include the chainId in the parameters, then encoded with block.chainid. This is because, in the absence of this type, a signature could be replicated on another chain.

Conclusions

EIP-712 is a standard that allows for secure and reliable digital signatures for messages exchanged in Ethereum, improving the integrity and authenticity of decentralized applications. By allowing users to precisely control what they are signing, EIP-712 makes Ethereum transactions even more secure and reliable.

While there are other standards available for digital signatures, EIP-712 stands out for its simplicity and reliability. Thanks to its widespread adoption in the Ethereum community, EIP-712 represents an effective solution for secure digital signatures in Ethereum. EIP-712 is a standard that is gaining increasing popularity in the Ethereum community.

Many decentralized applications, including digital wallets, cryptocurrency exchange platforms, and DeFi protocols, are already implementing EIP-712 to protect user transactions and activities.

--

--