A signature says who, not what
A signed transaction feels final. But a signature proves who approved something valid — not that the exact thing executing is the thing they approved. Here is what DeFiLoops puts in that gap: a content receipt, and a simulation that judges what a transaction actually does.
A signed transaction feels like the end of the story. Someone with authority approved it, the signature verifies, the chain will honour it. Done.
But there is a gap between approved and executed, and it is wider than it looks. Between the moment an agent signs and the moment a block includes the transaction, a route gets chosen, calldata gets built, and minutes pass. A signature is a statement about who approved — it says almost nothing about what finally runs. Closing that gap is most of the security work, and none of it is the signature.
What the agent actually signs
Our agent never signs a raw transaction. It signs a typed intent — an amount, a venue named from a list, a range — and the adapter builds the call from those parameters. That is the whole reason a compromised agent cannot aim a call at an attacker: it was never handed a target to aim (we wrote about that here).
But typed or not, a signature covers only the fields it hashes. And an EIP-712 message hashes a subset.
This is the crack. Anything that can change one of those uncovered fields between signing and executing changes what happens while leaving every signature verifying. Re-checking that the intent is still valid does not help — the changed intent is also valid. Both checks agree, about different intents.
Two approvals, minutes apart
An adapter plan is authorised twice, deliberately, because the calldata does not exist at the first approval.
- Sign the group, and mint a receipt
The signer validates every step, signs each one’s EIP-712 message, and mints a validation receipt — a statement, signed by the same key, that says: I validated intents with exactly these content hashes, under exactly these limits, for this account on this chain. The content hash covers every field, not the signed subset.
- Build the route the plan could not know yet
Only now does routing pick the swap’s pools and assemble the real calldata. This is the entire reason there is a second approval — the bytes did not exist a moment ago.
- Authorise the exact bytes, and check the receipt
The second hop recomputes the content hash of what it is holding and checks it against the receipt. Same intent, bit for bit — including every field the chain will never see. If a single uncovered field changed, the hashes disagree and it refuses.
The receipt does one precise thing: it replaces “is this valid?” — which the first hop already answered and the second cannot re-answer usefully — with “is this the same?”, which is the question that actually matters. Everything the second hop can only learn at the second hop, like whether the route filled well, it still learns there. Freshness is never trusted from the receipt; a receipt minted at 12:00 says nothing about 12:20, so timing is re-checked regardless.
The check that looks at what a transaction does
Everything above proves the transaction is the one that was approved. It does not prove the transaction is safe — a correctly-signed, correctly-matched intent can still route through a pool that fills terribly, or leave an allowance behind. So before anything is broadcast, we run it.
The simulator executes the transaction against a real, recent block without broadcasting —
debug_traceCall — and reports every call, log and state change. Then one rule judges the
result. It is the only rule in the system that looks at what a transaction does rather than
what was asked for.
- A net drain the plan did not declare
- The account may only lose what the plan says it loses. Anything beyond that, refused.
- Any leftover allowance
- Every (owner, spender) allowance must end at zero. A standing allowance is the most valuable thing an attacker can leave behind — it outlives the transaction, and nothing else looks for it.
- An output below the floor
- What the plan buys must arrive at or above the floor the plan already signed. A badly filled route is refused here — cheaply, before gas — instead of reverting on chain.
- An NFT that no step moved
- No position or token moves unless a step in the plan moves it. A Uniswap position is an NFT, and its id is a number that has been mistaken for an amount before.
The trick that makes this possible is that the simulation returns the real numbers, so it needs no price oracle — it compares against the floor the plan already carries. And a reverting call is not an error to route around; it is the finding. If any call in the bundle reverts, none of the transaction’s effects persist, and refusing before broadcast turns a wasted-gas on-chain revert into a cheap, legible no.
Everything in the gap fails closed
Line these up and the shape is the same at every step: the safe answer is no, and it is the default.
An unbound guard refuses. An absent receipt refuses. A content hash that does not match refuses. A simulation that does not match the plan refuses — before a single byte is broadcast. None of these is the signature. The signature was never the hard part; it only ever proved that someone approved something. The pipeline exists to prove it is this something, and that this something does what the plan said.
How to evaluate any of this
You do not need to trust this page. The question that separates a real execution pipeline from a signature and a prayer is short: between the moment it is approved and the moment it lands, what is checked?
If the answer is “the signature verifies,” then every field a signature does not cover is open, and whatever builds the calldata is trusted completely. If the answer names a content receipt and a simulation of effects, then the two things a signature cannot do — prove sameness, and judge what actually happens — are being done by something that can.