Skip to main content

What is an OFT on Stellar?

An Omnichain Fungible Token (OFT) on Stellar is a Soroban smart contract that enables crosschain token transfers via the LayerZero protocol. It extends the OApp contract with token handling logic for debiting (lock/burn) and crediting (unlock/mint) tokens. Stellar OFTs integrate with SEP-41 tokens — Stellar’s standard token interface (analogous to ERC-20 on EVM).

Classic Assets Receiving Requirements

These requirements apply when your OFT token is a SAC (wrapped classic asset). If you are using a custom contract token, trustlines are not required for any address type.

G-Address (EOA)

G-address recipients must meet two prerequisites before they can receive classic assets:
  1. Account activation: The account must hold a minimum of 1 XLM to exist on the Stellar network.
  2. Trustline: The account must have an explicit trustline for the classic asset being received.
If lz_receive fails due to unmet prerequisites, delivery can be retried once the recipient account is activated and the trustline is established.

C-Address (Smart Contract)

C-address recipients are not subject to these restrictions. As long as the contract address exists on-chain, it can receive assets directly.

OFT Types

Stellar uses a single OFT contract with an OftType enum to determine behavior:
MintBurn is the recommended approach when you have control over the token supply.

Setup & Deployment

For the full OFT reference implementation, see the OFT source in the LayerZero monorepo.
Prerequisites: This guide uses MintBurn mode. For LockUnlock mode, see Using LockUnlock Mode below. The MintBurn 3-contract pattern:
  1. Token (SAC): Your SEP-41 token
  2. SAC Manager: RBAC-controlled admin wrapper that implements Mintable. The OFT calls it to mint; it calls the SAC as its admin.
  3. OFT: Deployed with oft_type: MintBurn(sac_manager_address)

Step 1: Create and Deploy Your Token

If you don’t already have a token, deploy a SEP-41-compliant fungible token on Soroban. The simplest approach is to wrap a Stellar classic asset as a Stellar Asset Contract (SAC):
This returns the SAC contract address — your SEP-41 token address. Alternatively, you can build a custom Soroban contract token using OpenZeppelin Stellar Contracts for more control over token logic.

Step 2: Deploy the SAC Manager (Mintable Contract)

For MintBurn mode, the OFT needs a contract that implements the Mintable trait to mint tokens on crosschain receive:
The recommended approach is to use the SAC Manager contract provided by LayerZero, which wraps a SAC with role-based access control. Create SAC Manager project:
Configure Cargo.toml with LayerZero dependencies:
Implement SAC Manager:
The SAC Manager source is available in the LayerZero Stellar contracts. You can also implement your own contract — any contract that satisfies the Mintable trait will work.
Build and deploy:
The issuer account must be locked (master weight set to 0). In Stellar classic assets, transfers from/to the issuer are equivalent to minting/burning. The issuer can always mint more tokens and perform other classic operations directly, even when an explicit admin (this contract) is set. If the issuer account is not locked, the RBAC model enforced by this contract can be bypassed, breaking the trust model.

Step 3: Create and Deploy the OFT Contract

Create OFT project:
Configure Cargo.toml with LayerZero dependencies:
Check the OFT core, OApp contracts, protocol contracts on LayerZero GitHub for the latest Stellar contract packages.
Implement OFT:
The key traits generated and implemented: Build and deploy:
Constructor parameters:
Unlike the OApp constructor (which takes separate owner and delegate parameters), the OFT constructor uses delegate as both the initial owner and the endpoint delegate.

Step 4: Grant Minting Permissions

Grant the MINTER_ROLE on the SAC Manager to the OFT contract, so it can mint tokens during crosschain receives:

Step 5: Configure the OFT

The set_peer and set_enforced_options functions require the contract’s AUTHORIZER (the owner by default). After deployment, configure peers and enforced options:
The Stellar CLI (v25.1.0) has a known bug with Option<BytesN<32>> arguments — set_peer may silently set the peer to None. If this happens, use the JavaScript SDK workaround described in the OApp overview.

