Skip to content

feat(token): Integrate Stellar Asset Contract (SAC) Standard for Yield-Bearing Pool Shares#181

Open
senmalong wants to merge 1 commit into
BETAIL-BOYS:mainfrom
senmalong:feat/issue-171-sac-yield-bearing-lp-tokens
Open

feat(token): Integrate Stellar Asset Contract (SAC) Standard for Yield-Bearing Pool Shares#181
senmalong wants to merge 1 commit into
BETAIL-BOYS:mainfrom
senmalong:feat/issue-171-sac-yield-bearing-lp-tokens

Conversation

@senmalong

Copy link
Copy Markdown

Summary

Closes #171

Implements the Stellar Asset Contract (SAC) standard for yield-bearing LP share tokens in the TradeFlow factoring pools, as specified in issue #171.


What was built

New contract: contracts/liquidity_vault

A standalone Soroban vault contract (LiquidityVault) that LPs interact with instead of depositing into a raw pool. When a user deposits USDC they receive tfUSDC — a fully transferable, yield-bearing share token that is indistinguishable from any other SAC token on Stellar.


SAC Token Interface (acceptance criterion 1)

All standard Soroban token read/write functions are implemented:

Function Type
balance(id) read
total_supply() read
decimals() read
name() read
symbol() read
transfer(from, to, amount) write
transfer_from(spender, from, to, amount) write
approve(owner, spender, amount, expiration_ledger) write
allowance(owner, spender) read

Minting & Burning Logic (acceptance criterion 2)

deposit(from, assets, min_shares) -> shares

On first deposit: shares = assets (1:1 before inflation lock).
On subsequent deposits: shares = (assets * total_shares) / total_assets

Transfers underlying from caller to vault, mints shares at the current exchange rate. min_shares is a slippage guard.

withdraw(from, shares, min_assets_out) -> assets

assets = (shares * total_assets) / total_shares

Burns caller's shares (checks-effects-interactions), releases proportional underlying. min_assets_out is a slippage guard.

As the pool earns revenue (factoring discounts, loan interest, flash-loan fees) total_assets grows while total_shares stays the same — every share is now worth more underlying.


Inflation-Attack Protection (acceptance criterion 3)

On the very first deposit MINIMUM_LIQUIDITY = 1_000 shares are permanently locked by incrementing total_shares without issuing any balance entry. These shares can never be redeemed.

This is the same technique used by Uniswap V2 and recommended in ERC-4626 audit literature:

  • Forces the share-price denominator to always be >= 1_000.
  • Makes the classic "donate before first depositor" attack economically infeasible.
  • First deposit must exceed MINIMUM_LIQUIDITY; otherwise the transaction reverts.

Storage design

Key Storage tier Purpose
VaultState Instance token metadata + total_shares accounting
Admin Instance admin address
Paused Instance circuit-breaker flag
Frozen(Address) Instance per-address compliance freeze
Balance(Address) Persistent LP token balances
Allowance(owner, spender) Persistent spending allowances
AllowanceLedger(owner, spender) Persistent allowance expiry ledger

All persistent entries extend their own TTL on every access (~30 days / 535_680 ledgers), matching the pattern used across the existing contracts.


Tests (12 unit tests)

  • Basic deposit / withdraw round-trip
  • Inflation-attack mitigation scenario
  • Yield accrual (share value rises as pool earns)
  • Multi-depositor proportional withdrawal math
  • transfer, transfer_from, approve / allowance
  • Pause and freeze admin controls
  • Slippage guards on deposit and withdraw
  • First-deposit-below-minimum-liquidity guard

Contract compiles to wasm32v1-none with zero errors.


Checklist

  • SAC token interface fully implemented
  • deposit mints shares at correct exchange rate
  • withdraw burns shares and releases proportional assets
  • Inflation-attack mitigation via MINIMUM_LIQUIDITY lock
  • Slippage guards on both deposit and withdraw
  • Admin pause / per-address compliance freeze
  • All storage entries have TTL management
  • preview_deposit / preview_redeem read helpers for frontends
  • Compiles to wasm32v1-none with no errors

Implements issue BETAIL-BOYS#171 — Stellar Asset Contract (SAC) standard for
yield-bearing pool shares in the TradeFlow factoring pools.

## What changed

Added contracts/liquidity_vault — a new Soroban contract that acts as
the yield-bearing vault for liquidity providers. When an LP deposits
USDC (or any SAC-compatible token) they receive tfUSDC shares in return.
As the pool earns revenue (factoring discounts, interest, flash-loan
fees) the underlying value of each share grows, so LPs automatically
accrue yield without any extra claim transaction.

## SAC Token Interface

The vault implements the full standard Soroban token interface:

  balance, total_supply, decimals, name, symbol  (read)
  transfer, transfer_from, approve, allowance    (write)

This makes tfUSDC indistinguishable from any other SAC token, so any
wallet, DEX aggregator or lending protocol that understands the Stellar
Asset Contract standard can interact with it out of the box.

## Vault / ERC-4626 mechanics

  deposit(from, assets, min_shares) -> shares
    Pulls underlying from the caller, mints shares at the current
    exchange rate (assets * total_shares / total_assets). Returns
    shares minted. min_shares is a slippage guard.

  withdraw(from, shares, min_assets_out) -> assets
    Burns the caller's shares and releases proportional underlying.
    min_assets_out is a slippage guard.

  preview_deposit / preview_redeem
    Read-only helpers so frontends can quote before submitting.

  total_assets   — physical SAC balance held by the vault
  total_supply   — shares outstanding

## Inflation-attack mitigation

On the very first deposit MINIMUM_LIQUIDITY (1 000) shares are
permanently locked by crediting them to total_shares without issuing a
balance entry. This is the standard ERC-4626 / Uniswap V2 technique:
the locked shares force the share-price denominator to always be >= 1 000,
making the classic first-depositor rounding attack economically
infeasible regardless of how much an attacker donates to the vault.

## Admin & compliance

  set_paused(bool)          — emergency circuit breaker
  set_frozen(address, bool) — per-address compliance freeze (mirrors
                              the freeze pattern already in amm_pool)
  is_paused / is_frozen     — read helpers

Closes BETAIL-BOYS#171
@AlAfiz

AlAfiz commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

@senmalong could you please check back and fix the rust CI build that failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(token): Integrate Stellar Asset Contract (SAC) Standard for Yield-Bearing Pool Shares

2 participants