PROTOCOL DETAILS

MGUSD on Stellar

Technical overview of MGUSD, Stellar Asset Contract (SAC) stablecoin, administered via a Soroban smart contract.

MGUSD is an M0-powered Stellar Asset Contract (SAC) stablecoin. It is minted and burned via a Soroban smart contract (the "Minter Gateway"). The SAC holds balances and handles standard SEP-41 transfers; the minter gateway is the sole authority for supply changes, yield accrual, and compliance controls.

This document is for developers and integrators who want to understand how MGUSD is minted, distributed, redeemed, and how yield accrues. It assumes familiarity with Stellar's account model, trustlines, and the Stellar Asset Contract.


Architecture & Core Components

MGUSD is a two-layer system:

ComponentDescription
Minter Gateway (Soroban)The issuance control panel. Holds all roles, the yield index, and the supply accumulators. Every mint, burn, yield claim, rate change, and compliance action goes through it.
Stellar Asset Contract (SAC)The stablecoin token. Holds balances, enforces authorization (AUTH_REQUIRED), and exposes the standard SEP-41 transfer(). The Minter Gateway is the SAC admin and drives it via cross-contract calls.

The Minter Gateway never exposes a transfer() of its own — holders move tokens using the SAC's standard SEP-41 interface directly. The gateway's job is to be the single, authoritative gateway for everything that changes the supply or compliance state.

Actors

ActorResponsibility
BridgeThe primary operator — holds Admin, Minter, Yield Recipient Manager, and Forced Transfer Manager, and acts as a block / unblock operator and a pauser.
MoneyGramThe Yield Recipient (receives claimed yield) and the off-chain on/off-ramp for end users.
M0Co-holds the Pauser role.
PredicateCompliance partner — a block / unblock operator for onchain screening.

Lifecycle Flows

1. Minting (Bridge → Treasury)

  1. MoneyGram receives fiat from an end user and notifies the Bridge.
  2. Bridge calls mint(caller, treasury_address, amount) on the Minter Gateway.
  3. The gateway finalizes pending yield, increases total_principal and total_supply, then mints SAC tokens to the treasury address.
  4. The treasury now holds MGUSD stablecoin.

2. User Distribution (Treasury → End User)

  1. An unblock operator whitelists accounts — individually via unblock_user, or in batch via batch_unblock_users (up to 40 per call).
  2. The treasury transfers tokens to the user via the SAC's standard SEP-41 transfer().
  3. Whitelisted (unblocked) accounts can freely transfer among themselves.
  4. Non-whitelisted (blocked) accounts cannot send or receive tokens.

3. Redemption (End User → MoneyGram → Bridge)

  1. The end user initiates redemption through MoneyGram.
  2. Bridge calls burn(caller, user_address, amount) — removing tokens at the SAC layer via clawback.
  3. The Minter Gateway finalizes pending yield and decreases both accumulators.
  4. MoneyGram sends fiat to the end user off-chain.

4. Yield Claiming

  1. Bridge calls set_interest_rate(caller, rate_bps) to set the current interest rate (this is a Minter permission, not Admin).
  2. Yield accrues continuously on total_principal using the exponential index.
  3. The Yield Recipient Manager (Bridge) calls claim_yield(caller) to mint accrued yield as new SAC tokens to the Yield Recipient (MoneyGram).
  4. Claimed yield increases total_supply but not total_principal — it does not compound.

5. Forced Transfer (Compliance Action)

  1. The Forced Transfer Manager identifies a need to move tokens between accounts.
  2. It invokes force_transfer(caller, from, to, amount) — no authorization from the source account is required.
  3. The Minter Gateway claws back tokens from the source and mints them to the destination at the SAC layer.
  4. Accumulators are unchanged — this is a balance redistribution, not a supply change.
  5. Works even if the source account is blocked.

Roles

RolePermissionsHeld by
AdminRole administration only (see below)Bridge
Mintermint, burn, set_interest_rateBridge
Yield Recipient Managerset_yield_recipient, claim_yieldBridge
Yield RecipientPassive — receives tokens minted by claim_yieldMoneyGram
Forced Transfer Managerforce_transferBridge
Block operator (membership)block_user, batch_block_usersBridge, Predicate
Unblock operator (membership)unblock_user, batch_unblock_usersBridge, Predicate
Pauser (membership)pause, unpauseM0, Bridge
Admin is not a super-role. Admin's powers are limited to role administration: set_admin, set_minter, set_yield_recipient_manager, set_forced_transfer_manager, the block / unblock / pauser membership management functions, reconcile_burn, transfer_sac_admin, and upgrade. Admin cannot mint, burn, set the rate, block users, force-transfer, claim yield, or pause without first granting itself the relevant role. There is no implicit emergency fallback — operational procedures must ensure the dedicated role-signers remain reachable.

