Oct 27, 202610 min read

How I'd test a DeFi wallet policy

Turn an intended DeFi trade into tests that expose unintended permissions, check the fix and establish which checks are needed before approval.

JohnnyTimeFounder, Ginger Security

When I work with hedge funds, trading firms and other institutions in DeFi, I often think about how to give a trading team room to move quickly while keeping control of what its wallet can do.

It sounds simple. You pick the protocols the team can use, set some limits, add approvals and let them trade.

But a transaction can look almost identical to a legitimate trade and still send the proceeds somewhere else. I want to know whether the policy catches it.

In my DeFi security due-diligence article, I talked about understanding who can move assets. Here, I will test what a trading wallet is allowed to do.

We will start with a single swap, find a gap in a policy that looks reasonable, and work through the tests I would want to see before relying on the fix. I have also included 15 downloadable test cases you can adapt to your own setup.

The trade and numbers below are an illustrative example. They give us something concrete to test without exposing anyone’s wallet or operations.

The trade we want to allow

Let’s say a trading team wants to swap 1,000 USDC for WETH on Ethereum using an approved Uniswap v3 router. WETH is wrapped ETH, a token form of ETH. The proceeds should go straight back to the trading wallet.

An authorized trader can propose the transaction, but two separate people need to approve it. For this example, the quote is 0.4 WETH, the minimum acceptable output is 0.396 WETH, and the request expires after two minutes. A different trade would need its own quote and limits.

That is what the team wants. Now we need to translate it into something the wallet can enforce.

A wallet policy defines the conditions under which a transaction can be approved or signed. Here is a first attempt, written in plain English:

Allow an authorized trader to:
  use the trading wallet on Ethereum
  call the approved Uniswap router
  use the single-swap method
  spend up to 1,000 USDC to buy WETH
  keep the reviewed minimum output and deadline
Block other actions.

At first glance, this looks pretty good. We have the right wallet, the right router, the right assets and a spending limit. So let’s try it.

The first test looks good. The second one doesn’t.

I would start with the transaction the team actually wants: swap 1,000 USDC and send the resulting WETH back to the trading wallet. The policy allows it. Great.

Now I would copy that transaction and change just one thing: send the WETH to another wallet.

Same router. Same assets. Same amount. Different receiver.

The policy still allows it.

The trading wallet pays for the swap, and the other wallet receives the WETH. The router has done exactly what the transaction asked it to do. Our policy never said where the proceeds had to go.

This is possible because the contract being called and the address receiving the tokens are separate fields. Uniswap v3’s single-swap interface calls the latter recipient. You can see how it is passed into the swap in the router implementation.

The trading wallet spends 1,000 USDC through the approved swap router. The intended trade returns 0.4 WETH to the trading wallet. Changing only the receiver can send the WETH to another wallet if the policy leaves that field open.
One field changes where the purchased tokens end up.

The fix is to add the missing condition:

 Keep the existing swap conditions.
+Require recipient == trading_wallet_address.

Then I would run the two transactions again. The intended swap should still pass. The swap to another wallet should be blocked. I would also try a transaction whose receiver the custody system cannot decode. If it cannot establish where the WETH is going, I would expect it to block the request.

Test transaction Before the change After the change
Swap back to the trading wallet Allow Allow
Same swap, different receiver Allow Block
Receiver can’t be decoded Block Block

These are the policy decisions I would expect from the example. A real account needs its own verification. I would also check how the actual custody system reads the assets and receiver from the transaction data, and make sure it restricts the method to a single swap rather than a batch of other calls.

The new rule should reject the wrong receiver. Before relying on it, I want to see whether another rule lets that same transaction through.

The rule that gets there first

Imagine the team already had an older rule allowing this wallet to call the router. The new receiver restriction has been added underneath it:

1. Allow calls from this wallet to the router.
2. Block swaps that send output elsewhere.
3. Allow the narrowly defined swap.
4. Block everything else.

For this example, let’s assume the system checks rules from top to bottom and stops at the first match. The wrong-receiver transaction matches rule 1, so it is allowed before the system ever reaches rule 2.

A swap to another wallet matches an earlier rule allowing calls to the router. Evaluation stops, so the later receiver restriction is not reached. Removing the broad allow lets the narrow swap rule reject the changed receiver and the final block stop the transaction.
In this example, the first matching rule decides the result.

I would remove the broad permission or replace it with the full set of swap conditions. Moving the receiver block above it could stop this particular transaction, but the old rule might still allow a different asset, method or batch call. I would test those too, and keep the rule ID and evaluation trace for each result.

Not every custody product evaluates rules the same way. Fireblocks, for example, documents first-match policy examples and evaluation in defined order. The model above is an illustration. Before drawing a conclusion about an actual account, I would check that product’s rules, priorities and exceptions.

