Creating, signing and verifying transactions in Solidity (Foundry) 🥷🏽
In web3, users approve/reject transaction requests each time they interact with a dApp. Have you ever wondered how these requests are created and their signatures verified? In this article, we will learn how to create a function request followed by signing and verifying it. Lets get cracking!
Encoding
There are 3 ways of encoding stuff in Foundry:
abi.encode(…)
abi.encodePacked(…)
abi.encodeWithSignature(…)
The difference you ask?
Now, let’s say you want your user’s approval on calling the setPaused(…)
function of your smart contract.
function _setPaused(bool pauseStatus) internal {
paused = pauseStatus;
}
To do so, we got to first find the function selector of the function we want to call. Here’s where abi.encodeWithSignature(…)
is used.
bytes memory _setPausedEncodedFunction = abi.encodeWithSignature("_setPaused(bool pauseStatus)", true);
Now that we have the encoded function selector for the _setPaused(bool pauseStatus)
function, lets create the request hash of the function selector.
Hashing
This is done using keccak256(…)
. This will generate a fixed-size bytes32 array of the encoded request.
bytes32 setPausedHash = keccak256(_setPausedEncodedFunction);
Remember, the difference between encoding and hashing. Anything data encoded can be decoded back to it's original form, whereas hashing is an irreversible operation.
As a final step, the generated hash has to be converted into a eth message hash. This operation is required by the ERC-191 as following this standard will help in recoving the signer for verification purposes.
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
bytes32 ethSignedPauseRequestHash = ECDSA.toEthSignedMessageHash(setPausedHash);
That’s it! We finally have a ERC-191 compatible request hash which can be sent to the dApp client for user’s approval.
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract Account {
function _setPaused(bool pauseStatus) internal {
paused = pauseStatus;
}
function generateRequestHash() external returns(bytes32) {
// encoding
bytes memory setPausedEncodedFunction = abi.encodeWithSignature("_setPaused(bool pauseStatus)", true);
// hashing
bytes32 pauseRequestHash = keccak256(setPausedEncodedFunction);
bytes32 requestHash = ECDSA.toEthSignedMessageHash(pauseRequestHash);
}
}
Signing
As a bonus, I’ve added the signing section, which will demonstrate how you can sign the request hash and even verify the request signature, through a unit test in Foundry.
For signing a request hash, we use a Foundry cheatcode vm.sign(privateKey, requestHash)
. This returns us a tuple as seen below:
(address signer, uint256 signerPK) = makeAddrAndKey("signer");
// will generate a signer public & private key pair
vm.prank(signer);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK, requestHash);
uint8 v: v
is the recovery id or the recovery byte of the signature. It is a single byte value (an integer from 0 to 255). This parameter helps determine which public key was used to create the signature.
bytes32 r: r
is one of the two 32-byte components of the signature. Signatures are typically represented as (r, s)
where r
and s
are two 32-byte values. r
is the first component of the signature, and it is used in the signature verification process.
bytes32 s: s
is the second 32-byte component of the signature. Like r
, it is used in the signature verification process.
Verification
As a final step, lets see how we can verify if the request signature is that of the signer
and no one else. For this we will again be using the Openzeppelin’s ECDSA contract’s recover
function.
function recover(bytes32 hash, bytes memory signature) internal pure returns (address)
and match the returned address to the signer’s address. If they match, it proves that the signature on the request hash belongs to the signer
.
💡As you can see, the ECDSA recover(bytes32 hash, bytes memory signature
) function takes in a signature object instead of the tuple. So we got to convert!
function testToVerifySigner() external {
(address signer, uint256 signerPK) = makeAddrAndKey("signer");
bytes32 memory requestHash = contract.generateRequestHash();
// Setup
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK, requestHash);
bytes memory signature = abi.encodePacked(r, s, v);
// concadinating the indiviual attributes of the signature object into a single signature object. Pls note the order of concadenation here!
// ACT
address recoverdSignerAddress = ECDSA.recover(requestHash, signature);
// Assert
assertEq(recoveredSignerAddress, signer); // passes
}
So there you have it! This is how you can create a request hash, sign it and even verify the signer’s signature, in Foundry. Hope you learnt a ton from this one. If you have any questions/suggestions, feel free to drop a comment.
For more such blogs on Smart contract development and Blockchain engineering consider subscribing to my blog. Cheers!🧑🏻💻