CONTRACT_WHITELIST Deep Dive: Lock Your Trading Bot to Pre-Approved DeFi Protocols
Your trading bot just executed a swap on an unverified contract — not because you intended it, but because a compromised dependency injected a malicious address into the call. CONTRACT_WHITELIST, one of WAIaaS's 21 policy types, exists precisely to prevent this scenario by enforcing a default-deny contract allowlist at the wallet infrastructure level, below your application code.
Why This Matters for Automated Trading
Automated trading systems operate at speed and scale. A human reviews a transaction before signing it; your bot does not. That asymmetry is the attack surface. Whether you're running an arbitrage loop, an MEV strategy, or a multi-leg DeFi position manager, every transaction your bot signs is a potential loss event if the target contract isn't what you think it is.
The standard mitigation is input validation in application code — check the contract address before you send. That works until it doesn't: dependency confusion attacks, configuration drift across environments, a bug in your address resolution logic, or a front-end injection if your bot reads state from an external source. What you want is a second enforcement layer that lives outside your application entirely, enforced at signing time, with no bypass path from within your bot's process.
CONTRACT_WHITELIST gives you that. The policy is evaluated inside WAIaaS's 7-stage transaction pipeline, at the policy stage, before the execute stage runs. If the target contract isn't on the list, the transaction never gets signed. Full stop.
How CONTRACT_WHITELIST Works
The policy is simple by design. You provide a list of contract addresses with optional labels and chain identifiers. Any ContractCall transaction targeting an address not on the list is denied. The error comes back as a structured response:
{
"error": {
"code": "POLICY_DENIED",
"message": "Transaction denied by CONTRACT_WHITELIST policy",
"domain": "POLICY",
"retryable": false
}
}
retryable: false is important for your bot's retry logic — don't loop on this. It's a hard policy rejection, not a transient RPC error.
Here's how you create the policy via the REST API:
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "CONTRACT_WHITELIST",
"rules": {
"contracts": [
{
"address": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
"name": "Jupiter Aggregator v6",
"chain": "solana"
}
]
}
}'
This uses masterAuth — the X-Master-Password header. Policy management is an administrative operation. Your bot never holds the master password; it operates with a session token that has no policy management authority. That separation matters.
Composing Policies for a Real Trading Bot
CONTRACT_WHITELIST alone isn't a complete risk posture for a trading bot. It prevents calls to unauthorized contracts, but it doesn't cap your exposure per trade, per hour, or per day. WAIaaS's policy engine evaluates all policies on a wallet, so you compose them.
A practical setup for a Solana-based arbitrage bot might look like this:
Policy 1: CONTRACT_WHITELIST — Only the protocols your strategy uses
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "CONTRACT_WHITELIST",
"rules": {
"contracts": [
{
"address": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
"name": "Jupiter Aggregator v6",
"chain": "solana"
},
{
"address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH",
"name": "Drift Protocol",
"chain": "solana"
}
]
}
}'
Policy 2: SPENDING_LIMIT — Cap the blast radius of any single bad trade or bug
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "SPENDING_LIMIT",
"rules": {
"instant_max_usd": 500,
"notify_max_usd": 2000,
"delay_max_usd": 10000,
"delay_seconds": 900,
"daily_limit_usd": 50000
}
}'
The 4-tier security model means transactions under $500 execute immediately (INSTANT), $500–$2,000 execute with a notification to the fund owner (NOTIFY), $2,000–$10,000 queue for 15 minutes before executing (DELAY), and anything above $10,000 requires explicit human approval (APPROVAL). For a bot that's running automated strategies, you're mostly operating in INSTANT and NOTIFY territory by design.
Policy 3: ALLOWED_TOKENS — Restrict which tokens the bot can move
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "ALLOWED_TOKENS",
"rules": {
"tokens": [
{
"address": "So11111111111111111111111111111111111111112",
"symbol": "SOL",
"chain": "solana"
},
{
"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"symbol": "USDC",
"chain": "solana"
}
]
}
}'
ALLOWED_TOKENS, like CONTRACT_WHITELIST, is default-deny. If your bot tries to acquire or transfer a token not on this list — say, because a compromised price feed routed it into a honeypot token — the transaction is rejected.
Policy 4: RATE_LIMIT — Bound the transaction rate
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "RATE_LIMIT",
"rules": {
"maxTransactions": 60,
"period": "hourly"
}
}'
A runaway loop in your bot code is a real failure mode. Rate limiting at the wallet layer caps the damage.
Executing DeFi Actions Through the Policy Layer
Once your policies are in place, your bot executes against the protocols it's been authorized for. Here's a Jupiter swap from the TypeScript SDK:
import { WAIaaSClient } from '@waiaas/sdk';
const client = new WAIaaSClient({
baseUrl: 'http://127.0.0.1:3100',
sessionToken: process.env.WAIAAS_SESSION_TOKEN,
});
// This will be rejected by CONTRACT_WHITELIST if Jupiter isn't on your list.
// It will be rejected by ALLOWED_TOKENS if SOL or USDC aren't whitelisted.
// If both policies allow it, the SPENDING_LIMIT tier determines execution path.
const tx = await client.sendToken({
type: 'TRANSFER',
to: 'recipient-address',
amount: '0.1',
});
Or directly via the REST API for a DeFi action against an integrated protocol:
curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "1000000000"
}'
WAIaaS integrates 15 DeFi protocol providers natively, including Jupiter (Solana swaps), Drift (perpetual futures and spot), Hyperliquid (perps and spot with sub-accounts), LI.FI and Across (cross-chain bridging), Aave v3, Kamino, and others. For protocols with their own risk dimensions — leverage, position sizing — there are dedicated policy types: PERP_MAX_LEVERAGE, PERP_MAX_POSITION_USD, PERP_ALLOWED_MARKETS, LENDING_LTV_LIMIT.
Dry-Run Before You Commit
For a new strategy or a new protocol addition, run a simulation first. The dryRun flag sends the transaction through the full pipeline — including policy evaluation — without executing on-chain:
curl -X POST http://127.0.0.1:3100/v1/transactions/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"type": "TRANSFER",
"to": "recipient-address",
"amount": "0.1",
"dryRun": true
}'
If your CONTRACT_WHITELIST doesn't include the target, the dry-run will return a POLICY_DENIED error — before you've committed gas or capital. Add the contract to the policy, re-run the dry-run, confirm it passes, then go live. This belongs in your deployment checklist every time you add a new protocol to a bot's strategy.
Gas Conditional Execution
Trading bots care about gas costs, especially on EVM chains where gas price variance directly affects PnL on thin-margin arb trades. WAIaaS includes gas conditional execution: transactions are queued and only execute when the current gas price meets a threshold you define. This is handled at the pipeline level, in the wait stage, before execution. Your bot submits the transaction; the pipeline watches gas and fires when conditions are met, without your bot needing to poll or implement its own gas oracle logic.
Quick Start: Setting Up a Policy-Controlled Trading Wallet
Step 1: Install the CLI and start the daemon
npm install -g @waiaas/cli
waiaas init --auto-provision
waiaas start
Step 2: Create a wallet and a session token for your bot
curl -X POST http://127.0.0.1:3100/v1/wallets \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{"name": "arb-bot", "chain": "solana", "environment": "mainnet"}'
curl -X POST http://127.0.0.1:3100/v1/sessions \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{"walletId": "<wallet-uuid>"}'
Step 3: Apply your CONTRACT_WHITELIST policy
Use the policy creation call from the section above, adding every contract address your strategy legitimately calls.
Step 4: Apply ALLOWED_TOKENS, SPENDING_LIMIT, and RATE_LIMIT policies
Compose the full policy set before you start the bot. The wallet is default-deny on contract calls and token transfers until these policies are in place.
Step 5: Run a dry-run to verify the policy stack
curl -X POST http://127.0.0.1:3100/v1/transactions/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"type": "TRANSFER",
"to": "recipient-address",
"amount": "0.1",
"dryRun": true
}'
Confirm the dry-run passes. If it returns POLICY_DENIED, your whitelist isn't complete — fix it before going live.
The Architecture Guarantee
The policies aren't advisory. They're enforced in the 7-stage transaction pipeline before signing. Your bot's session token has no ability to modify policies — that requires the master password, which your bot never holds. Even if your bot's process is fully compromised, the worst an attacker can do is what the policy stack allows: contracts on the whitelist, tokens on the allowed list, within the spending limits, within the rate limits. That's a bounded loss, not an unlimited one.
The 3-layer security model — session auth for the bot, policy evaluation in the pipeline, and owner auth for approvals above the DELAY threshold — means your human-on-the-loop (you, the operator) gets notified or required for anything outside the bot's normal operating envelope.
What's Next
The full list of 21 policy types is documented in the interactive API reference at http://127.0.0.1:3100/reference once your daemon is running — the OpenAPI 3.0 spec is auto-generated and covers every policy schema with request/response examples. For multi-protocol strategies spanning Solana and EVM chains, WAIaaS supports 2 chain types across 18 networks, so your CONTRACT_WHITELIST can include addresses on both with per-chain scoping.
If you're building an MCP-connected trading agent alongside your bot — for the cases where you want Claude or another LLM to make discretionary trades within a policy envelope — the same wallet, the same policies, and the same session token model apply. The 45 MCP tools include simulate-transaction, get-defi-positions, and get-health-factor, giving your LLM agent the same guardrails your automated bot runs under.
Explore the codebase and self-host:
- GitHub: https://github.com/waiaas/WAIaaS
- Official site: https://waiaas.ai
Top comments (0)