Account Demolisher
Security

Contract allow-list

The compiled-in set of contracts the app will sign an invocation against, and how it is enforced.

Every Soroban transaction is checked against a network-specific allow-list of contract ids immediately before signing. A transaction invoking a contract that is not on the list is refused and the closure halts.

The list is a compile-time constant in src/lib/config/contracts.ts. There is no environment variable, no fetch, and no runtime mutation path. A position discovered on chain cannot extend it. Only editing the source file changes it.

Contents

NetworkEntriesBreakdown
mainnet25Soroswap 2, Blend 15, Aquarius 1, FxDAO 7
testnet19Soroswap 2, Blend 9, Aquarius 2, FxDAO 6
futurenet0The allow-list is empty, so every invocation is refused

Blend's mainnet 15 is 4 pools plus 11 infrastructure contracts (two pool factories, two backstops, the emitter, BLND, USDC, the XLM contract, the Comet factory, the BLND/USDC LP, and the bootstrapper). Testnet's 9 is 1 pool plus 8 infrastructure contracts.

Entry shape

interface AllowedContract {
  readonly id: string; // C... contract id
  readonly name: string; // e.g. "SoroswapRouter", "BlendPool::FixedV2"
  readonly protocol: string; // "soroswap" | "blend" | "aquarius" | "fxdao"
  readonly verified_at: string; // ISO date the id was checked upstream
  readonly source: string; // the upstream citation
}

verified_at and source exist so every id is auditable against the protocol's own published deployment files rather than taken on trust.

When the check runs

assertTransactionAllowed is called from exactly one place: inside the Soroban sign-and- submit path, after any node-specific guard and immediately before the connector signs.

Running it at signing time rather than only at build time is deliberate defense in depth on top of each adapter's own build-time check.

Node kindAllow-list checked
ConvertSorobanToXLMYes
All remaining DeFi nodesYes
TransferAsIsNo
RevokeAllowanceNo
FinalClassicTx, MediatorForwardNot applicable, they never take this path

Why two kinds are exempt

Both target the user's own token contract, chosen from the user's own scan rather than from the DeFi allow-list, so the protocol allow-list does not apply to them.

They are not unguarded. Each is held to an exact call shape instead:

  • TransferAsIs is pinned to an exact transfer(user, destination, amount) by assertSafeTransferInvocation.
  • RevokeAllowance is pinned to an exact zero-approval by assertSafeRevokeInvocation.

The reasoning in source is explicit: a revoke's contract id and its simulation-supplied authorization tree are attacker-influenceable, so it must be guarded exactly like a transfer.

ConvertSorobanToXLM is different because it invokes the allow-listed router, so the gate does apply.

What gets refused

inspectOperation passes every classical operation and inspects every invokeHostFunction:

Host functionOutcome
invokeContract on an allow-listed C... addressAllowed
invokeContract on a non-allow-listed addressRefused, naming the network's list
invokeContract whose target is not a C... strkeyRefused
createContract, createContractV2Always refused
uploadContractWasmAlways refused
An unknown discriminantRefused
An unparseable discriminantRefused

Refusal reasons name the specific list, for example Contract is not on TESTNET_ALLOWLIST. On futurenet the reason states the list is empty.

Fee-bump wrappers are unwrapped transparently, so a fee-bump cannot hide an invocation from the check.

The authorization-entry guard

Operation-level inspection alone is not enough, because a Soroban authorization tree can authorize actions the top-level call does not name.

inspectAuthEntries runs first, before the host-function switch, and walks every authorization entry's invocation tree recursively. It refuses when any node in the tree names a function matching createContract or uploadContractWasm, case-insensitively.

An authorization entry that cannot be parsed is also refused, because an unparseable entry is suspicious and signing blind is worse than stopping.

A documented scope limit

Sub-invocations to token contracts that are not on the allow-list are intentionally not flagged. They are a normal part of any swap or withdrawal: a real exit authorizes token transfers on the account owner's own token contracts, which are not protocol contracts. Hard-gating them would break legitimate closes.

The error

A violation raises AllowlistViolation, carrying every violation rather than only the first, so the UI can display them all:

Allow-list violation: 2 contract invocation(s) blocked.
  1. C...: Contract is not on MAINNET_ALLOWLIST.
  2. <auth-entry>: A Soroban authorization entry authorizes contract creation or wasm upload.

A non-throwing variant returns the same list for display.

This is a deliberate hard stop. If closing a position needs an unknown contract, the safe move is to escalate, not to sign blindly.

On this page