Every role-gated function calls require_auth() on its caller argument and verifies the caller equals the designated role holder — no implicit trust and no admin override. All roles are single-address except Block operator, Unblock operator, and Pauser, each of which is a membership set that Admin grants and revokes.


Contract Interface

Each role can only call its own functions. Admin is not a super-role and cannot call non-admin functions without first granting itself the relevant role.

Admin-exclusive functions

FunctionSignatureDescription
set_admin(new_admin: Address)Transfer the admin role to a new address
set_minter(new_minter: Address)Set a new minter address
set_yield_recipient_manager(new_yrm: Address)Set a new yield recipient manager
set_forced_transfer_manager(new_ftm: Address)Set a new forced transfer manager
add_block_operator / remove_block_operator(addr: Address)Grant / revoke block permission (membership set; idempotent)
add_unblock_operator / remove_unblock_operator(addr: Address)Grant / revoke unblock permission (membership set; idempotent)
add_pauser / remove_pauser(addr: Address)Grant / revoke pause permission (membership set; idempotent)
reconcile_burn(amount: i128)Decrease both accumulators to reconcile tokens destroyed outside the contract
transfer_sac_admin(new_sac_admin: Address)Transfer SAC admin role from the Minter Gateway to another address
upgrade(new_wasm_hash: BytesN<32>)Upgrade the contract WASM to a new version

Minter functions

FunctionSignatureDescription
mint(caller: Address, to: Address, amount: i128)Mint SAC tokens and increase both accumulators
burn(caller: Address, from: Address, amount: i128)Remove SAC tokens (clawback) and decrease both accumulators
set_interest_rate(caller: Address, rate_bps: u32)Set the interest rate in basis points (max 5000 = 50%)

Forced Transfer Manager functions

FunctionSignatureDescription
force_transfer(caller: Address, from: Address, to: Address, amount: i128)Force-move SAC tokens between accounts (clawback + mint)

Yield Recipient Manager functions

FunctionSignatureDescription
set_yield_recipient(caller: Address, new_yr: Address)Set the address that receives claimed yield
claim_yield(caller: Address) -> i128Claim accrued yield; mints new SAC tokens to the yield recipient

Block / unblock (allowlist) functions

block_user / batch_block_users require a block operator; unblock_user / batch_unblock_users require an unblock operator. Backed by the SAC allowlist.

FunctionSignatureDescription
block_user(user: Address, operator: Address)Block a user on the SAC (set_authorized(false))
unblock_user(user: Address, operator: Address)Unblock a user on the SAC (set_authorized(true))
batch_block_users(users: Vec<Address>, operator: Address)Block up to 40 users in a single transaction
batch_unblock_users(users: Vec<Address>, operator: Address)Unblock up to 40 users in a single transaction

Pauser functions

FunctionSignatureDescription
pause(caller: Address)Pause the contract — blocks mint, burn, claim_yield, set_interest_rate
unpause(caller: Address)Unpause the contract — resumes all blocked operations

View functions

admin, minter, yield_recipient_manager, yield_recipient, forced_transfer_manager, is_block_operator, is_unblock_operator, is_pauser, sac_token, interest_rate, current_index, latest_index, accrued_yield, total_principal, total_supply, blocked(account), balance(id), and paused.

Initialization

The contract is initialized via __constructor during deployment. It takes the SAC address plus eight role addresses (admin, minter, yield recipient manager, yield recipient, forced transfer manager, and one seed address each for the block operator, unblock operator, and pauser membership sets). The same address may be reused across arguments when one key should hold multiple permissions. The yield index starts at 1.0 (INDEX_SCALE) on first use.


Yield Mechanics

Continuous index model

Yield accrues continuously using an exponential index:

currentIndex = latestIndex × e^(rate × elapsed / SECONDS_PER_YEAR)

Where latestIndex is the last stored index (initialized to 1.0, scaled as 1e12), rate is the annual rate converted from basis points, elapsed is seconds since the last update, and SECONDS_PER_YEAR is 31,536,000. The e^x term uses a 4th-order Taylor series, accurate for all realistic rates.

Two accumulators

AccumulatorTracksModified by
total_principalYield-earning base (mints − burns)mint, burn, reconcile_burn
total_supplyAll outstanding tokens (principal + claimed yield)mint, burn, reconcile_burn, claim_yield

