Soroban Examples Coverage
This page maps upstream examples from stellar/soroban-examples to documented Solang Solidity examples or coverage in this repository.
The table below only includes upstream examples for which this repository currently has a clear Solidity counterpart or nearest documented coverage example. Absence from this table does not prove that an upstream example is impossible in Solang; it means there is not yet a documented counterpart in this repository.
For the current feature-oriented support status, see Soroban Support Matrix.
Documented Counterparts
Upstream Rust example |
Solang Solidity example or coverage |
Notes |
|---|---|---|
Covered by Solidity testcases for dynamic memory arrays, including vector allocation, |
||
Atomic swap between two parties, with companion token contracts. |
||
docs/examples/soroban/atomic_multiswap.sol and tests/soroban_testcases/example_atomic_multiswap.rs |
Batches a set of atomic token swaps between multiple parties with simple price matching. Demonstrates |
|
Simple host-managed authorization via |
||
integration/soroban/caller.sol and integration/soroban/callee.sol |
Covered in cross_contract.spec.js. |
|
docs/examples/soroban/custom_types.sol and tests/soroban_testcases/example_custom_types.rs |
Struct stored in contract state ( |
|
docs/examples/soroban/other_custom_types.sol and tests/soroban_testcases/example_other_custom_types.rs |
Type-showcase contract: ABI round-trips for the supported subset ( |
|
Nested contract authorization via |
||
docs/examples/soroban/eth_abi.sol and tests/soroban_testcases/example_eth_abi.rs |
ABI codec round-trip: decode an |
|
docs/examples/soroban/groth16_verifier.sol and tests/soroban_testcases/example_groth16_verifier.rs |
Groth16 zkSNARK proof verifier over BLS12-381. Rebuilds the public-input combination |
|
docs/examples/soroban/hello_world.sol and tests/soroban_testcases/example_hello_world.rs |
Minimal |
|
Closest local counterpart for a stored counter that can be incremented. |
||
docs/examples/soroban/increment_with_pause.sol and tests/soroban_testcases/example_increment_with_pause.rs |
Counter that first checks a separate |
|
Liquidity-pool and token-swap example with companion token contracts. |
||
Demonstrates |
||
docs/examples/soroban/merkle_distribution.sol and tests/soroban_testcases/example_merkle_distribution.rs |
Merkle-proof airdrop: recipients claim tokens by supplying a Merkle proof against a stored root, and verified claims trigger a cross-contract token transfer. Demonstrates |
|
docs/examples/soroban/mint_lock.sol and tests/soroban_testcases/example_mint_lock.rs |
Admin-controlled minting proxy: an admin authorizes per-contract minters, each with a per-epoch limit, and every mint is dispatched as a cross-contract |
|
Timelock-style example using enums, mappings, authorization, and |
||
Token-style contract with balances, allowances, and Soroban auth. |
||
Solidity |
||
docs/examples/soroban/pause/pause.sol and tests/soroban_testcases/example_pause.rs |
Simple pause-flag contract: a single |
|
docs/examples/soroban/single_offer.sol and tests/soroban_testcases/example_single_offer.rs |
Single-offer exchange between a seller and a buyer, using a struct in instance storage, cross-contract token calls, and |
|
Extending TTL on stored contract data. |
||
docs/examples/soroban/upgradeable_contract.sol and tests/soroban_testcases/example_upgradeable_contract.rs |
Admin-gated self-upgrade: replaces the running contract Wasm via |
Solidity Translations
The following abridged snippets show how selected upstream Soroban examples are expressed in Solang Solidity.
atomic_multiswap
Upstream Soroban example: atomic_multiswap
Solang Solidity example: docs/examples/soroban/atomic_multiswap.sol
Batches a set of atomic token swaps between multiple parties, matching each swaps_a entry against the first compatible swaps_b entry and settling it through a deployed atomic_swap contract. Soroban memory arrays do support push/pop, but Solidity has no remove(i) for deleting an arbitrary element; rather than emulating removal, matched swaps_b entries are marked in a used flag array.
contract atomic_multiswap {
struct SwapSpec {
address addr;
int128 amount;
int128 min_recv;
}
function multi_swap(
address swap_contract,
address token_a,
address token_b,
SwapSpec[] memory swaps_a,
SwapSpec[] memory swaps_b
) public {
bool[] memory used = new bool[](swaps_b.length);
for (uint256 i = 0; i < swaps_a.length; i++) {
SwapSpec memory acc_a = swaps_a[i];
for (uint256 j = 0; j < swaps_b.length; j++) {
if (used[j]) {
continue;
}
SwapSpec memory acc_b = swaps_b[j];
if (acc_a.amount >= acc_b.min_recv && acc_a.min_recv <= acc_b.amount) {
bytes memory payload = abi.encode(
"swap",
acc_a.addr,
acc_b.addr,
token_a,
token_b,
acc_a.amount,
acc_a.min_recv,
acc_b.amount,
acc_b.min_recv
);
swap_contract.call(payload);
used[j] = true;
break;
}
}
}
}
}
pause
Upstream Soroban example: pause
Solang Solidity example: docs/examples/soroban/pause/pause.sol
contract Pause {
bool instance paused_flag = false;
function paused() public view returns (bool) {
return paused_flag;
}
function set(bool paused) public {
paused_flag = paused;
}
}
auth
Upstream Soroban example: auth
Solang Solidity example: docs/examples/soroban/auth.sol
contract auth {
address public owner =
address"GDRIX624OGPQEX264NY72UKOJQUASHU3PYKL6DDPGSTWXWJSBOTR6N7W";
uint64 public instance counter = 20;
function increment() public returns (uint64) {
owner.requireAuth();
counter = counter + 1;
return counter;
}
}
custom_types
Upstream Soroban example: custom_types
Solang Solidity example: docs/examples/soroban/custom_types.sol
contract CustomTypes {
struct State {
uint32 count;
uint32 last_incr;
}
State state;
function increment(uint32 incr) public returns (uint32) {
state.count += incr;
state.last_incr = incr;
return state.count;
}
function get_state() public view returns (State memory) {
return state;
}
}
other_custom_types
Upstream Soroban example: other_custom_types
Solang Solidity example: docs/examples/soroban/other_custom_types.sol
The upstream contract is a showcase of every custom and primitive type the
Soroban host understands. This port mirrors every upstream method with a
Solidity/Soroban-target counterpart, following the upstream method names and
ordering — the primitive echoes, a Symbol (string) echo, unit enums, a
uint32[] vector, struct round-trips, event emission with host
authorization, a persistent counter, multiple arguments, a void method and
require-based error handling. The only names that diverge from upstream are
bytes_ and string_ (bytes and string are Solidity type
keywords):
contract other_custom_types {
struct Test {
uint32 a;
bool b;
string c;
}
enum SimpleEnum { First, Second, Third }
event AuthEvent(address indexed hello, string world);
uint32 persistent count;
function inc() public returns (uint32) {
count += 1;
return count;
}
function auth(address addr, string memory world) public returns (address) {
addr.requireAuth();
emit AuthEvent(addr, world);
return addr;
}
function simple(SimpleEnum v) public pure returns (SimpleEnum) {
return v;
}
function vec(uint32[] memory v) public pure returns (uint32[] memory) {
return v;
}
function strukt(Test memory t) public pure returns (Test memory) {
return t;
}
function u32_fail_on_even(uint32 v) public pure returns (uint32) {
require(v % 2 == 1, "NumberMustBeOdd");
return v;
}
}
The composite structs TupleStruct and ComplexStruct are ported with
their sum-type-enum fields adapted to unit enums and uint32[] vectors, the
closest supported types. The upstream methods relying on sum-type enums with
associated data (ComplexEnum/ComplexEnum2/ComplexEnum3), tuples,
Map, Option and the untyped Val are omitted, as those types have no
Solidity/Soroban-target counterpart.
token
Upstream Soroban example: token
Solang Solidity example: docs/examples/soroban/token.sol
contract token {
address public admin;
mapping(address => int128) public balances;
constructor(address _admin, string memory _name, string memory _symbol, uint32 _decimals) {
admin = _admin;
}
function mint(address to, int128 amount) public {
require(amount >= 0, "Amount must be non-negative");
admin.requireAuth();
balances[to] = balances[to] + amount;
}
function transfer(address from, address to, int128 amount) public {
from.requireAuth();
balances[from] = balances[from] - amount;
balances[to] = balances[to] + amount;
}
}
timelock
Upstream Soroban example: timelock
Solang Solidity example: docs/examples/soroban/timelock/timelock.sol
contract timelock {
enum TimeBoundKind { Before, After }
struct TimeLock {
TimeBoundKind kind;
uint64 bound_timestamp;
address claimant;
uint64 amount;
}
mapping(address => TimeLock) public timelocks;
function is_claimable(address claimant) public view returns (bool) {
TimeLock storage tl = timelocks[claimant];
return block.timestamp >= tl.bound_timestamp;
}
}
ttl
Upstream Soroban example: ttl
Solang Solidity example: docs/examples/soroban/ttl_storage.sol
contract ttl_storage {
uint64 public persistent pCount = 11;
uint64 temporary tCount = 7;
uint64 instance iCount = 3;
function extend_persistent_ttl() public view returns (int64) {
return pCount.extendTtl(1000, 5000);
}
function extend_temp_ttl() public view returns (int64) {
return tCount.extendTtl(3000, 7000);
}
}
events
Upstream Soroban example: events
Solang Solidity example: docs/examples/soroban/events.sol
contract IncrementContract {
uint32 public instance count = 0;
event IncrementEvent(string indexed action, string indexed method, uint32 count);
function increment() public returns (uint32) {
count += 1;
emit IncrementEvent("COUNTER", "increment", count);
return count;
}
}
increment_with_pause
Upstream Soroban example: increment_with_pause
Solang Solidity example: docs/examples/soroban/increment_with_pause.sol
contract IncrementContract {
address public instance pause_contract;
uint32 public instance count = 0;
constructor(address _pause) {
pause_contract = _pause;
}
function increment() public returns (uint32) {
bytes payload = abi.encode("paused");
(, bytes memory ret) = pause_contract.call(payload);
bool is_paused = abi.decode(ret, (bool));
require(!is_paused, "Paused");
count += 1;
extendInstanceTtl(50, 100);
return count;
}
}
hello_world
Upstream Soroban example: hello_world
Solang Solidity example: docs/examples/soroban/hello_world.sol
contract hello_world {
function hello(string memory to) public pure returns (string[] memory) {
string[] memory result = new string[](2);
result[0] = "Hello";
result[1] = to;
return result;
}
}
single_offer
Upstream Soroban example: single_offer
Solang Solidity example: docs/examples/soroban/single_offer.sol
contract single_offer {
struct Offer {
address seller;
address sell_token;
address buy_token;
uint32 sell_price;
uint32 buy_price;
}
Offer instance offer;
bool instance created = false;
function create(
address seller,
address sell_token,
address buy_token,
uint32 sell_price,
uint32 buy_price
) public {
require(!created, "offer is already created");
require(buy_price != 0 && sell_price != 0, "zero price is not allowed");
seller.requireAuth();
offer = Offer({
seller: seller,
sell_token: sell_token,
buy_token: buy_token,
sell_price: sell_price,
buy_price: buy_price
});
created = true;
}
function trade(
address buyer,
int128 buy_token_amount,
int128 min_sell_token_amount
) public {
buyer.requireAuth();
Offer memory o = offer;
int128 sell_token_amount = (buy_token_amount * int128(o.sell_price)) / int128(o.buy_price);
require(sell_token_amount >= min_sell_token_amount, "price is too low");
address contract_address = address(this);
token_transfer(o.buy_token, buyer, contract_address, buy_token_amount);
token_transfer(o.sell_token, contract_address, buyer, sell_token_amount);
token_transfer(o.buy_token, contract_address, o.seller, buy_token_amount);
}
function withdraw(address token, int128 amount) public {
Offer memory o = offer;
o.seller.requireAuth();
token_transfer(token, address(this), o.seller, amount);
}
function updt_price(uint32 sell_price, uint32 buy_price) public {
require(buy_price != 0 && sell_price != 0, "zero price is not allowed");
offer.seller.requireAuth();
offer.sell_price = sell_price;
offer.buy_price = buy_price;
}
function get_offer() public view returns (Offer memory) {
return offer;
}
function token_transfer(address token, address from, address to, int128 amount) internal {
bytes memory payload = abi.encode("transfer", from, to, amount);
(bool success, bytes memory returndata) = token.call(payload);
}
}
upgradeable_contract
Upstream Soroban example: upgradeable_contract
Solang Solidity example: docs/examples/soroban/upgradeable_contract.sol
Replaces the running contract’s Wasm in place. The admin-gated upgrade first calls requireAuth() on the stored admin, then hands an already-uploaded Wasm hash to updateCurrentContractWasm(bytes32), which lowers to the host function update_current_contract_wasm. The version() marker returns 1 in this build and 2 in the upgraded build, so a caller can observe the swap.
contract UpgradeableContract {
address instance admin;
constructor(address _admin) {
admin = _admin;
}
function version() public pure returns (uint32) {
return 1;
}
function upgrade(bytes32 new_wasm_hash) public {
admin.requireAuth();
updateCurrentContractWasm(new_wasm_hash);
}
}
merkle_distribution
Upstream Soroban example: merkle_distribution
Solang Solidity example: docs/examples/soroban/merkle_distribution.sol
A Merkle-proof airdrop. The constructor stores the tree root and pulls a
funding deposit from a token contract; claim recomputes the Merkle root
from a leaf and its proof, verifies it against the stored root, and pays the
receiver. Inner nodes hash a real 64-byte buffer via bytes.concat (not
abi.encode, which on Soroban would pack ScVal object handles). The leaf is
sha256(to_xdr(Receiver{index, recipient, amount})), matching upstream’s
sha256(Receiver.to_xdr()) — the to_xdr builtin lowers to the Soroban
host’s serialize_to_bytes, the same primitive the Rust SDK uses, so the leaf
commits to the value’s canonical XDR rather than to ephemeral ScVal object
handles. The one divergence from upstream is the field name recipient
(Solidity reserves address as a keyword), which changes the struct’s ScMap
symbol keys and therefore the XDR bytes, so an off-chain tree builder must
serialize with this contract’s exact field names.
contract merkle_distribution {
bytes32 instance rootHash;
address instance tokenAddress;
mapping(uint32 => bool) instance claimed;
struct Receiver {
uint32 index;
address recipient;
int128 amount;
}
constructor(
bytes32 root_hash,
address token,
int128 funding_amount,
address funding_source
) {
rootHash = root_hash;
tokenAddress = token;
bytes payload = abi.encode(
"transfer", funding_source, address(this), funding_amount
);
(bool ok, ) = token.call(payload);
require(ok, "funding transfer failed");
}
function compute_root(bytes32 leaf, bytes32[] memory proof)
public
pure
returns (bytes32)
{
bytes32 h = leaf;
for (uint32 i = 0; i < proof.length; i++) {
if (h < proof[i]) {
h = sha256(bytes.concat(h, proof[i]));
} else {
h = sha256(bytes.concat(proof[i], h));
}
}
return h;
}
function claim(
uint32 index,
address receiver,
int128 amount,
bytes32[] memory proof
) public {
require(!claimed[index], "AlreadyClaimed");
Receiver memory node = Receiver(index, receiver, amount);
bytes32 leaf = sha256(to_xdr(node));
require(compute_root(leaf, proof) == rootHash, "InvalidProof");
bytes payload = abi.encode(
"transfer", address(this), receiver, amount
);
(bool ok, ) = tokenAddress.call(payload);
require(ok, "payout transfer failed");
claimed[index] = true;
}
}
eth_abi
Upstream Soroban example: eth_abi
Solang Solidity example: docs/examples/soroban/eth_abi.sol
The upstream example decodes an ABI-encoded Input, computes an Output whose r is the sum of the two input words, and returns the ABI-encoded Output. The port keeps that logic in exec using abi.decode/abi.encode. On the Soroban target these builtins operate on Soroban’s native value model rather than Ethereum’s 32-byte-word ABI: an encoded buffer carries live host-object handles (a struct becomes one Map object, bytes32 a Bytes object, uint256 a 256-bit-integer object), not portable padded words. Those handles are only valid within the invocation that produced them, so the run driver performs the whole encode → exec → decode chain in a single call.
contract EthAbi {
struct Input { bytes32 a; uint256 b; uint256 c; }
struct Output { bytes32 a; uint256 r; }
function exec(bytes memory input) public pure returns (bytes memory) {
Input memory i = abi.decode(input, (Input));
return abi.encode(Output(i.a, i.b + i.c));
}
function run(bytes32 a, uint256 b, uint256 c) public pure returns (Output memory) {
bytes memory input = abi.encode(Input(a, b, c));
bytes memory output = exec(input);
return abi.decode(output, (Output));
}
}
mint-lock
Upstream Soroban example: mint-lock
Solang Solidity example: docs/examples/soroban/mint_lock.sol
An admin-controlled minting proxy. The admin authorizes minters against a wrapped token, each with a per-epoch limit; the admin can always mint, an authorized minter can mint until it exhausts the epoch’s limit, and anyone else is rejected. The upstream composite storage keys become nested mappings (minters[contract_][minter_] and the four-level stats[contract_][minter_][epoch_length][epoch]), the epoch is derived from block.number (Soroban’s ledger sequence), and each mint is dispatched with contract_.call(abi.encode("mint", to, amount)). The minter authorizes the mint with requireAuthForArgs(contract_, to, amount) — the require_auth_for_args mapping that scopes the signed authorization to those exact arguments instead of the full call argument list that requireAuth() would demand — while set_admin/set_minter gate on the admin’s requireAuth(). The per-epoch stats are declared persistent rather than temporary: a temporary entry would be evicted once the ledger sequence advances past its un-extended TTL, and the tests jump the sequence to mid-epoch.
contract mint_lock {
struct MinterConfig {
int128 limit;
uint32 epoch_length;
}
struct MinterStats {
int128 consumed_limit;
}
struct MinterInfo {
MinterConfig config;
uint32 epoch;
MinterStats minter_stats;
}
address instance admin;
mapping(address => mapping(address => MinterConfig)) persistent minters;
mapping(address => mapping(address => bool)) persistent minter_exists;
mapping(address =>
mapping(address =>
mapping(uint32 =>
mapping(uint32 => MinterStats)))) persistent stats;
constructor(address admin_) {
admin = admin_;
}
function set_admin(address new_admin) public {
admin.requireAuth();
admin = new_admin;
}
function get_admin() public view returns (address) {
return admin;
}
function set_minter(
address contract_,
address minter_,
MinterConfig memory config
) public {
admin.requireAuth();
minters[contract_][minter_] = config;
minter_exists[contract_][minter_] = true;
}
function minter(address contract_, address minter_)
public
view
returns (MinterInfo memory)
{
require(minter_exists[contract_][minter_], "not authorized minter");
MinterConfig memory config = minters[contract_][minter_];
uint32 epoch = uint32(block.number / uint64(config.epoch_length));
MinterStats memory minter_stats = stats[contract_][minter_][config.epoch_length][epoch];
return MinterInfo({config: config, epoch: epoch, minter_stats: minter_stats});
}
function mint(
address contract_,
address minter_,
address to,
int128 amount
) public {
minter_.requireAuthForArgs(contract_, to, amount);
require(amount >= 0, "negative amount");
// The admin can always mint; everyone else is rate-limited per epoch.
if (admin != minter_) {
require(minter_exists[contract_][minter_], "not authorized minter");
MinterConfig memory config = minters[contract_][minter_];
uint32 epoch = uint32(block.number / uint64(config.epoch_length));
MinterStats memory minter_stats = stats[contract_][minter_][config.epoch_length][epoch];
int128 new_consumed = minter_stats.consumed_limit + amount;
require(new_consumed <= config.limit, "daily limit insufficient");
minter_stats.consumed_limit = new_consumed;
stats[contract_][minter_][config.epoch_length][epoch] = minter_stats;
}
// Dispatch the actual mint into the wrapped token contract.
bytes memory payload = abi.encode("mint", to, amount);
(bool success, bytes memory returndata) = contract_.call(payload);
}
}
deployer
Upstream Soroban example: deployer
Solang Solidity example: docs/examples/soroban/deployer
A factory contract. The admin-gated deploy deploys an already-uploaded Wasm blob — identified by its 32-byte wasm_hash — on behalf of the factory, deriving the new contract’s address from salt, and runs the deployed contract’s constructor with the supplied arguments. This maps to the deployContract(wasm_hash, salt, ...args) builtin, which lowers to the host function create_contract_with_constructor using the current contract address as the deployer (as in upstream’s env.deployer().with_address(env.current_contract_address(), salt).deploy_v2(...)). Because Soroban deploys by Wasm hash rather than embedding child code the way the EVM does, the deployed contract’s Wasm must already be uploaded to the ledger; only its hash is passed here. The trailing arguments (init_value below) are marshalled into the constructor-args vector.
contract deployer {
address instance admin;
constructor(address admin_) {
admin = admin_;
}
function deploy(bytes32 wasm_hash, bytes32 salt, uint32 init_value)
public
returns (address)
{
admin.requireAuth();
return deployContract(wasm_hash, salt, init_value);
}
}
The contract that gets deployed simply stores its constructor argument so a caller can confirm the constructor ran:
contract deployed {
uint32 instance stored_value;
constructor(uint32 value) {
stored_value = value;
}
function value() public view returns (uint32) {
return stored_value;
}
}
groth16_verifier
Upstream Soroban example: groth16_verifier
Solang Solidity example: docs/examples/soroban/groth16_verifier.sol
A Groth16 zkSNARK proof verifier over the BLS12-381 curve. verify_proof first rebuilds the linear combination of the public inputs, vk_x = ic[0] + sum(pub_signals[i] * ic[i+1]), then checks the pairing product e(-A,B) * e(alpha,beta) * e(vk_x,gamma) * e(C,delta) == 1. Three BLS12-381 host functions are exposed as builtins — bls12_381_g1_mul(bytes, uint256), bls12_381_g1_add(bytes, bytes) and bls12_381_pairing_check(bytes[], bytes[]) — mapping to the host’s bls12_381_g1_mul, bls12_381_g1_add and bls12_381_multi_pairing_check. Curve points stay opaque bytes (G1 = 96 bytes, G2 = 192 bytes) that the host validates, and pub_signals are uint256 Fr scalars, mirroring upstream’s Vec<Fr>. The host exposes no G1 negation, so -A is obtained without any in-contract field arithmetic: because G1 has prime order r, (r-1)*A == -A, computed with bls12_381_g1_mul(proof.a, R - 1) where R is the scalar-field order.
contract groth16_verifier {
uint256 constant R =
0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001;
uint256 constant R_MINUS_1 = R - 1;
struct VerificationKey {
bytes alpha; // G1
bytes beta; // G2
bytes gamma; // G2
bytes delta; // G2
bytes[] ic; // G1[]
}
struct Proof {
bytes a;
bytes b;
bytes c;
}
function verify_proof(
VerificationKey vk,
Proof proof,
uint256[] pub_signals
) public returns (bool) {
require(pub_signals.length + 1 == vk.ic.length, "MalformedVerifyingKey");
bytes memory vk_x = vk.ic[0];
for (uint32 i = 0; i < pub_signals.length; i++) {
bytes memory prod = bls12_381_g1_mul(vk.ic[i + 1], pub_signals[i]);
vk_x = bls12_381_g1_add(vk_x, prod);
}
// -A via scalar (r-1); no dedicated negation host function exists.
bytes memory negA = bls12_381_g1_mul(proof.a, R_MINUS_1);
// e(-A, B) * e(alpha, beta) * e(vk_x, gamma) * e(C, delta) == 1
bytes[] memory vp1 = new bytes[](4);
vp1[0] = negA;
vp1[1] = vk.alpha;
vp1[2] = vk_x;
vp1[3] = proof.c;
bytes[] memory vp2 = new bytes[](4);
vp2[0] = proof.b;
vp2[1] = vk.beta;
vp2[2] = vk.gamma;
vp2[3] = vk.delta;
return bls12_381_pairing_check(vp1, vp2);
}
}
Upstream Examples Not Yet Documented as Supported
The following upstream examples do not currently have a documented Solidity counterpart, as some needed Soroban features are not yet supported. - bls_signature - errors - fuzzing - privacy-pools - simple_account - workspace
Want to add support for one of the remaining examples? Open a pull request against hyperledger-solang/solang and follow the contribution guide.