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.
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_sharesonly changes when minting or burning.underlyingis 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.tswraps Soroban RPC. Reads simulate transactions and decode return values; writes build and submit signed transactions.frontend/lib/wallet.tsis the Creit Tech wallet kit glue (connect + sign).frontend/app/page.tsxorchestrates 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.