Using LockUnlock Mode

If you don’t have mint authority over the token (e.g., bridging an existing asset), use LockUnlock mode instead. The OFT holds tokens in its own balance — locking on send, unlocking on receive. No SAC Manager or Mintable contract is needed. Simply deploy the OFT with LockUnlock:
Then proceed to Step 5: Configure the OFT.

Core Operations

Do not use the issuer account as a sender or recipient. On Stellar, transfers from/to the token issuer are equivalent to minting/burning — the issuer does not hold a balance of its own asset. Tokens sent to the issuer are silently destroyed, which would cause asset loss in crosschain transfers.When testing OFT transfers, always use a non-issuer account. To set up a non-issuer holder:
  1. Generate a separate keypair and fund it via Friendbot
  2. Add a trustline for the custom asset: Operation.changeTrust({ asset })
  3. Transfer tokens from the issuer to the holder: Operation.payment({ destination, asset, amount })

Sending Tokens

Sending tokens crosschain involves quoting the fee and executing the transfer:

Step 1: Build SendParam

Step 2: Quote the Transfer

quote_oft returns: Both quote_oft and quote_send are read-only — they do not execute any state changes.

Step 3: Execute Send

The sender must authorize the call (from.require_auth() is called internally).

Receiving Tokens

Token receipt is handled automatically by the OFT contract’s lz_receive implementation:
  1. Decode the OFTMessage from the inbound payload
  2. Resolve the recipient address — the crosschain OFT message carries only the 32-byte address payload, but Stellar addresses are 35 bytes (1-byte version + 32-byte payload + 2-byte checksum) with two possible types (C-address for contracts, G-address for accounts). The resolve_address utility disambiguates by checking if a contract exists at the 32-byte address; if so, it resolves to a C-address, otherwise it falls back to a G-address. This is handled automatically.
  3. Convert amount from shared decimals to local decimals
  4. Credit tokens:
    • LockUnlock: token.transfer(from: oft_contract, to: recipient, amount)
    • MintBurn: mintable.mint(to: recipient, amount, operator: oft_contract)
  5. If a compose message is included, register it via endpoint.send_compose for subsequent execution
  6. Emit OFTReceived event

Decimal Precision

OFTs use a shared decimal system for crosschain consistency. The shared decimals value determines the precision used in the crosschain message, while local decimals are the token’s actual precision.
Any amount below the decimal_conversion_rate is considered dust and is removed before sending. For example, with a conversion rate of 10, an amount of 1,000,015 becomes 1,000,010 (the trailing 5 is dust).
Recommended configuration:

OFT Extensions

The Stellar OFT supports three optional extensions that can be enabled at deployment:

Pausable

Pause and unpause all OFT operations (send, receive, quote):
When paused, send, quote_send, quote_oft, and lz_receive (credit) all revert with error code 3110 (Paused).

OFT Fee

Charge a fee on outbound transfers, configurable per destination or as a default:
Fee resolution order: Per-destination fee > Default fee > 0 (no fee) Fee calculation: fee = amount_ld * fee_bps / 10_000

Rate Limiter

Control transfer volume with a leaky bucket algorithm:
Rate limit modes:

OFT Wire Format

For developers building custom integrations, the OFT message wire format:
  • Message type 1 (SEND): send_to + amount_sd
  • Message type 2 (SEND_AND_CALL): send_to + amount_sd + compose_from + compose_msg

Events

Best Practices & Deployment Checklist

  • Set shared_decimals carefully — it cannot be changed after deployment. Use 6 for compatibility with most chains.
  • Configure peers on both sides — the remote OFT must also set your Stellar OFT as a peer.
  • Set enforced options — always configure minimum gas for each destination to prevent failed deliveries.
  • Test with small amounts — verify the full send/receive flow with minimal amounts before large transfers.
  • Monitor rate limits — if enabled, ensure limits are set appropriately for expected volume.
  • Grant minting permissions — for MintBurn mode, ensure the OFT has the required role on the Mintable contract.

Next Steps