Skip to main content

Token Allowlist & Decimal Normalization

Last updated: 16 August 2026

Documents a contract that does not exist

The only Soroban contract in this project is grainhack-escrow (initialise, fund, publish_root, claim, is_claimed, sweep), and it has never been deployed. The program-escrow and bounty-escrow surfaces this page describes — publish_program, lock_program_funds, single_payout, batch_payout, create_bounty and the rest — have no implementation in any repository, verified 16 August 2026.

This page is kept as a design record. Do not read it as a description of working software, and do not implement against it without checking what the contract actually exposes. See Payout path — what exists today.

Overview​

The program-escrow contract maintains a token allowlist that restricts which token contract addresses may be used when initialising a program. When the list is non-empty, only explicitly permitted tokens are accepted; an empty list disables enforcement (any token is accepted).

Issue #1295 extends the allowlist to store each token's decimal precision alongside its address. This lets off-chain tooling display human-readable payout amounts without extra RPC calls and provides an auditable, admin-controlled record of each token's precision.


Storage Layout​

KeyTypeDescription
DataKey::TokenAllowlistVec<Address>Legacy V1 list (backward compat)
DataKey::TokenAllowlistV2Vec<AllowedTokenEntry>V2 list with decimals
DataKey::TokenDecimals(addr)u32Per-token decimal cache (O(1) lookup)
DataKey::TokenAllowlistSchemaVersionu32Upgrade-safety marker

AllowedTokenEntry​

pub struct AllowedTokenEntry {
pub token: Address, // token contract address
pub decimals: u32, // 0–18
}

Entrypoints​

Admin​

FunctionDescription
add_allowed_token_with_decimals(token, decimals)Add token with explicit decimal precision (preferred)
add_allowed_token(token)Add token with decimals = 0 (legacy, backward-compat)
remove_allowed_token(token)Remove token; clears decimal cache

View​

FunctionReturns
get_allowed_tokens()Vec<Address> — V1-compatible list
get_allowed_tokens_with_decimals()Vec<AllowedTokenEntry> — V2 list with decimals
get_token_decimals(token)u32 — stored decimals for a token (0 if not found)
is_token_allowed(token)bool
get_allowlist_schema_version()u32

Decimal Normalization​

All payout amount parameters (single_payout, batch_payout) are expressed in the token's own base units — the smallest indivisible unit of that token.

TokenDecimals1 human unit =
USDC61_000_000 base units
XLM710_000_000 base units
Custom181_000_000_000_000_000_000 base units

The contract does not re-scale amounts at payout time. Callers must supply amounts already denominated in the token's base units. The decimals field is stored for off-chain tooling only.

Security Assumptions​

  1. Admin controls decimals — only the contract admin can add/remove tokens and set their decimal values. A compromised admin key can set incorrect decimals, but this only affects off-chain display, not on-chain transfer amounts.
  2. Decimals are capped at 18 — add_allowed_token_with_decimals panics if decimals > 18 to prevent overflow in any future normalization arithmetic.
  3. Backward compatibility — tokens added via the legacy add_allowed_token path have decimals = 0. Off-chain tooling should treat 0 as "unknown" and query the token contract directly if needed.
  4. Deterministic enforcement — the allowlist check runs before any state mutation in init_program, so rejected tokens never produce partial writes.

Events​

TokenAllowlistUpdatedEvent​

Emitted on every add_allowed_token* and remove_allowed_token call.

topic: (TkAllow,)
payload: TokenAllowlistUpdatedEvent {
version: u32,
token: Address,
added: bool, // true = added, false = removed
updated_by: Address,
timestamp: u64,
decimals: u32, // stored decimals (0 on remove)
}

TokenRejectedEvent​

Emitted before panic when init_program is called with a non-allowlisted token.

topic: (TkReject,)
payload: TokenRejectedEvent { version, token, program_id, timestamp }

Example Usage​

# Add USDC (6 decimals) to the allowlist
stellar contract invoke --id $CONTRACT \
-- add_allowed_token_with_decimals \
--token $USDC_ADDRESS \
--decimals 6

# Add a custom 18-decimal token
stellar contract invoke --id $CONTRACT \
-- add_allowed_token_with_decimals \
--token $CUSTOM_TOKEN \
--decimals 18

# Query the V2 allowlist
stellar contract invoke --id $CONTRACT \
-- get_allowed_tokens_with_decimals

# Query decimals for a specific token
stellar contract invoke --id $CONTRACT \
-- get_token_decimals \
--token $USDC_ADDRESS