The admin-controlled token allowlist prevents malicious or worthless tokens from being deposited into vaults. Only tokens explicitly added to the allowlist can be used in multi-asset vault operations.
Before this feature:
- Any token contract address could be deposited into vaults
- Malicious tokens could drain vault funds
- Worthless or test tokens could be added accidentally
- No control over asset quality
Admin maintains a persistent list of approved tokens. All token deposits must pass allowlist validation.
enum DataKey {
// ...
AllowedTokens, // Issue #1118: admin-controlled token allowlist
}pub const TOKEN_ALLOWLIST_ADDED_TOPIC: Symbol = symbol_short!("tok_add");
pub const TOKEN_ALLOWLIST_REMOVED_TOPIC: Symbol = symbol_short!("tok_rem");pub enum ContractError {
// ...
TokenNotAllowed = 95, // Issue #1118: Token not in allowlist
}Purpose: Add a token to the allowlist
Authorization: Admin-only
Parameters:
env: Soroban environmenttoken: Address of token contract to allow
Returns: Result<(), ContractError>
Behavior:
- Verify caller is admin (require_auth)
- Load current allowlist (empty Vec if none)
- Check if token already exists (no duplicates)
- Append token to allowlist
- Store updated list in persistent storage
- Emit TOKEN_ALLOWLIST_ADDED_TOPIC event
- Log audit entry
- Extend TTL on allowlist key
- Return Ok(())
Errors:
NotAdmin- Caller is not adminPaused- Contract is paused
Idempotency:
- Adding already-allowed token is a no-op (returns Ok immediately)
Purpose: Remove a token from the allowlist
Authorization: Admin-only
Parameters:
env: Soroban environmenttoken: Address of token contract to disallow
Returns: Result<(), ContractError>
Behavior:
- Verify caller is admin (require_auth)
- Load current allowlist
- Search for token in list
- If found, rebuild list without token
- Store updated list in persistent storage
- Emit TOKEN_ALLOWLIST_REMOVED_TOPIC event
- Log audit entry
- Extend TTL on allowlist key
- Return Ok(())
Errors:
NotAdmin- Caller is not adminPaused- Contract is paused
Idempotency:
- Removing non-existent token is a no-op (returns Ok)
Purpose: Retrieve complete allowlist
Authorization: Public
Parameters:
env: Soroban environment
Returns: Vec
Behavior:
- Load allowlist from persistent storage
- Return empty Vec if none exist
- Return complete list otherwise
Purpose: Check if single token is allowed
Authorization: Public
Parameters:
env: Soroban environmenttoken: Address to check
Returns: bool - true if allowed, false otherwise
Behavior:
- Load allowlist from persistent storage
- Iterate through tokens
- Return true on first match
- Return false if no match found
When implementing token deposits, validation must be added:
pub fn deposit_token(
env: Env,
vault_id: u64,
caller: Address,
token: Address,
amount: i128,
) -> Result<(), ContractError> {
// ... existing validation ...
// Check token is allowed
if !Self::is_token_allowed(&env, &token) {
return Err(ContractError::TokenNotAllowed);
}
// ... rest of deposit logic ...
}Data: (admin: Address, token: Address) When: Token successfully added to allowlist
Data: (admin: Address, token: Address) When: Token successfully removed from allowlist
All allowlist operations are logged with:
- Action: "add_allowed_token" | "remove_allowed_token"
- Caller: Admin address
- Timestamp: Ledger timestamp
- Token: Address added/removed
- Key: DataKey::AllowedTokens
- Value: Vec
- Type: Persistent (long-term storage)
- TTL Extension: VAULT_TTL_LEDGERS on each modification
- Typical allowlist size: 10-100 tokens
- Read: O(n) for is_token_allowed (acceptable for small lists)
- Write: O(n) for add/remove (acceptable frequency)
- Only admin can modify allowlist
- Prevents unauthorized token additions
- Clear authority chain
- Allowlist operations blocked during contract pause
- Prevents allowlist changes during security incidents
- Allows emergency halt of changes
add_allowed_tokenchecks for existing token- Prevents duplicate entries in list
- Maintains clean state
- Tokens cannot be modified once added
- Can only be removed (full control)
- No partial modifications
- All modifications logged
- Transparent history of allowlist changes
- Forensic capability
Admin calls: add_allowed_token(USDC_address)
Result: Users can now deposit USDC into vaults
Event: TOKEN_ALLOWLIST_ADDED_TOPIC emitted
Admin calls: remove_allowed_token(compromised_token_address)
Result: Compromised token can no longer be deposited
Existing vaults: Keep their holdings (not affected)
Event: TOKEN_ALLOWLIST_REMOVED_TOPIC emitted
User calls: get_allowed_tokens()
Result: Vec<Address> of all allowed tokens
Use: UI shows available deposit options
User calls: deposit_token(vault_id, token, amount)
Contract checks: is_token_allowed(token)?
If false: Returns TokenNotAllowed error
If true: Proceeds with deposit
- ✅ Add single token
- ✅ Add multiple tokens
- ✅ Add duplicate token (no-op)
- ✅ Remove existing token
- ✅ Remove non-existent token (no-op)
- ✅ Query empty allowlist
- ✅ Query non-empty allowlist
- ✅ Non-admin cannot add token (NotAdmin error)
- ✅ Non-admin cannot remove token (NotAdmin error)
- ✅ Public can query allowlist
- ✅ Add token when paused (Paused error)
- ✅ Remove token when paused (Paused error)
- ✅ Query allowlist when paused (allowed)
- ✅ Deposit allowed token (success)
- ✅ Deposit non-allowed token (TokenNotAllowed error)
- ✅ Remove token, then try deposit (TokenNotAllowed error)
- ✅ TOKEN_ALLOWLIST_ADDED_TOPIC emitted with correct data
- ✅ TOKEN_ALLOWLIST_REMOVED_TOPIC emitted with correct data
For existing deployments:
- Deploy contract with allowlist feature
- Allowlist starts empty (no tokens allowed)
- Admin adds whitelisted tokens over time
- Previous vault tokens remain usable (no retroactive enforcement)
- New deposits must use allowlisted tokens
Potential improvements (not in scope):
- Token metadata caching (name, decimals)
- Token tier system (premium, standard, test)
- Bulk add/remove operations
- Time-locked allowlist changes
- Multi-sig approval for allowlist changes