Skip to content

Commit de8881e

Browse files
committed
test(safe): tighten dynamic owner test
- Replace the mock ERC-1271 code with a signature-dependent validator - Use a fixed marker signature to verify the dynamic owner path
1 parent 58005fe commit de8881e

1 file changed

Lines changed: 35 additions & 13 deletions

File tree

packages/permissionless/accounts/safe/signUserOperation.test.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { http, createTestClient, size, slice } from "viem"
1+
import { http, createTestClient, padHex, size, slice } from "viem"
22
import {
33
entryPoint06Address,
44
entryPoint07Address
@@ -190,11 +190,13 @@ describe("signUserOperation", () => {
190190

191191
testWithRpc("signUserOperation_V07 with dynamic owner", async ({ rpc }) => {
192192
const eoaOwner = privateKeyToAccount(generatePrivateKey())
193-
const dynamicSignerKey = privateKeyToAccount(generatePrivateKey())
194193

195-
// Contract owner validated through EIP-1271. The runtime code echoes
196-
// the called selector back, so isValidSignature always returns the
197-
// expected magic value (both the bytes32 and legacy bytes variants).
194+
// Contract owner validated through EIP-1271. The owner "signs" with a
195+
// fixed 65-byte marker signature and the owner contract only returns
196+
// the magic value when that marker arrives in the signature argument,
197+
// proving the dynamic part is threaded through to isValidSignature.
198+
const markerSignature = padHex("0xc0ffee", { dir: "right", size: 65 })
199+
198200
const erc1271OwnerAddress = privateKeyToAccount(
199201
generatePrivateKey()
200202
).address
@@ -205,21 +207,41 @@ describe("signUserOperation", () => {
205207
chain: foundry
206208
})
207209

210+
// Minimal EIP-1271 validator. Safe v1.4.1 calls the legacy
211+
// isValidSignature(bytes _data, bytes _signature) variant, so the
212+
// calldata layout is: 4-byte selector, then the two head words with
213+
// the offsets of the `_data` and `_signature` tails. The code loads
214+
// the first 32 bytes of `_signature`, requires them to start with the
215+
// 0xc0ffee marker, and only then returns the magic value 0x20c13b0b
216+
// (the selector of the legacy variant); anything else reverts:
217+
//
218+
// PUSH1 0x24 CALLDATALOAD // offset of `_signature` tail
219+
// PUSH1 0x24 ADD // + 4-byte selector + 32-byte length
220+
// CALLDATALOAD // signature[0..32]
221+
// PUSH1 0xe8 SHR // >> 232 bits: keep first 3 bytes
222+
// PUSH3 0xc0ffee EQ // matches the marker?
223+
// PUSH1 0x17 JUMPI // if so, jump to the return block
224+
// PUSH1 0x00 PUSH1 0x00 REVERT
225+
// JUMPDEST // 0x17
226+
// PUSH4 0x20c13b0b // legacy EIP-1271 magic value
227+
// PUSH1 0xe0 SHL // left-align it as bytes4
228+
// PUSH1 0x00 MSTORE // store word at memory offset 0
229+
// PUSH1 0x20 PUSH1 0x00 RETURN // return that 32-byte word
208230
await testClient.setCode({
209231
address: erc1271OwnerAddress,
210-
bytecode: "0x60003560e01c60e01b60005260206000f3"
232+
bytecode:
233+
"0x6024356024013560e81c62c0ffee1460175760006000fd5b6320c13b0b60e01b60005260206000f3"
211234
})
212235

213-
// A KMS-style owner: signs with a local key but lives at the
214-
// ERC-1271 contract address.
236+
// An owner that lives at the ERC-1271 contract address and produces
237+
// the marker signature the contract expects.
215238
const dynamicOwner = toAccount({
216239
address: erc1271OwnerAddress,
217-
async signMessage({ message }) {
218-
return dynamicSignerKey.signMessage({ message })
240+
async signMessage() {
241+
return markerSignature
219242
},
220-
async signTypedData(typedData) {
221-
// biome-ignore lint/suspicious/noExplicitAny: test helper
222-
return dynamicSignerKey.signTypedData(typedData as any)
243+
async signTypedData() {
244+
return markerSignature
223245
},
224246
async signTransaction() {
225247
throw new Error("Not supported")

0 commit comments

Comments
 (0)