Only total_principal earns yield. The accrued amount is total_principal × (newIndex − oldIndex) / INDEX_SCALE, accumulated in accrued_yield on every index update.

Non-compounding

When claim_yield() is called, pending yield is finalized, accrued_yield is captured and reset to zero, total_supply increases by the claimed amount, and new SAC tokens are minted to the yield recipient. total_principal is unchanged — claimed yield does not earn more yield.

The index is updated before every state-changing operation (mint, burn, reconcile_burn, claim_yield, set_interest_rate). This guarantees yield is finalized at the correct principal and rate before any change takes effect.

Transfers & the Issuer Burn Problem

Holders move MGUSD using the SAC's standard SEP-41 transfer() — both sender and receiver must be authorized (unblocked) for a transfer to succeed. Transfers happen entirely at the SAC layer and do not change total_principal or total_supply; they are balance redistributions, not mints or burns.

On classic Stellar, sending tokens to the issuer address burns them automatically. If MGUSD is sent to the issuer, tokens are destroyed at the SAC layer but the Minter Gateway's accumulators are never updated — so yield would keep accruing on phantom principal.

Mitigation: AUTH_REQUIRED + whitelist

The SAC is configured with AUTH_REQUIRED, so all accounts start unauthorized (blocked) by default. Accounts can only transact after an unblock operator calls unblock_user(), which limits who can move tokens at all.

The issuer account is exempt from AUTH_REQUIRED at the Stellar protocol level. Authorized users can still send tokens directly to the issuer, destroying them without updating the accumulators. The whitelist reduces accidental issuer burns but does not eliminate them. If it happens, Admin calls reconcile_burn(amount) to decrease both accumulators and bring the Minter Gateway's bookkeeping back in line with the actual circulating supply.

Compliance & Safety

Block / unblock

The SAC operates in AUTH_REQUIRED mode — accounts are blocked by default. unblock_user maps to SAC set_authorized(true) and block_user to set_authorized(false). Only a block operator can block, and only an unblock operator can unblock. Batch operations (batch_block_users / batch_unblock_users) accept up to 40 users per call, are atomic (any failure reverts the whole transaction), and emit one event per user for indexer compatibility.

Pausable

The Minter Gateway implements a pause mechanism. When paused, the following revert immediately:

Blocked functionRole
mintMinter
burnMinter
set_interest_rateMinter
claim_yieldYield Recipient Manager

Compliance operations (block_user, unblock_user, their batch variants, and force_transfer) and all view functions remain fully accessible while paused, so regulatory actions can still be executed. reconcile_burn is also intentionally callable while paused — issuer-burn destruction happens outside Minter Gateway control and continues during a pause, so reconciliation must remain available to prevent unbounded accumulator divergence.


Events

Event names are the snake_case form of the underlying contract event struct. Topic fields are marked (topic); the rest are payload data.

EventEmitted byFields
admin_setset_adminold (topic), new
minter_setset_minterold (topic), new
yield_recipient_manager_setset_yield_recipient_managerold (topic), new
yield_recipient_setset_yield_recipientold (topic), new
forced_transfer_manager_setset_forced_transfer_managerold (topic), new
block_operator_added / block_operator_removedadd_block_operator / remove_block_operatoraddr (topic)
unblock_operator_added / unblock_operator_removedadd_unblock_operator / remove_unblock_operatoraddr (topic)
pauser_added / pauser_removedadd_pauser / remove_pauseraddr (topic)
interest_rate_setset_interest_raterate_bps
mintmintto (topic), amount, new_total_principal, new_total_supply
burnburnfrom (topic), amount, new_total_principal, new_total_supply
reconcilereconcile_burnamount, new_total_principal, new_total_supply
yield_claimedclaim_yieldrecipient (topic), amount
update_indexevery supply/rate mutationlatest_index
force_transferforce_transferfrom (topic), to (topic), amount
upgradedupgradeby (topic), new_wasm_hash
sac_admin_transferredtransfer_sac_adminnew_sac_admin (topic)
user_blocked / user_unblockedblock / unblock functionsuser (topic)
paused / unpausedpause / unpause(no fields)

Onchain Addresses

The MGUSD Minter Gateway and SAC addresses for Stellar Mainnet and Testnet can be found on the MGUSD Deployments page.


Source Code & Audits


Next Steps

  • Integrate MGUSD: Use the SAC's standard SEP-41 interface to hold and transfer MGUSD once your account is whitelisted.
  • Look up addresses: Find the Minter Gateway and SAC contract addresses on the MGUSD Deployments page.
Copyright © M0 Foundation 2026