Skip to content

Calldata Decoding in a Wallet

A verification-first guide to transaction calldata, ABI words and offsets, selector collisions, proxies, batches, approvals, typed-data permits, simulation and post-transaction reconciliation.

Updated

For educational purposes only; not investment, legal, or security advice. Decoding and simulation reduce ambiguity but do not guarantee authorization, execution, asset safety, or finality.

Direct answer

Calldata is the immutable byte string supplied as input to a top-level Ethereum transaction or an internal message call. A conventional Solidity function call starts with a 4-byte selector followed by ABI-encoded arguments, but calldata is not self-describing: the same bytes can mean different things for different runtime code, proxy implementations or schemas. Fallback functions, raw assembly and non-Solidity protocols need not follow the conventional function ABI at all.

A wallet should therefore show more than a candidate function name. Safe review binds the bytes to chainId, a block, from, to, native value, runtime codeHash, the active implementation and a trusted ABI; strictly decodes every nested call; distinguishes on-chain calldata from EIP-712 signatures; simulates in an explicit state; and reconciles the actual receipt and state changes after inclusion.

Calldata decoding
0 / 5
0 items reviewed; 5 items still unresolved

Completing this review does not prove an asset, transaction, or system is safe.

How it works

  1. Pin the signing envelope and observation point: chainId, block number and hash, from, to, native value, input bytes, nonce and fee fields. Preserve the wallet or RPC source; a decoded payload from another chain or block is not the same claim.
  2. Classify the object before decoding. A transaction, EIP-712 typed-data request, ERC-2612 permit, ERC-4337 UserOperation and raw personal-sign message use different domains and schemas; do not force all of them through the transaction ABI.
  3. Resolve the target at the pinned block. Read runtime bytecode and codeHash; identify a proxy, beacon or implementation where applicable; record implementation and admin slots; and obtain an ABI that matches that exact code version. A selector registry provides candidates, not authority.
  4. Decode strictly. The selector is the first 4 bytes of Keccak-256 over the canonical function signature, excluding return types. Static values occupy 32-byte words; dynamic heads contain offsets from the arguments block after the selector. Reject truncated data, out-of-bounds offsets, impossible lengths, invalid padding and unexplained trailing bytes.
  5. Recursively expand multicalls, nested calldata and delegated execution. For every child, list target, native value, selector, arguments, call type and any allowFailure flag. With delegatecall, implementation code runs in the caller’s address, balance and storage context while preserving msg.sender and msg.value.
  6. Build separate authority and value ledgers, then simulate. Record recipients, spenders, NFT operators, raw token units, decimals, deadlines, slippage bounds and native value. Simulate with the exact block, sender and value, but treat the result as a conditional snapshot because state, prices, time, code and transaction ordering can change.
  7. Confirm every material field before signing. After inclusion, inspect receipt status, logs, traces where available and balance, allowance and operator-state deltas; distinguish caught child failures from top-level success; account for gas even on a revert; wait for the required finality; and stop rather than blindly re-signing an unexplained failure.

Worked examples

  • Static ERC-20 transfer. transfer(address,uint256) commonly uses selector 0xa9059cbb. One selector plus two ABI words is 4 + 2 * 32 = 68 bytes. A raw amount of 1,500,000 for a token independently verified to use 6 decimals displays as 1.5 tokens. Decimals are external contract metadata, not encoded in those arguments, and the selector alone does not identify the contract or function uniquely.
  • Dynamic bytes offset. For f(address,bytes) with a 3-byte payload, the two-word head occupies 64 bytes. The dynamic offset is 0x40, measured from the start of the arguments block and excluding the selector. Its tail contains one 32-byte length word and one 32-byte padded data word, so total calldata is 4 + 64 + 32 + 32 = 132 bytes. Treating the offset as absolute from byte zero lands four bytes late.
  • Batch value is implementation-specific. An outer call carries 1.00 ETH; three decoded children explicitly request 0.20 ETH, 0.30 ETH and 0.10 ETH, totaling 0.60 ETH. The remaining 0.40 ETH might be refunded, retained, forwarded or cause a revert depending on the batch code. If the third child fails with allowFailure=true, earlier children may remain committed; an atomic implementation may instead revert everything.
  • A permit is not the relayer’s calldata at signing time. An owner with 1,000 USDC signs an ERC-2612 permit for 300 USDC at nonce 41. The signature alone changes neither balance nor allowance. After a relayer successfully submits it, nonce becomes 42 and allowance becomes 300; after the spender uses 180, balance is 820 and remaining allowance is 120. Disconnecting the site does not revoke the permission.

Risks

  • Decoding against the wrong chain, fork, block tag or transaction envelope.
  • Signing for a spoofed domain, target address or recipient.
  • Treating a 4-byte selector as unique despite possible collisions.
  • Using a guessed, stale or incorrectly verified ABI.
  • Trusting a verified source label without matching current runtime codeHash.
  • Missing an implementation, beacon or admin upgrade between review and execution.
  • Ignoring a proxy function whose selector clashes with the implementation.
  • Forgetting that delegatecall writes in the caller’s storage context.
  • Accepting malformed dynamic offsets, lengths, padding or trailing bytes.
  • Failing to expand a nested batch that hides targets, values or permissions.
  • Assuming atomicity when the implementation catches or permits child failures.
  • Ignoring top-level native value because token arguments look harmless.
  • Applying wrong decimals or assuming fee-on-transfer and rebasing tokens are standard ERC-20s.
  • Granting unlimited ERC-20 allowance or mishandling the allowance-update race.
  • Overlooking the collection-wide scope of NFT setApprovalForAll.
  • Mistaking EIP-712 typed data or an ERC-2612 permit for transaction calldata.
  • Missing nonce, deadline, verifying-contract, chain-domain or replay boundaries.
  • Treating simulation as stable despite oracle, timestamp, pending-state, MEV or code changes.
  • Treating receipt status, logs or provider traces as a complete economic state proof.
  • Blindly re-signing through a compromised UI or ignoring inclusion, reorg and finality risk.

Common misconceptions

  • A function selector uniquely identifies what the contract will execute.
  • A verified front-end summary is identical to the bytes and current implementation being signed.
  • A transaction with value=0 cannot move tokens, NFTs or delegated assets.
  • Successful simulation or a successful receipt proves safety and the intended economic outcome.
  • Disconnecting a dapp revokes approvals, permits and NFT operator permissions.

Sources

Navigation

Search the wiki...