Building a Share-Based Rebasing SEP-41 Token on Stellar

rebasing token

Swap USDC for rUSD once, then just hold it while the balance grows in your wallet. What could be easier? No staking screens, no lockups, no claim buttons.

This UX needs a rebasing token, so this repo is the smallest possible implementation of one a SEP41 rebasing token on Stellar, plus a tiny frontend to prove the flow end‑to‑end.

James On YouTube

This codebase ships three things that work together:

  • A Stellar smart contract that implements a SEP-41 token with share-based rebasing.
  • A set of unit tests that model the math and edge cases.
  • A Next.js frontend that connects via CreitTech Wallet kit v2 and exposes approve/mint/burn flows.

The key idea within the contract is shares, not balances.

Every account stores shares. The displayed rUSD balance is computed from the contract’s live USDC balance. If extra USDC arrives, the exchange rate increases and everyone’s displayed balance increases automatically.

How It Works

rUSD is treated like a vault share token:

  • total_shares only changes when minting or burning.
  • underlying is the USDC balance held by the contract address.
  • User balance is derived: balance = shares * underlying / total_shares.

There is no rebase function. The “rebase” happens because every balance() call re-reads the current USDC balance. This makes yield distribution permissionless: anyone can send USDC directly to the contract and everyone’s rUSD balances scale up pro‑rata.

The other important bit is rounding. Inputs (mint/transfer/burn amounts) are converted to shares with ceil to avoid under-collecting. Outputs (shares → rUSD or USDC out) use floor to avoid overpaying.


Code Walkthrough

Contract surface and overrides

The contract in contracts/src/lib.rs leans on stellar_tokens::fungible for SEP‑41 behavior and overrides the places where rebasing matters. I override balance, total_supply, transfer, and transfer_from so every user‑facing amount is interpreted as rebased rUSD, then converted into shares internally.

Here’s the core pattern (simplified to highlight intent):

impl stellar_tokens::fungible::ContractOverrides for RebasingOverrides {
    fn balance(e: &Env, account: &Address) -> i128 {
        let total_shares = read_total_shares(e);
        if total_shares == 0 { return 0; }
        let shares = read_shares(e, account);
        let underlying = read_underlying(e);
        rusd_from_shares(e, shares, total_shares, underlying)
    }

    fn transfer(e: &Env, from: &Address, to: &Address, amount: i128) {
        let total_shares = read_total_shares(e);
        let underlying = read_underlying(e);
        let shares_to_move = shares_from_rusd(e, amount, total_shares, underlying);
        // move shares and emit transfer in rebased units
    }
}

That override block is the heart of the rebasing behavior. Everything else is just careful bookkeeping.

Minting: exchange rate snapshot without storing it

mint in contracts/src/lib.rs pulls USDC first, then computes shares using the pre‑mint exchange rate. That’s why you see the subtle underlying_after - amount logic — it avoids “giving away” extra shares when the deposit itself changes the denominator.

pub fn mint(env: Env, to: Address, amount: i128) {
    usdc.transfer_from(&contract, &to, &contract, &amount);
    let total_shares = read_total_shares(&env);
    let underlying_after = read_underlying(&env);
    let shares_to_mint = if total_shares == 0 {
        amount
    } else {
        let underlying_before = underlying_after - amount;
        shares_from_rusd(&env, amount, total_shares, underlying_before)
    };
    // update shares + total_shares, emit mint
}

Burning does the inverse: convert rebased amount → shares, reduce share balances, compute USDC out with floor rounding, then transfer USDC back to the user.

Rounding utilities

The conversion helpers (shares_from_rusd and rusd_from_shares) are tiny but they drive the entire financial safety model. They’re all in contracts/src/lib.rs and worth reading straight through; the math is intentionally explicit and uses i128 with overflow checks.

Tests as executable math proofs

The tests in contracts/src/test.rs aren’t just “does it run.” I wrote them like a miniature math model:

  • A local USDC mock token controls balances.
  • Share math is re-implemented in test helpers so I can compare the contract’s results to the expected outcomes.
  • There’s a multi‑user scenario that exercises mint, transfer, burn, and a yield‑like deposit in one flow.

That last test (rebase_fair_share_after_activity_three_users) is the one I use to trust the model. If it passes, I feel confident that share conservation and proportional rebasing hold under messy activity.

Frontend Walkthrough

The frontend is intentionally simple: a single page.tsx plus a few helper modules.

  • frontend/lib/soroban.ts wraps Soroban RPC. Reads simulate transactions and decode return values; writes build and submit signed transactions.
  • frontend/lib/wallet.ts is the Creit Tech wallet kit glue (connect + sign).
  • frontend/app/page.tsx orchestrates the flow: connect, read balances, approve USDC, mint rUSD, burn rUSD.

The UI polls every ~12 seconds to catch rebases. It also computes and shows a soft exchange rate by dividing underlying / total_supply in the browser. That’s not the source of truth (the contract is), but it’s good enough to show the effect of yield inflows.

How I Run It Locally

I keep the flow simple and explicit. Here’s the exact setup I use.

You can clone the repository from here: https://github.com/jamesbachini/Rebasing-SEP41-Token/

or via the command line

git clone https://github.com/jamesbachini/Rebasing-SEP41-Token.git
1) Contract tests
cd contracts
cargo test
2) Deploy to testnet

The repository includes a deployment script that builds the WASM, deploys it, and runs init with the testnet USDC address:

./scripts/deploy_testnet.sh

You need the Stellar CLI (stellar or soroban) configured with a funded identity. Unless your name is james change this in the build script.

3) Frontend
cd frontend
npm install
npm run dev

From there it’s the familiar flow: approve USDC → mint rUSD → optionally send USDC directly to the contract address to simulate yield → watch balances rebase.


Takeaways

  • Share-based rebasing is simpler than it sounds when balances are derived and never stored.
  • Rounding rules are part of your security model, not a detail — this repo makes them explicit.
  • SEP‑41 can be adapted cleanly by overriding a small handful of functions rather than re‑implementing everything.
  • “Rebase” doesn’t need a method; it can be a consequence of your balance formula.
  • Tests can encode invariants, not just happy paths; the multi‑user test is essentially a small spec.
Where I’d Take It Next

This repo deliberately stops short of real yield. The contract just holds USDC, and “yield” is simulated by manually sending USDC to the contract. The natural next step is to wire the USDC into a vault (Blend or similar) and feed the returns back into the contract balance.

If I were productionizing it, I’d also add:

  • Events tailored for indexers (rebasing makes supply and balances time‑dependent).
  • An ERC‑4626‑style interface to make integrations more straightforward.
  • A stronger deployment and config story (less manual env shuffling).

The core idea holds up: share accounting gives you rebasing for free, and on Soroban that maps cleanly onto SEP‑41. Everything beyond that is product and risk management.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.


Posted

in

, , , , , ,

by