Skip to content

Commit 5219c96

Browse files
committed
feat(safesignatures): implement contract signature verification
To support filecoin we need to bypass the SafeSDK to verify Safe signatures. The SafeSignatureVerifier class was updated to support different strategies based on chain ID. A SignatureVerifierFactory was implemented to either execute a contract call or use the Safe SDK depending on the provided chain ID.
1 parent e7032c8 commit 5219c96

File tree

4 files changed

+95
-15
lines changed

4 files changed

+95
-15
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@graphql-yoga/plugin-response-cache": "^3.5.0",
3131
"@hypercerts-org/contracts": "2.0.0-alpha.12",
3232
"@hypercerts-org/marketplace-sdk": "0.3.37",
33-
"@hypercerts-org/sdk": "2.5.0-beta.3",
33+
"@hypercerts-org/sdk": "2.5.0-beta.6",
3434
"@ipld/car": "^5.2.5",
3535
"@openzeppelin/merkle-tree": "^1.0.5",
3636
"@safe-global/api-kit": "^2.5.4",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/safe/signature-verification/SafeSignatureVerifier.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import Safe from "@safe-global/protocol-kit";
21
import { getAddress, hashTypedData, type HashTypedDataParameters } from "viem";
32

43
import { EvmClientFactory } from "../../../client/evmClient.js";
54

65
import { RpcStrategyFactory } from "../safe-rpc-urls.js";
6+
import { SignatureVerifierStrategyFactory } from "./SignatureVerifierStrategy.js";
77

88
export default abstract class SafeSignatureVerifier {
99
protected chainId: number;
@@ -35,13 +35,10 @@ export default abstract class SafeSignatureVerifier {
3535

3636
abstract buildTypedData(): Omit<HashTypedDataParameters, "domain">;
3737

38-
async verify(signature: string): Promise<boolean> {
39-
const safe = await Safe.default.init({
40-
provider: this.rpcUrl,
41-
safeAddress: this.safeAddress,
42-
});
43-
44-
const protocolKit = await safe.connect({});
45-
return protocolKit.isValidSignature(this.hashTypedData(), signature);
38+
async verify(signature: `0x${string}`): Promise<boolean> {
39+
return SignatureVerifierStrategyFactory.getStrategy(
40+
this.chainId,
41+
this.hashTypedData(),
42+
).verify(signature, this.rpcUrl, this.safeAddress);
4643
}
4744
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import Safe from "@safe-global/protocol-kit";
2+
import { ethers } from "ethers";
3+
4+
const EIP1271_MAGIC_VALUE =
5+
"0x1626ba7e00000000000000000000000000000000000000000000000000000000";
6+
7+
export interface SignatureVerifierStrategy {
8+
verify(
9+
signature: `0x${string}`,
10+
rpcUrl: string,
11+
safeAddress: `0x${string}`,
12+
): Promise<boolean>;
13+
}
14+
15+
export class SignatureVerifierStrategyFactory {
16+
static getStrategy(
17+
chainId: number,
18+
hashTypedData: `0x${string}`,
19+
): SignatureVerifierStrategy {
20+
switch (chainId) {
21+
case 314:
22+
case 314159:
23+
return new ContractSignatureVerifierStrategy(hashTypedData);
24+
default:
25+
return new SafeSignatureVerifierStrategy(hashTypedData);
26+
}
27+
}
28+
}
29+
30+
export class SafeSignatureVerifierStrategy
31+
implements SignatureVerifierStrategy
32+
{
33+
private hashTypedData: `0x${string}`;
34+
constructor(hashTypedData: `0x${string}`) {
35+
this.hashTypedData = hashTypedData;
36+
}
37+
38+
async verify(
39+
signature: `0x${string}`,
40+
rpcUrl: string,
41+
safeAddress: `0x${string}`,
42+
): Promise<boolean> {
43+
const safe = await Safe.default.init({
44+
provider: rpcUrl,
45+
safeAddress: safeAddress,
46+
});
47+
48+
const protocolKit = await safe.connect({});
49+
return protocolKit.isValidSignature(this.hashTypedData, signature);
50+
}
51+
}
52+
53+
export class ContractSignatureVerifierStrategy
54+
implements SignatureVerifierStrategy
55+
{
56+
private hashTypedData: `0x${string}`;
57+
constructor(hashTypedData: `0x${string}`) {
58+
this.hashTypedData = hashTypedData;
59+
}
60+
61+
async verify(
62+
signature: `0x${string}`,
63+
rpcUrl: string,
64+
safeAddress: `0x${string}`,
65+
): Promise<boolean> {
66+
const provider = new ethers.JsonRpcProvider(rpcUrl);
67+
const iface = new ethers.Interface([
68+
"function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)",
69+
]);
70+
const calldata = iface.encodeFunctionData("isValidSignature", [
71+
this.hashTypedData,
72+
signature,
73+
]);
74+
75+
try {
76+
const result = await provider.call({ to: safeAddress, data: calldata });
77+
return result === EIP1271_MAGIC_VALUE;
78+
} catch (error) {
79+
console.error("Error:", error);
80+
return false;
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)