Skip to content

Trampoline isolation from GPv2Settlement funds

Status: proven (COW-1152)

What this guarantees

A sub-solver authors an arbitrary route — a list of (target, value, callData) interactions — that the Trampoline runs during a settlement. This document states, and backs with tests, what such a route can and cannot reach.

The guarantee is structural rather than filtered. Routes execute as the Trampoline instance (msg.sender is the instance), never as GPv2Settlement, so they inherit none of the settlement's buffer-spend or approval-granting authority (#topology). execute sweeps the instance's full remaining balance of both trade tokens to the settlement (#residue), so the instance is empty of trade tokens at rest, and each sub-solver has a distinct instance. The blast radius of any route is the trade capital in flight during its own settlement.

The tests run against the real deployed GPv2Settlement on a mainnet fork, exercising CoW's actual semantics (allowance checks, onlySolver, the reentrancy guard, owner-scoped order state). A controlled ERC-20 buffer is seeded into the settlement in setUp, so every "no value moved" assertion runs against non-zero value.

The invariant asserted is that value does not move — buffer balances, allowances, and order state are unchanged after the route runs. A revert is one mechanism that enforces this, but it is not the bar: several attacks are also proven inside a settlement that succeeds (the failed attack swallowed so the transaction finalizes), because a real adversary wants the settlement to complete unattributed rather than self-abort.

Reachability

TargetReachable by a routeWhyBacking
Own instance balance, in flightyesThe route runs as the instance, so during its own settlement it moves the instance's balance freely. This is the boundary's positive edge, and isolation is instance-scoped, not token-scoped: a route reaches the capital passing through its own instance while the settlement's buffer of the same token stays put. At rest there is nothing left to reach — the sweep empties the instance of trade tokens, so a planted approval drains nothing.Cited: test_execute_sweeps_full_route_output_and_emits_executed, test_execute_buy_order_sweeps_unconsumed_sell_token_to_settlement, test_planted_approval_cannot_reach_other_instances_residue (test/Trampoline/Trampoline.t.sol)
Settlement token buffersnoA transferFrom from the settlement needs an allowance the settlement never granted the instance. Proven inside a successful settlement where the failed attempt is swallowed, so the guarantee holds even when the transaction finalizes rather than aborting.test_settlement_succeeds_but_buffer_transferFrom_moves_nothing
Settlement via re-entering settle()nosettle is nonReentrant onlySolver. A route always runs inside a live settle, so the reentrancy guard (the first modifier) reverts before onlySolver is even reached. onlySolver is the backstop that applies if the guard weren't engaged — the instance is not an allow-listed solver.test_route_cannot_reenter_settle (guard), test_route_settle_call_is_rejected_by_onlySolver (backstop)
Another party's order statenosetPreSignature and invalidateOrder require the order's encoded owner to equal msg.sender. A route is the instance, so it cannot pre-sign or cancel an order owned by anyone else; the victim's state is unchanged. A route can pre-sign an order it owns, but nobody places orders naming a Trampoline, so that capability is inert.test_route_cannot_presign_another_owners_order, test_route_cannot_invalidate_another_owners_order
Vault-relayer allowances (user funds)noThe vault relayer pulls users' sell tokens and is onlyCreator — only the settlement may call it. A route calling it is rejected at the gate even against a user who really approved the relayer.test_route_cannot_pull_through_vault_relayer
Other instances' balancesnoCross-instance isolation is a property of per-instance EVM storage; an approval or call from one instance grants nothing over another's balance (instances end settlements swept empty, but a stray token could still land outside the flow).Cited: test_route_cannot_call_another_instances_execute, test_planted_approval_cannot_reach_other_instances_residue, test_signature_from_other_factory_generation_fails (test/Trampoline/Trampoline.t.sol)
Escrow collateralnoCollateral lives in the Escrow contract, which never routes funds through a Trampoline; payouts are gated to escrow's own access-controlled roles, unreachable from a route.Cited: test/Escrow/AccessControl.t.sol, test/Escrow/SubSolverActions.t.sol

Two directions are deliberately inert rather than blocked, because they move value toward the settlement:

ActionEffectBacking
Approving the settlementGrants the settlement an allowance over the instance's funds, not the reverse; the instance holds nothing for it to reach.test_route_approving_settlement_is_inert
Sending native value at the settlementA one-way donation; the settlement ends richer, the instance poorer, nothing extracted.test_route_sending_value_at_settlement_is_inert

Running the proofs

The suite (test/fork/SettlementIsolation.t.sol) is fork-gated. It uses a public RPC by default, so it runs in CI without extra configuration; override with MAINNET_RPC_URL, or set it empty to skip when offline.

MAINNET_RPC_URL=<url> forge test --match-path test/fork/SettlementIsolation.t.sol

This specification is normative. Where an implementation disagrees with it, the implementation is wrong.