Raum NetworkDeveloper Docs
RaumFi v2

RaumFi v2 Architecture

Architecture guide for RaumFi v2, covering the Soroban factory, pair, router, reserve accounting, LP token design, and deterministic pair addresses.

                     ┌────────────────────────────┐
                     │ Factory (raumfi-factory)   │
                     │ create_pair(a, b)          │
                     │ get_pair() / pair_exists() │
                     │ fee_to / fee_to_setter     │
                     │ fees_enabled               │
                     └────────────────────────────┘
                                   │ deploys via e.deployer()
                                   │ .with_current_contract(salt)
                                   ▼

                     ┌───────────────────────────────────────┐        ┌──────────────────────────────┐
                     │ Pair (raumfi-pair)                    │◀───────│ Router (raumfi-router)       │
                     │ deposit() / swap() / withdraw()       │        │ add_liquidity                │
                     │ skim() / sync()                       │        │ remove_liquidity             │
                     │ get_reserves() / k_multiplier()       │        │ swap_exact_tokens_for_tokens │
                     │ pair_token: SEP-41 LP token, embedded │        │ swap_tokens_for_exact_tokens │
                     └───────────────────────────────────────┘        └──────────────────────────────┘

Deterministic pair addresses

Pair addresses aren't stored and looked up on-chain by the router - they're computed locally. Both the factory (in create_pair) and the router (in router_pair_for / internal pair_for) derive the same salt from a SHA-256 hash of the two sorted token addresses, then call e.deployer().with_current_contract(salt) (factory, when deploying) or .with_address(factory, salt) (router, when only computing the address). This means the router can resolve a pair's address without a cross-contract call to the factory for every swap.

The LP token lives inside the pair contract

There is no separate LP token contract. raumfi-pair implements the full Soroban token::Interface directly (see pair_token/contract.rs) - transfer, approve, balance, burn, etc. all live on the pair contract itself, so the pair's own contract address is its LP token's address. internal_mint and internal_burn are called from deposit and withdraw respectively.

On a pair's first deposit, MINIMUM_LIQUIDITY (100 units) is minted to the pair contract's own address and permanently excluded from any single depositor's withdrawable share - the same anti-donation-attack pattern Uniswap V2 uses, adapted to Soroban's i128 arithmetic.

Protocol fee: a "last-k" multiplier, not per-swap minting

Unlike Uniswap V2's canonical implementation, protocol fees here aren't computed from kLast directly - the pair stores a multiplier (the product of reserves at the last fee-mint event) and calls into the factory's fees_enabled() / fee_to() on every deposit and withdraw via mint_fee(). If fees are enabled and the pool has grown since the last checkpoint, new LP shares are minted to the factory's fee_to address, diluting existing LPs by the accrued growth - the actual formula in mint_fee divides by root_k * 5 + root_multiplier, not the * 5 + 1 ratio from the original Uniswap V2 whitepaper, so the protocol's cut of growth is proportionally different.

Swap fee enforcement

The 0.3% fee isn't subtracted from the trader's output up front - swap() instead checks, after the requested amounts are sent out, that (balance_0 - fee_0) * (balance_1 - fee_1) >= reserve_0 * reserve_1, using ceiling division for the fee. If the constant-product invariant doesn't hold after accounting for the fee, the swap reverts with SwapConstantNotMet. This lets a caller send in either token, or both, in a single swap() call, as long as the post-fee invariant is satisfied.

Router: local math, no shared library crate

The router's quoting math (quote, get_amount_out, get_amount_in, and their chained get_amounts_out / get_amounts_in variants for multi-hop paths) lives in router/src/quotes.rs. It mirrors the pricing logic in the pair contract's swap() function but is a separate, independently-maintained implementation - there's no shared crate the two import from, so the two implementations need to be kept in sync by hand if the fee changes.