1- import { http , createTestClient , size , slice } from "viem"
1+ import { http , createTestClient , padHex , size , slice } from "viem"
22import {
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