If you've ever configured a delegated signer, a session key, or a bot's wallet permissions, you've probably reached for two knobs: an allowlist of assets it's allowed to touch, and a cap on how much value it can move. It's tempting to treat these as two settings on the same dial — both feel like "limits." They're not. They prevent different failure modes, and conflating them is how permission systems end up weaker than their configuration screen suggests.
What a spending cap actually prevents
A spending cap bounds magnitude. If a delegated key can move at most 500 USDC per day, then no matter what it does with that authority — swap, stake, send to an arbitrary address — the maximum damage from a compromised or misbehaving key is 500 USDC. It says nothing about what the money touches. A cap enforced correctly stops a runaway loop or a fully compromised signer from draining a wallet in one shot. It does not stop that signer from moving the capped amount into something worthless.
What an allowlist actually prevents
An allowlist bounds scope. If a signer can only interact with three named token contracts, then even with no spending cap at all, it categorically cannot send funds to an arbitrary address or interact with an unknown contract. This is a different property: it's not about how much moves, it's about which counterparties are even reachable.
The two compose well in theory — cap the blast radius, and separately narrow the attack surface — but each has failure modes the other doesn't cover, and "we have both" is not the same claim as "we're safe."
Failure mode 1: the allowlisted token isn't the code you allowlisted
An allowlist entry is usually just an address. But an address is not fixed behavior. If the token contract sits behind a proxy — which a large fraction of tokens do, for legitimate upgrade reasons — the logic executed when your signer calls transfer or approve can change after you added the address to your list. You audited version 1. The proxy now points at version 2. Your allowlist still matches, because the allowlist only ever checked the address, and the address never moved. This isn't a hypothetical: it's the entire reason "audit the contract, then allowlist the address" is a weaker guarantee than people assume — the audit has a timestamp, the allowlist doesn't.
Failure mode 2: the router that accepts arbitrary calldata
A lot of "swap-only" permission schemes are enforced by pointing the signer at a router contract instead of individual token contracts, on the theory that a router only does swaps. But look at what the router's function signature actually accepts. Plenty of router and aggregator interfaces expose something shaped like:
function execute(address target, bytes calldata data) external;
That's not a swap function. That's a generic call forwarder that happens to usually be used for swaps. If your policy allowlists the router's address and stops there, you've allowlisted "anything the router is willing to forward," which — depending on the router's own internal checks — can include calls into arbitrary target contracts with arbitrary calldata. The allowlist has to be checked against the target and selector inside the calldata, not just the outermost contract address, or it isn't restricting anything meaningful.
Failure mode 3: "it can only swap" is a claim about a function signature you haven't read
This is the general version of failure mode 2. "Swap-only" is not a security property until you know exactly what parameters the swap function takes. A swap function that hard-codes the output token and the pool is a narrow, auditable action. A swap function that takes a path: address[] or a router: address parameter chosen by the caller is, functionally, "call arbitrary code with a legitimate-looking wrapper around it." The English sentence "it can only swap" is doing no work — the actual boundary is whatever the function's parameter types and the policy engine's validation of those parameters allow. Read the signature, not the label.
A practical checklist
When someone tells you an authorization is scoped, four questions separate a real boundary from a documentation claim:
- Allowlisted at what granularity — contract address, function selector, or full calldata shape?
- Capped on what — per-transaction, cumulative, or notional value at execution time (which can be gamed by slippage)?
- Enforced where — checked by a policy engine before signing, checked by the contract itself on-chain, or just described in a UI with no enforcement at all?
- What happens when a check fails — does the transaction revert, or does the check silently pass because the policy engine and the contract disagree about what "the same asset" means?
None of this makes allowlists useless. It makes them one layer, with a specific and narrow claim, that has to be paired with selector-level restriction and cap enforcement at the layer that actually executes the call — and it means the honest description of any "restricted signer" is a list of exactly what's checked and where, not the word "scoped" on its own.
Disclosure: I build Tradevo, which runs delegated strategy execution through a scoped session signer with a token allowlist, per-subscription allocation cap, and slippage limits — enforced by the signing provider's policy and by our own executor, not by the chain, which is exactly why I spent this piece on where enforcement actually lives instead of what the label says.
Top comments (0)