The permission that stays after the swap

There is another transaction hiding behind our trade: the token approval.

Before the router can spend USDC, it generally needs an allowance. That is permission to take tokens from the wallet. The approval is a separate transaction from the swap itself.

Suppose the wallet holds 50,000 USDC, but the trader asks for an unlimited allowance to make the 1,000 USDC trade. If approved, the router has spending authority over the USDC balance, subject to its code. Our 1,000 USDC swap limit does not shrink that allowance. After the trade, another 49,000 USDC remains in the wallet.

For this example, I would start with a confirmed zero allowance and allow an approval of exactly 1,000 USDC to the approved router. Then I would test an amount one smallest token unit higher and an unlimited approval. Both should be blocked. The team should also be able to set the allowance back to zero and revoke unused permission.

I would check this transaction separately because it calls the USDC contract and names the router as the spender. A rule that only recognizes the router as a destination does not cover it. The ERC-20 specification describes allowances and the risks of changing a nonzero approval. After the swap, I would read the remaining allowance instead of assuming it has been consumed.

Existing approvals matter too. So do other ways of granting spending permission. An ERC-2612 permit, for example, can change an allowance through a signed message. Blocking a direct approval transaction does not establish that every other route is closed.

There is one more question for the team: is 1,000 USDC a limit per trade or a total budget? A per-trade limit can still allow the same trade repeatedly. If the intention is a total budget, I would test that control across simultaneous requests and resets.

The signing service is another part of this picture. In my signer security article, I follow what a compromised bot can ask a key to sign and how to limit its access.

Who can make the request?

So far we have focused on what the wallet can do. Now I want to know who can make it happen.

The authorized trader should be able to propose the swap. An account without that permission should not. If the request comes through an API, I would verify which identity the credential actually represents. A field in the request saying “authorized trader” is not enough.

Then I would test the two approvals. One should not be enough. The same person should not count twice. In this example, the trader proposing the transaction should not fill either independent approval role.

I would also change the receiver after the first approval and check that the earlier approval no longer counts. Both people need to approve the same transaction, including its chain, wallet, router, value and complete encoded instruction.

And who can change the policy? Someone who can add a broad allow rule, change group membership or enroll another trading credential may be able to give themselves more access. I would review how those changes are authorized, and map any other signing or execution routes the team can use.

What would actually happen on-chain?

Let’s say the request passes the policy and has the right approvals. I still want to see what it is expected to do before signing.

A simulation runs the transaction against a particular blockchain state without broadcasting it. For our swap, I would look for 1,000 USDC leaving the trading wallet, at least 0.396 WETH arriving back, and the remaining allowance. I would also look for unexplained transfers or new spending permissions.

The simulation has to match the transaction being approved. A result for an earlier quote, a different wallet or different calldata, the encoded instruction sent to the contract, does not answer that question. I would keep the decoded transaction, policy revision, chain, block reference, code identities, starting balances and allowances with the result.

If the simulation is missing, reverts, is too old or leaves a relevant balance change unexplained, I would hold approval until the team has the simulation results. Even a successful simulation is a snapshot. Prices, code and permissions can change before execution, so the team needs to decide when a fresh simulation and approval are required.

Taking the tests to the real account

The example has helped us find questions to ask. It has not tested anyone’s live policy.

Before relying on a real setup, I would compare an export of the active policy with the reviewed rules, including their order, inherited permissions and exceptions. Then I would run the positive and negative cases through the custody product’s supported test or sandbox workflow with the actual wallet identities, contract addresses and encoded transactions. I would record the decision and the rule that produced it. There is no need to broadcast an unwanted payment to test a restriction.

I would also return to the execution paths we mapped earlier. Can the trader sign directly, use a session key or reach the wallet through a module or recovery route? Safe modules, for example, can execute transactions using their own validation logic. Each path needs to enforce the trading restriction or have a separately reviewed authorization the trader cannot bypass.

If the active policy rejects the forbidden requests, independent approvals work, other execution paths are accounted for and a fresh simulation matches the intended funds flow, I would be comfortable approving this narrow swap permission. An unexplained allow, a bypass or missing test results would keep signing on hold. If the policy or signing paths change later, I would review them again.

Try the tests yourself

I put together 15 cases based on this example, including the intended swap, the wrong receiver, rule order, allowances and approvals. Each case includes the inputs, expected decision, reason and checks still needed.

Download the test cases as JSON or get the readable version. Replace the aliases with identities verified in your custody system, then record the actual result and the rule that decided it. The files use receiver for the router’s recipient argument. Use these fictional cases to build a test plan. Establishing how an active policy works requires tests in that account.

If your team would like help reviewing its wallet permissions, let’s discuss a wallet-policy review through my fractional security advisory service.