Incorrect encoding of bytes for EIP712 digest in TitleGraph causes signatures generated by common EIP712 tools to be unusable
Summary
The signature in TitleGraph.acknowledgeEdge() and TitleGraph.unacknowledgeEdge() is generated based on a digest computed from edgeId and data. However, the data bytes argument is not correctly encoded according to the EIP712 specification. Consequently, a signature generated using common EIP712 tools would not pass validation in TitleGraph.checkSignature().
However, the checkSignature() modifier in the TitlesGraph contract reconstructs the digest by encoding the data bytes argument without first applying keccak256 hashing.
As a result, a signature generated using common EIP712 tools (e.g. using the signTypedData function from ethers.js) would not pass validation in TitleGraph.checkSignature().
POC
EIP712 signature computed by using ethers.js
// main.js
const { ethers } = require("ethers");
async function main() {
const pk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const signer = new ethers.Wallet(pk);
const domain = {
name: "TitlesGraph",
version: '1',
chainId: 31337,
verifyingContract: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" // should match the address in foundry test
};
const types = {
Ack: [
{ name: "edgeId", type: "bytes32" },
{ name: "data", type: "bytes" },
],
};
const value = {
edgeId: ethers.id("test edgeId"),
data: "0xabcd"
};
const signature = await signer.signTypedData(domain, types, value);
console.log(signature);
}
main();
here we run
```bash
npm install ethers
node main.js
The output is 0xab4623a7bacf25ed3d6779684f195ed63a5ed1ed46c278c107390086e74b739b35f1db213c6075dedc041d68ced3d11798d49afaf3c47743d4696c49f03037b51b
test_sig() simulates the way the digest is reconstructed in TitleGraph.checkSignature(), while test_sigShouldBe() shows how the digest should be reconstructed.
From the above output, we can see the signature generated by ethers.js matches the signature generated in test_sigShouldBe() and does not match the signature generated in test_sig().
This PoC shows the way TitleGraph.checkSignature() reconstruct the digest is not compatible with the way data is encoded in EIP712.
Impact
A signature generated by the signer using common EIP712 tools (e.g. signTypedData in ethers.js) would not pass validation in TitleGraph.checkSignature().
mt030d
medium
Incorrect encoding of bytes for EIP712 digest in
TitleGraph
causes signatures generated by common EIP712 tools to be unusableSummary
The signature in
TitleGraph.acknowledgeEdge()
and TitleGraph.unacknowledgeEdge()
is generated based on a digest computed from edgeId
and data
. However, the data
bytes argument is not correctly encoded according to the EIP712 specification. Consequently, a signature generated using common EIP712 tools would not pass validation in TitleGraph.checkSignature()
.Vulnerability Detail
According to EIP712:
However, the
checkSignature()
modifier in theTitlesGraph
contract reconstructs the digest by encoding the data bytes argument without first applying keccak256 hashing. As a result, a signature generated using common EIP712 tools (e.g. using thesignTypedData
function fromethers.js
) would not pass validation in TitleGraph.checkSignature()
.POC
async function main() { const pk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; const signer = new ethers.Wallet(pk); const domain = { name: "TitlesGraph", version: '1', chainId: 31337, verifyingContract: "0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496" // should match the address in foundry test }; const types = { Ack: [ { name: "edgeId", type: "bytes32" }, { name: "data", type: "bytes" }, ], }; const value = { edgeId: ethers.id("test edgeId"), data: "0xabcd" }; const signature = await signer.signTypedData(domain, types, value); console.log(signature);
}
main();
The output is
0xab4623a7bacf25ed3d6779684f195ed63a5ed1ed46c278c107390086e74b739b35f1db213c6075dedc041d68ced3d11798d49afaf3c47743d4696c49f03037b51b
import {Test, console} from "forge-std/Test.sol"; import {EIP712} from "lib/solady/src/utils/EIP712.sol";
contract EIP712Test is Test, EIP712 { bytes32 public constant ACK_TYPEHASH = keccak256("Ack(bytes32 edgeId,bytes data)");
}
The output is
test_sig()
simulates the way the digest is reconstructed inTitleGraph.checkSignature()
, whiletest_sigShouldBe()
shows how the digest should be reconstructed. From the above output, we can see the signature generated by ethers.js matches the signature generated intest_sigShouldBe()
and does not match the signature generated intest_sig()
. This PoC shows the wayTitleGraph.checkSignature()
reconstruct the digest is not compatible with the way data is encoded in EIP712.Impact
A signature generated by the signer using common EIP712 tools (e.g. signTypedData in
ethers.js
) would not pass validation in TitleGraph.checkSignature()
.Code Snippet
https://github.com/sherlock-audit/2024-04-titles/blob/main/wallflower-contract-v2/src/graph/TitlesGraph.sol#L41
Tool used
Manual Review, ethers.js, foundry
Recommendation
Encoding the
data
bytes as a keccak256 hash of its contents before computing the digest from it: