anchornet-contracts Soroban smart contracts for AnchorNet — the liquidity coordination network for Stellar anchors. This repo contains on-chain logic for liquidity pools, routing metadata, and settlement hooks.
Overview Stack: Rust, Soroban SDK Network: Stellar (Soroban) Prerequisites Rust (stable, with rustfmt) Optional: Soroban CLI for deployment and local testing Setup Bash
Clone the repo (or use your fork) git clone cd anchornet-contracts
Install Rust if needed curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Check formatting, build, and test cargo fmt --all -- --check cargo build cargo test
text
src/lib.rs– contract entrypoint and public interfacesrc/error.rs– error codes returned to clientsdocs/ADMIN.md– privileged admin/operator roles, lifecycle, and security propertiesdocs/ERRORS.md– stable error-code reference and originating entrypointsdocs/PAGINATION.md– stable pagination semantics reference and worked examplesdocs/EVENTS.md– event topics, argument types, and indexer integration guidesrc/types.rs– on-chain data types (Pool)src/storage.rs– storage keys and TTL-aware accessorssrc/events.rs– event publishing helperssrc/test.rs– unit testsCargo.toml– dependencies and crate config
The AnchornetContract tracks per-asset liquidity pools funded by registered
anchors. The off-chain indexer subscribes to the emitted events to mirror pool
state.
| Function | Auth | Description |
|---|---|---|
initialize(admin) |
once | Set the contract administrator |
admin() |
– | Read the current administrator |
set_admin(new_admin) |
admin | Transfer administration in a single step |
propose_admin(candidate) |
admin | Propose candidate as the next administrator |
accept_admin(candidate) |
candidate | Accept a pending admin transfer |
pending_admin() |
– | Read the proposed next administrator, if any |
register_anchor(anchor) |
admin | Approve an anchor as a liquidity provider |
register_anchors(anchors) |
admin | Approve a batch of anchors atomically in one call |
is_anchor(anchor) |
– | Check whether an address is registered |
anchor_status(anchor) |
– | Read the registration status (NeverRegistered, Active, or Deregistered) of an address |
list_anchors(start, limit) |
– | Page through currently registered anchors |
anchor_count() |
– | Read the number of currently registered anchors |
provide_liquidity(provider, asset, amount) |
provider | Add liquidity to a pool |
provide_liquidity_multi(provider, requests) |
provider | Add liquidity to several assets in one call and authorization; validates the whole batch (no duplicate assets) before applying any of i[...] |
withdraw_liquidity(provider, asset, amount) |
provider | Remove liquidity from a pool |
withdraw_all_liquidity(provider, asset) |
provider | Withdraw a provider's entire balance in one call |
withdraw_liquidity_multi(provider, requests) |
provider | Withdraw from several assets in one call and authorization; validates the whole batch (no duplicate assets) before applying any of it [...] |
deregister_anchor(anchor) |
admin | Remove an anchor from the approved set |
pool(asset) |
– | Read aggregate pool state |
pool_exists(asset) |
– | Check whether a pool entry exists for an asset (i.e. liquidity has ever been provided for it) |
total_liquidity(asset) |
– | Read total liquidity for an asset |
total_liquidity_all() |
– | Read the sum of total liquidity across every asset ever funded |
balance(provider, asset) |
– | Read a provider's balance |
anchor_balances(provider, start, limit) |
– | Page through a provider's non-zero balances across every known asset |
list_assets(start, limit) |
– | Page through every asset that has ever had liquidity provided |
asset_count() |
– | Read the number of distinct assets that have ever had liquidity provided |
set_min_liquidity(asset, floor) |
admin | Set the minimum liquidity floor an asset's pool may not be withdrawn below (0 disables) |
min_liquidity(asset) |
– | Read the minimum liquidity floor configured for an asset |
set_max_settlement_amount(asset, amount) |
admin | Cap the amount a single settlement may reserve for an asset (0 disables) |
max_settlement_amount(asset) |
– | Read the maximum settlement amount configured for an asset |
| Function | Auth | Description |
|---|---|---|
pause(caller) / unpause(caller) |
admin or operator | Halt or resume liquidity & settlement mutations |
set_operator(operator) |
admin | Appoint an operator that may pause/unpause but cannot change fees or admin |
clear_operator() |
admin | Revoke the operator role entirely |
operator() |
– | Read the currently appointed operator |
is_operator(address) |
– | Check whether an address is the currently appointed operator |
extend_instance_ttl(caller) |
admin or operator | Extend the contract instance/code TTL so it survives long inactivity |
Note:
extend_instance_ttlonly refreshes the instance storage bucket. Persistent entries (e.g.,Anchor,Pool,Balance, etc.) have independent TTLs managed by per‑keyextendc[...]
| set_fee(bps) | admin | Set the protocol fee in basis points (max 1000) |
| fee() / quote_fee(asset, amount) | – | Read the global fee rate / preview the effective fee for an asset |
| max_fee_bps() | – | Read the maximum fee set_fee/set_asset_fee will accept |
| set_asset_fee(asset, bps) | admin | Override the protocol fee for one asset, independent of the global rate |
| clear_asset_fee(asset) | admin | Remove an asset's fee override, reverting it to the global rate |
| asset_fee(asset) | – | Read the effective fee for an asset (its override, or the global rate) |
| has_asset_fee_override(asset) | – | Read whether an explicit fee override is configured for asset, distinguishing an admin-set 0 bps override from the absence of any override |
| collect_fees(asset) | admin | Collect accrued protocol fees for an asset |
| fees_accrued(asset) | – | Read uncollected fees for an asset |
| total_fees_accrued() | – | Read the sum of uncollected fees across every asset ever funded |
| set_fee_waiver(anchor, waived) | admin | Grant or revoke a fee waiver for a registered anchor |
| is_fee_waived(anchor) | – | Check whether an anchor is exempt from settlement fees |
| list_fee_waived_anchors(start, limit) | – | Page through currently registered anchors with an active fee waiver |
| fee_waived_anchor_count() | – | Read the number of currently registered anchors with an active fee waiver |
| version() | – | Read the contract interface version |
Fee calculations intentionally use floor division: floor(amount * bps / 10_000). As a result, tiny settlements can have a zero fee even when the configured rate is nonzero. For example, at 1 bps, amounts below 10,000 quote and accrue a fee of 0, while an amount of 10,000 produces a fee of 1. This rounding behavior is an accepted protocol tradeoff.
asset_fee(asset) collapses the per-asset fee override into the effective rate (Option<u32> → u32), so it cannot distinguish an explicit 0 bps override (set_asset_fee(asset, 0)) from the absence of any override when the global fee is also 0. has_asset_fee_override(asset) resolves this ambiguity by exposing whether the override entry exists in storage (Some(_)), independent of its value.
Settlement Function Auth Description open_settlement(anchor, asset, amount) anchor Reserve pool liquidity, returns a settlement id execute_settlement(id) admin Finalize a settlement and accrue its fee cancel_settlement(id) anchor Cancel and return reserved liquidity to the pool cancel_expired_settlement(id) – Reclaim a timed-out pending settlement's liquidity to the pool set_settlement_expiry_ledgers(ledgers) admin Set the ledger window after which a pending settlement may be reclaimed (0 disables) settlement_expiry_ledgers() – Read the settlement expiry window in ledgers settlement_exists(id) – Check whether a settlement exists settlement_status(id) – Read only the status of a settlement, or SettlementNotFound if missing is_settlement_pending(id) – Check whether a settlement exists and its status is Pending is_settlement_expired(id) – Check whether a pending settlement has passed the expiry window, without reclaiming it settlement(id) – Read a settlement record settlement_count() – Read the number of settlements list_settlements(start, limit) – Page through settlements list_settlements_by_anchor(anchor, start, limit) – Page through settlements opened by one anchor list_settlements_by_asset(asset, start, limit) – Page through settlements in one asset list_settlements_by_anchor_and_asset(anchor, asset, start, limit) – Page through settlements matching both anchor and asset list_settlements_by_status(status, start, limit) – Page through settlements in a given lifecycle state settlement_count_by_status(status) – Count every settlement in a given lifecycle state (no pagination) total_settled_amount(status) – Sum settled amount across every settlement in a given lifecycle state contract_info() – One-call snapshot of version, paused flag, fee, and anchor/asset/settlement counts Settlement lifecycle (state machine) SettlementStatus has four variants: Pending, Executed, Cancelled, Expired. Only the three one-way transitions shown below are valid. All three destination states are terminal — no further transition is possible from Executed, Cancelled, or Expired, and any attempt to transition from them will be rejected with InvalidSettlementState.
From To Function Authorization Condition Pending Executed execute_settlement(id) admin only Settlement must exist and be Pending Pending Cancelled cancel_settlement(id) settlement's anchor (auth required) Settlement must exist and be Pending Pending Expired cancel_expired_settlement(id) permissionless (any caller) settlement_expiry_ledgers > 0 and ledger >= opened_at + expiry mermaid
stateDiagram-v2 [] --> Pending : open_settlement(anchor, asset, amount) [anchor auth] Pending --> Executed : execute_settlement(id) [admin only] Pending --> Cancelled : cancel_settlement(id) [anchor auth] Pending --> Expired : cancel_expired_settlement(id) [permissionless, after expiry window] Executed --> [] : terminal (no exit) Cancelled --> [] : terminal (no exit) Expired --> [] : terminal (no exit) Terminal-state finality: Executed, Cancelled, and Expired are mutually exclusive and final. The contract enforces this by rejecting any transition call (execute_settlement, cancel_settlement, cancel_expired_settlement) on a settlement whose status is not exactly Pending (Error::InvalidSettlementState). The executable proof of this behavior is covered by the settlement lifecycle regression tests in src/test.rs (e.g. test_execute_cancelled_settlement_fails, test_execute_expired_settlement_fails, test_cancel_executed_fails, test_cancel_expired_settlement_rejects_already_executed, test_cancel_expired_settlement_rejects_before_expiry), which verify that already-terminal settlements cannot be re-transitioned.
cancel_expired_settlement requires no authorization: it only ever returns liquidity to the shared pool it was reserved from, never to an external party, so anyone (including an off-chain keeper) may call it once a pending settlement has passed the configured expiry window.
pause and unpause take an explicit caller argument (Soroban contracts have no implicit sender) that must be either the admin or the appointed operator; the operator role is scoped to this one lifecycle switch and carries no ability to change the fee, the admin, or any other admin-only setting. Note that appointing the admin as its own operator is a supported (if redundant) dual-role configuration.
Operator permission boundary The table below lists every gated entrypoint and which guard function it calls in src/lib.rs, so integrators and delegates can verify the boundary without reading individual doc comments.
require_admin_or_operator — admin or operator may call
Entrypoint Description pause(caller) Halt liquidity & settlement mutations unpause(caller) Resume after a pause extend_instance_ttl(caller) Extend contract instance/code TTL require_admin — admin only (operator excluded)
Entrypoint Description set_admin(new_admin) Transfer administration (single-step) propose_admin(candidate) Initiate a two-step admin transfer set_operator(operator) Appoint or replace the operator register_anchor(anchor) Approve a new liquidity provider register_anchors(anchors) Batch-approve liquidity providers deregister_anchor(anchor) Remove an anchor from the approved set set_fee(bps) Set the global protocol fee set_asset_fee(asset, bps) Override the fee for one asset clear_asset_fee(asset) Remove an asset's fee override set_fee_waiver(anchor, waived) Grant or revoke a fee waiver collect_fees(asset) Collect accrued protocol fees set_min_liquidity(asset, floor) Set the minimum liquidity floor set_max_settlement_amount(asset, amount) Cap per-settlement reserve size set_settlement_expiry_ledgers(ledgers) Set the settlement expiry window execute_settlement(id) Finalize a pending settlement Note: The three-entry require_admin_or_operator list and the fifteen-entry require_admin list are derived directly from the corresponding call sites in src/lib.rs. When a new entrypoint is added, check which guard it calls and update this table accordingly.
| Entrypoint | Description |
|---|---|
set_admin(new_admin) |
Transfer administration (single-step) |
propose_admin(candidate) |
Initiate a two-step admin transfer |
set_operator(operator) |
Appoint or replace the operator |
register_anchor(anchor) |
Approve a new liquidity provider |
register_anchors(anchors) |
Batch-approve liquidity providers |
deregister_anchor(anchor) |
Remove an anchor from the approved set |
set_fee(bps) |
Set the global protocol fee |
set_asset_fee(asset, bps) |
Override the fee for one asset |
clear_asset_fee(asset) |
Remove an asset's fee override |
set_fee_waiver(anchor, waived) |
Grant or revoke a fee waiver |
collect_fees(asset) |
Collect accrued protocol fees |
set_min_liquidity(asset, floor) |
Set the minimum liquidity floor |
set_max_settlement_amount(asset, amount) |
Cap per-settlement reserve size |
set_settlement_expiry_ledgers(ledgers) |
Set the settlement expiry window |
execute_settlement(id) |
Finalize a pending settlement |
Note: The three-entry
require_admin_or_operatorlist and the fifteen-entryrequire_adminlist are derived directly from the corresponding call sites insrc/lib.rs. When a new entrypoint is added, check which guard it calls and update this table accordingly.
For detailed event documentation, including argument shapes, emission sites, and indexer integration guidance, see docs/EVENTS.md.
Event topics at a glance:
("init",)– contract initialized("admin",)– administrator changed (viaset_adminoraccept_admin)("propose",)– admin transfer proposed("anchor", anchor)/("deanchor", anchor)– anchor registered / removed("provide", provider, asset)– liquidity provided("onboarded", asset)– first liquidity provision for a new asset("withdraw", provider, asset)– liquidity withdrawn("paused",)– paused flag flipped (data:bool)("fee",)– fee rate changed (data:u32bps)("waiver", anchor)– anchor fee waiver granted or revoked("settle", anchor, asset)– settlement opened("executed", id)/("cancelled", id)– settlement finalized / cancelled("expired", id)– settlement reclaimed after timing out("expiry",)– settlement expiry window changed("collect", asset)– fees collected("minliq", asset)– minimum liquidity floor configured("maxamt", asset)– maximum settlement amount configured("assetfee", asset)– asset-specific fee override set (data:u32bps)("feeclear", asset)– asset-specific fee override cleared("operator",)– operator appointed or replaced("op_clear",)– operator role revoked
The compiled wasm embeds Name and Description entries (via
contractmeta!) so tooling that inspects the deployed contract can identify
it without an off-chain registry.
| Command | Description |
|---|---|
cargo build |
Build the contract |
cargo test |
Run unit tests |
cargo fmt --all |
Format code |
cargo fmt --all -- --check |
Check formatting (CI) |
- Fork the repo and create a branch from
main. - Make changes; keep formatting with
cargo fmt --all. - If modifying public contract functions, parameters, return types, data structures, error codes, or events, review and complete the
Public API Compatibility Checklist. - Run
cargo fmt --all -- --check,cargo build, andcargo test. - Open a pull request. CI will run format check, build, and tests.
MIT