diff --git a/.changeset/grumpy-melons-grow.md b/.changeset/grumpy-melons-grow.md new file mode 100644 index 0000000000..dfd599d0ca --- /dev/null +++ b/.changeset/grumpy-melons-grow.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fixed an issue where \`hashAuthorization\` & \`serializeAuthorizationList\` did not RLP-serialize zeroish nonces correctly. diff --git a/src/actions/wallet/sendTransaction.test.ts b/src/actions/wallet/sendTransaction.test.ts index 96e062bc35..b73c495772 100644 --- a/src/actions/wallet/sendTransaction.test.ts +++ b/src/actions/wallet/sendTransaction.test.ts @@ -10,6 +10,7 @@ import { maxUint256 } from '~viem/constants/number.js' import { BatchCallInvoker } from '../../../contracts/generated.js' import { getSmartAccounts_07 } from '../../../test/src/account-abstraction.js' import { deploy } from '../../../test/src/utils.js' +import { generatePrivateKey } from '../../accounts/generatePrivateKey.js' import { createWalletClient } from '../../clients/createWalletClient.js' import { http } from '../../clients/transports/http.js' import { signAuthorization } from '../../experimental/index.js' @@ -1406,3 +1407,48 @@ describe('errors', () => { ) }) }) + +test('https://github.com/wevm/viem/issues/2721', async () => { + const invoker = privateKeyToAccount(generatePrivateKey()) + const authority = privateKeyToAccount(generatePrivateKey()) + const recipient = privateKeyToAccount(generatePrivateKey()) + + await setBalance(client, { + address: invoker.address, + value: parseEther('100'), + }) + await setBalance(client, { + address: authority.address, + value: parseEther('100'), + }) + + const { contractAddress } = await deploy(client, { + abi: BatchCallInvoker.abi, + bytecode: BatchCallInvoker.bytecode.object, + }) + + const authorization = await signAuthorization(client, { + account: authority, + contractAddress: contractAddress!, + }) + + const hash = await sendTransaction(client, { + account: invoker, + authorizationList: [authorization], + data: encodeFunctionData({ + abi: BatchCallInvoker.abi, + functionName: 'execute', + args: [ + [ + { + to: recipient.address, + data: '0x', + value: parseEther('1'), + }, + ], + ], + }), + to: authority.address, + }) + expect(hash).toBeDefined() +}) diff --git a/src/experimental/eip7702/utils/hashAuthorization.ts b/src/experimental/eip7702/utils/hashAuthorization.ts index 6b544a3fbd..9a6773ab66 100644 --- a/src/experimental/eip7702/utils/hashAuthorization.ts +++ b/src/experimental/eip7702/utils/hashAuthorization.ts @@ -48,7 +48,11 @@ export function hashAuthorization( const hash = keccak256( concatHex([ '0x05', - toRlp([numberToHex(chainId), contractAddress, numberToHex(nonce)]), + toRlp([ + numberToHex(chainId), + contractAddress, + nonce ? numberToHex(nonce) : '0x', + ]), ]), ) if (to === 'bytes') return hexToBytes(hash) as HashAuthorizationReturnType diff --git a/src/experimental/eip7702/utils/serializeAuthorizationList.ts b/src/experimental/eip7702/utils/serializeAuthorizationList.ts index 99d2fcd007..71df1faa72 100644 --- a/src/experimental/eip7702/utils/serializeAuthorizationList.ts +++ b/src/experimental/eip7702/utils/serializeAuthorizationList.ts @@ -24,7 +24,7 @@ export function serializeAuthorizationList( serializedAuthorizationList.push([ toHex(chainId), contractAddress, - toHex(nonce), + nonce ? toHex(nonce) : '0x', ...toYParitySignatureArray({}, signature), ]) }