SATURN Reference Interactive docs · Launch v4 ↗ · Launch v3 ↗

Saturn DEX — Full Contract Reference

Saturn is a modular, fully on-chain decentralized exchange (DEX) and lending protocol built on the Phantasma blockchain. This documents every public smart-contract method that developers and AI agents can call to build on Saturn. Two products: v4 (30 modular contracts across core DEX, advanced pools & capital, financial products, agent automation, and lending) and v3 (the legacy monolithic SATRN contract). Read methods are free to call; write methods require the caller's wallet signature (witness). Address each contract by its lowercase symbol (e.g. saturnrouter, saturnswap, saturnmarket) or SATRN for v3.

How to build: Use phantasma-sdk-ts (or the Poltergeist / Ecto wallets). To READ: build a script with ScriptBuilder.callContract(contractId, method, [args]) and call api.invokeRawScript('main', script), then decode the result. To WRITE: build a transaction that calls the method with its args; the user signs and their wallet becomes the on-chain witness. Gas is paid in KCAL on Phantasma.

Launch the app: mainnet Saturn v4 · Saturn v3; devnet v4 · v3. Machine-readable: llms.txt · llms-full.txt. Examples: GitHub.

31 contracts · 662 public methods documented.

Saturn DEX v4

Core DEX · Contract #1

SaturnAdmin

saturnadmin

Central configuration hub. Exposes global protocol parameters — fee splits, pool fee range, scaling limits, minimum amounts, and SOUL storage fees. All other Saturn contracts read from here, and so does your frontend. Every method below is a pure view call, free to invoke, and the primary source-of-truth for showing limits to users before they submit transactions.

Ownership & Access

getAdmin()

READ
getAdmin(): address

Returns the current protocol admin address. Use this to verify whether a wallet has privileged control or simply to display protocol ownership in your UI.

Returns
address — The admin address for the Saturn protocol.
What to expect
Always returns a valid, non-null address. No reverts.
Example
// Phantasma SDK — invokeRawScript to read
const script = ScriptBuilder
  .begin()
  .callContract("saturnadmin", "getAdmin", [])
  .endScript();

const res = await api.invokeRawScript("main", script);
const adminAddress = res.decoded[0]; // address

Fee Split Configuration

getFeeSplitRatios()

READ
getFeeSplitRatios(): string

Returns all three fee split ratios packed into a single string of the form "reinvest:X_provider:Y_admin:Z". Convenient for single-call rendering of the complete fee split.

Returns
string — Example: "reinvest:60_provider:10_admin:30" — percentages sum to 100.
What to expect
The three numbers always sum to 100. Admin is always ≥ 1. Use this if you want one round-trip instead of three.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnadmin", "getFeeSplitRatios", [])
  .endScript();

const { decoded } = await api.invokeRawScript("main", script);
// "reinvest:60_provider:10_admin:30"

getReinvestPct()

READ
getReinvestPct(): number

Percentage of each swap fee that is reinvested back into the pool's reserves. This grows the pool's depth over time.

Returns
number — Percentage out of 100 (default: 60).
What to expect
Typically 60. Always ≥ 0 and ≤ 100.
Example
const reinvest = await readContract("saturnadmin", "getReinvestPct", []);

getProviderPct()

READ
getProviderPct(): number

Percentage of each swap fee paid to the pool provider, accrued in the FeeVault and claimable via SaturnFees.claimProviderFees().

Returns
number — Percentage out of 100 (default: 10).
What to expect
Typically 10. Always ≥ 0.
Example
const provider = await readContract("saturnadmin", "getProviderPct", []);

getAdminPct()

READ
getAdminPct(): number

Percentage of each swap fee routed directly to the protocol admin treasury at swap time.

Returns
number — Percentage out of 100 (default: 30).
What to expect
Always ≥ 1 (protocol guarantee).
Example
const admin = await readContract("saturnadmin", "getAdminPct", []);

Pool Fee Range

getPoolFeeRange()

READ
getPoolFeeRange(): string

Returns both min and max per-pool fee rate as a single packed string. Use when creating a pool so the UI can clamp the user's fee slider to the valid range.

Returns
string — Example: "min:30_max:3000" (basis points per 10k → 0.3% to 30%).
What to expect
min is always < max. min ≥ 10, max ≤ 5000.
Example
const range = await readContract("saturnadmin", "getPoolFeeRange", []);
// "min:30_max:3000"

getMinPoolFeePer10k()

READ
getMinPoolFeePer10k(): number

Minimum fee rate (in basis points out of 10,000) a provider may set when creating a pool. 30 means 0.3%.

Returns
number — Minimum fee in basis points (default: 30 = 0.3%).
What to expect
Never below 10. Default is 30 (0.3%).
Example
const minFee = await readContract("saturnadmin", "getMinPoolFeePer10k", []);

getMaxPoolFeePer10k()

READ
getMaxPoolFeePer10k(): number

Maximum allowed pool fee rate (in basis points out of 10,000). 3000 means 30%.

Returns
number — Maximum fee in basis points (default: 3000 = 30%).
What to expect
Never above 5000. Default is 3000 (30%).
Example
const maxFee = await readContract("saturnadmin", "getMaxPoolFeePer10k", []);

Scaling & Minimum Amounts

getTargetDecimals()

READ
getTargetDecimals(): number

The internal scaling target — all tokens are scaled up to this many decimals inside the protocol to keep math precise regardless of each token's native decimals.

Returns
number — Target decimals (default: 8).
What to expect
Never changes after deployment in practice. Default: 8.
Example
const dec = await readContract("saturnadmin", "getTargetDecimals", []);

getMinScaledPoolUnits()

READ
getMinScaledPoolUnits(): number

Minimum amount (in scaled units) required when creating a pool. Combined with each token's scale factor, this becomes the raw-unit minimum returned by SaturnRouter.getMinRawForPoolCreation().

Returns
number — Minimum scaled units for pool creation.
What to expect
Default: 10,000,000,000. Used by Liquidity Manager when validating createPool().
Example
const minPool = await readContract("saturnadmin", "getMinScaledPoolUnits", []);

getMinScaledSwapUnits()

READ
getMinScaledSwapUnits(): number

Minimum amount (in scaled units) required per swap. Enforced by the Swap Engine and used by Router views to show users the minimum swap they can submit.

Returns
number — Minimum scaled units for a single swap.
What to expect
Default: 112,000,000,000.
Example
const minSwap = await readContract("saturnadmin", "getMinScaledSwapUnits", []);

getMinScaledAddLiqUnits()

READ
getMinScaledAddLiqUnits(): number

Minimum amount (in scaled units) when adding liquidity to an existing pool.

Returns
number — Minimum scaled units for add-liquidity.
What to expect
Default: 100,000,000.
Example
const minAdd = await readContract("saturnadmin", "getMinScaledAddLiqUnits", []);

getAbsoluteMinRaw()

READ
getAbsoluteMinRaw(): number

The absolute floor for any computed raw-unit minimum. Even if scaling math would give a smaller number, raw minimums never drop below this value.

Returns
number — Absolute minimum raw units (default: 100).
What to expect
Default: 100. Protects against tokens with extreme scale factors.
Example
const floor = await readContract("saturnadmin", "getAbsoluteMinRaw", []);

getMaxTruncationPercent()

READ
getMaxTruncationPercent(): number

Maximum allowable truncation loss (in %) when removing a pool. If scale-down rounding would lose more than this percentage of either token's reserve, removePool() reverts.

Returns
number — Maximum truncation percent (default: 10).
What to expect
Range: 1–50. Default: 10%.
Example
const maxTrunc = await readContract("saturnadmin", "getMaxTruncationPercent", []);

SOUL Storage Fees

getSOULfeeCreatePool()

READ
getSOULfeeCreatePool(): number

The SOUL-denominated storage fee a provider must pay (on top of their token pair) when calling createPool(). The fee is staked by the protocol for on-chain storage rent.

Returns
number — Raw SOUL amount (default: 2,500,000).
What to expect
The provider must have getSOULfeeCreatePool() + a little buffer in their wallet before calling createPool(). Default: 2,500,000.
Example
const fee = await readContract("saturnadmin", "getSOULfeeCreatePool", []);

getSOULfeeAddLiquidity()

READ
getSOULfeeAddLiquidity(): number

SOUL-denominated storage fee required when calling addLiquidity() on an existing pool.

Returns
number — Raw SOUL amount (default: 1,250,000).
What to expect
Half of the pool-creation fee by default.
Example
const fee = await readContract("saturnadmin", "getSOULfeeAddLiquidity", []);

getStorageFee()

READ
getStorageFee(): number

Current balance of SOUL fees the protocol has accrued but not yet staked. Purely informational — useful for protocol dashboards.

Returns
number — Raw SOUL currently held pending staking.
What to expect
The protocol auto-stakes once this crosses 100,000,000 (1 SOUL), so this value resets periodically.
Example
const pending = await readContract("saturnadmin", "getStorageFee", []);

Reentrancy Inspection

getGuardLocked()

READ
getGuardLocked(user: address): number

Returns 1 if the given user currently holds a reentrancy guard slot (meaning they are mid-transaction somewhere in the protocol), 0 otherwise. Normally this should be 0 between transactions.

Parameters
NameTypeDescription
useraddressThe wallet address to inspect.
Returns
number — 1 = locked, 0 = free.
What to expect
If this ever returns 1 for a user outside an active call, it means a previous transaction left a stuck guard (very rare). Admin can clear it via clearReentrancy().
Example
const locked = await readContract("saturnadmin", "getGuardLocked", [userAddress]);
if (locked === 1) console.warn("guard stuck for", userAddress);
Core DEX · Contract #2

SaturnPools

saturnpools

Canonical source of every pool in the protocol. Stores per-pool state (provider, token pair, reserves, fee, active/pawned/lock flags), the canonical pair index, global reserves and token scale factors. Most methods here are view calls that your frontend uses to enumerate pools and read state. Two actions are available to the pool provider: updating the per-pool fee and pawning / unpawning a pool.

Canonical Keys

getCanonicalPairKey()

READ
getCanonicalPairKey(symbolA: string, symbolB: string): string

Returns the protocol's canonical key for a token pair. Symbols are sorted alphabetically so that "SOUL+KCAL" and "KCAL+SOUL" both map to the same key "KCAL_SOUL". Use this whenever you need to look up how many pools exist for a pair.

Parameters
NameTypeDescription
symbolAstringFirst token symbol.
symbolBstringSecond token symbol.
Returns
string — Canonical pair key, e.g. "KCAL_SOUL".
What to expect
Pure function — always returns the same key for the same two symbols in any order. Never reverts.
Example
const key = await readContract("saturnpools", "getCanonicalPairKey", ["SOUL", "KCAL"]);
// "KCAL_SOUL"
const count = await readContract("saturnpools", "getPairPoolCount", [key]);

getPoolPairKey()

READ
getPoolPairKey(poolId: number): string

Returns the canonical pair key for a specific pool. Equivalent to calling getCanonicalPairKey() with that pool's two token symbols.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
string — Canonical pair key for the pool's token pair.
What to expect
Reverts on non-existent poolId.
Example
const pairKey = await readContract("saturnpools", "getPoolPairKey", [poolId]);

Scaling Helpers

scaleUp()

READ
scaleUp(amount: number, symbol: string): number

Converts a raw (on-chain decimal) amount into the protocol's internal scaled units, using the stored scale factor for that token. Use this before passing amounts to any method that expects scaled units.

Parameters
NameTypeDescription
amountnumberRaw token amount.
symbolstringToken symbol.
Returns
number — Scaled amount (amount * scaleFactor).
What to expect
If the scale factor has never been computed for this token, returns the amount unchanged. Most tokens are pre-scaled during pool creation.
Example
const scaled = await readContract("saturnpools", "scaleUp", [1000000, "KCAL"]);

scaleDown()

READ
scaleDown(amount: number, symbol: string): number

Inverse of scaleUp — converts a scaled internal amount back into raw token units for display. Use this when reading reserves, payouts, or any number returned by a protocol view.

Parameters
NameTypeDescription
amountnumberScaled amount.
symbolstringToken symbol.
Returns
number — Raw token amount (scaled / scaleFactor).
What to expect
Rounds down (integer division). Safe to call on any token symbol.
Example
const scaledReserve = await readContract("saturnpools", "getPoolReserveA", [poolId]);
const tokenA = await readContract("saturnpools", "getPoolTokenA", [poolId]);
const raw = await readContract("saturnpools", "scaleDown", [scaledReserve, tokenA]);

getScaleFactor()

READ
getScaleFactor(tokenSymbol: string): number

Returns the stored scale factor for a token. A scale factor of 0 means the token has not yet been registered in any pool.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Multiplier used to scale raw amounts to protocol units.
What to expect
Returns 0 for tokens that have never been added to a pool.
Example
const factor = await readContract("saturnpools", "getScaleFactor", ["SOUL"]);

getMinRawForToken()

READ
getMinRawForToken(symbol: string, minScaledUnits: number): number

Given a token and a scaled-unit minimum from SaturnAdmin (e.g. getMinScaledSwapUnits), returns the equivalent raw-unit minimum that a user must provide. The result is floored by getAbsoluteMinRaw so tiny scale factors can't produce zero minimums.

Parameters
NameTypeDescription
symbolstringToken symbol.
minScaledUnitsnumberScaled-unit threshold.
Returns
number — Minimum raw amount the user must submit.
What to expect
Equivalent to the higher-level helpers SaturnRouter exposes (getMinRawForSwap/AddLiquidity). Prefer those unless you want fine control.
Example
const minScaled = await readContract("saturnadmin", "getMinScaledSwapUnits", []);
const minRaw = await readContract("saturnpools", "getMinRawForToken", ["KCAL", minScaled]);

validateTokenSymbol()

READ
validateTokenSymbol(symbol: string)

Reverts with "Token does not exist" if the given symbol isn't registered as a Phantasma token. Useful as a cheap preflight before submitting a transaction that would otherwise fail mid-execution.

Parameters
NameTypeDescription
symbolstringToken symbol to validate.
Returns
void — No return value — reverts on invalid token.
What to expect
Success = symbol exists. Failure throws and your invokeRawScript will return an error containing the revert message.
Example
try {
  await readContract("saturnpools", "validateTokenSymbol", ["MYTOKEN"]);
  // token exists
} catch (e) {
  console.error("Unknown token");
}

Per-Pool State

getPoolProvider()

READ
getPoolProvider(poolId: number): address

Returns the address of the wallet that created (and currently owns) the given pool.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
address — The pool provider's wallet address.
What to expect
Pool ownership is transferable via the SATURN NFT — this value reflects the current holder.
Example
const provider = await readContract("saturnpools", "getPoolProvider", [poolId]);

getPoolTokenA()

READ
getPoolTokenA(poolId: number): string

Returns the symbol of the pool's first token (slot A). Note: A/B slot order is provider-chosen at creation and is NOT canonical.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
string — Token symbol stored in slot A.
What to expect
Use getCanonicalPairKey if you need a consistent ordering.
Example
const tokenA = await readContract("saturnpools", "getPoolTokenA", [poolId]);

getPoolTokenB()

READ
getPoolTokenB(poolId: number): string

Returns the symbol of the pool's second token (slot B).

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
string — Token symbol stored in slot B.
What to expect
Together with getPoolTokenA, identifies the pool's trading pair.
Example
const tokenB = await readContract("saturnpools", "getPoolTokenB", [poolId]);

getPoolReserveA()

READ
getPoolReserveA(poolId: number): number

Returns the scaled reserve of token A in the pool. Pass the result through scaleDown() to get the raw amount for display.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Scaled reserve of token A.
What to expect
Updated atomically by swap / add-liquidity / fee-accrual flows.
Example
const resA = await readContract("saturnpools", "getPoolReserveA", [poolId]);
const symA = await readContract("saturnpools", "getPoolTokenA", [poolId]);
const displayA = await readContract("saturnpools", "scaleDown", [resA, symA]);

getPoolReserveB()

READ
getPoolReserveB(poolId: number): number

Returns the scaled reserve of token B in the pool.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Scaled reserve of token B.
What to expect
Pair with getPoolReserveA for full constant-product state.
Example
const resB = await readContract("saturnpools", "getPoolReserveB", [poolId]);

getPoolActive()

READ
getPoolActive(poolId: number): number

Returns 1 if the pool is active (can be swapped, can accept liquidity), 0 if it has been removed.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = active, 0 = removed.
What to expect
Removed pools keep their ID forever but are filtered out by router views.
Example
const isActive = (await readContract("saturnpools", "getPoolActive", [poolId])) === 1;

getPoolPawned()

READ
getPoolPawned(poolId: number): number

Returns 1 if the pool is currently pawned (provider has locked it against transfer / removal), 0 otherwise.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = pawned, 0 = free.
What to expect
Pawned pools still swap normally but can't be transferred or removed until unpawned.
Example
const pawned = await readContract("saturnpools", "getPoolPawned", [poolId]);

getPoolNftId()

READ
getPoolNftId(poolId: number): number

Returns the token ID of the SATURN NFT certificate that represents ownership of this pool. Transferring this NFT transfers pool ownership.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — SATURN NFT token ID (0 if not yet minted).
What to expect
Set by the SATURN NFT contract immediately after pool creation.
Example
const nftId = await readContract("saturnpools", "getPoolNftId", [poolId]);

getPoolFee()

READ
getPoolFee(poolId: number): number

Returns the pool's per-swap fee rate in basis points out of 10,000. 300 = 3%. Range: 30–3000 (0.3%–30%).

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Fee in basis points per 10k.
What to expect
Always between getMinPoolFeePer10k() and getMaxPoolFeePer10k().
Example
const feeBp = await readContract("saturnpools", "getPoolFee", [poolId]);
const pct = feeBp / 100; // e.g. 300 → 3%

getPoolScaledLiquidity()

READ
getPoolScaledLiquidity(poolId: number): number

Returns the smaller of the pool's two scaled reserves — a cheap "depth" metric for ranking pools.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — min(reserveA, reserveB) in scaled units.
What to expect
Returns 0 if either reserve is zero (drained or uninitialized).
Example
const depth = await readContract("saturnpools", "getPoolScaledLiquidity", [poolId]);

getPoolCampaignLockCount()

READ
getPoolCampaignLockCount(poolId: number): number

How many active reward campaigns currently reference this pool. When > 0, the provider can't change the pool fee or remove the pool.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Number of active campaign locks on the pool.
What to expect
Managed entirely by SaturnRewards — you should never see negative values.
Example
const locks = await readContract("saturnpools", "getPoolCampaignLockCount", [poolId]);

getPoolFinancialLockCount()

READ
getPoolFinancialLockCount(poolId: number): number

How many active bond, rental, option, or syndicate positions are holding this pool. While > 0, the pool cannot be removed.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Number of financial-product locks on the pool.
What to expect
Incremented by bonds/rental/options/syndicate when a position opens, decremented on settle/expire/cancel.
Example
const finLocks = await readContract("saturnpools", "getPoolFinancialLockCount", [poolId]);

Pair Index & Listings

getPairPoolCount()

READ
getPairPoolCount(pairKey: string): number

Returns how many pools exist for a given canonical pair key. Use it to iterate pools for a pair with getPairPoolAtIndex().

Parameters
NameTypeDescription
pairKeystringCanonical pair key from getCanonicalPairKey().
Returns
number — Number of pools for the pair.
What to expect
Includes removed pools in the count — always check getPoolActive() afterwards.
Example
const key = await readContract("saturnpools", "getCanonicalPairKey", ["SOUL", "KCAL"]);
const n = await readContract("saturnpools", "getPairPoolCount", [key]);

getPairPoolAtIndex()

READ
getPairPoolAtIndex(lookupKey: string): number

Returns the poolId stored at a given index within a pair's pool list. The lookupKey is the canonical pair key concatenated with "_INDEX".

Parameters
NameTypeDescription
lookupKeystringFormat: "<pairKey>_<index>" — e.g. "KCAL_SOUL_0".
Returns
number — Pool ID at that index (0 if none).
What to expect
Use in a loop from index 0 up to getPairPoolCount()-1.
Example
const key = "KCAL_SOUL";
const count = await readContract("saturnpools", "getPairPoolCount", [key]);
for (let i = 0; i < count; i++) {
  const poolId = await readContract("saturnpools", "getPairPoolAtIndex", [`${key}_${i}`]);
}

getReserveValue()

READ
getReserveValue(tokenSymbol: string): number

Total scaled reserve of a token across every pool in the DEX. Useful for protocol-level stats.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Sum of all scaled reserves of the token.
What to expect
Scaled units — apply scaleDown() for display.
Example
const scaledTotal = await readContract("saturnpools", "getReserveValue", ["SOUL"]);

getCountOfTokensOnList()

READ
getCountOfTokensOnList(): number

Total number of unique token symbols that have ever been added to any pool.

Returns
number — Token count.
What to expect
Monotonically increasing.
Example
const n = await readContract("saturnpools", "getCountOfTokensOnList", []);

getCountOfPairsOnList()

READ
getCountOfPairsOnList(): number

Total number of unique token pairs that have ever had at least one pool.

Returns
number — Pair count.
What to expect
Monotonically increasing.
Example
const n = await readContract("saturnpools", "getCountOfPairsOnList", []);

getTokensInDEXList()

READ
getTokensInDEXList(): string*

Generator-style method that yields every token symbol registered in the DEX. Phantasma exposes this as an enumerable script call.

Returns
string* — Iterable of token symbols.
What to expect
Order matches insertion order — tokens are appended as new pairs are created.
Example
// Phantasma invokeRawScript returns all yielded values in a single list
const script = ScriptBuilder
  .begin()
  .callContract("saturnpools", "getTokensInDEXList", [])
  .endScript();
const { decoded } = await api.invokeRawScript("main", script);

getPairsInDEXList()

READ
getPairsInDEXList(): string*

Generator yielding every canonical pair key currently indexed.

Returns
string* — Iterable of canonical pair keys.
What to expect
Safe to enumerate even on large DEXes — returns at most a few hundred keys.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnpools", "getPairsInDEXList", [])
  .endScript();

getAllPoolIds()

READ
getAllPoolIds(): number*

Generator yielding every poolId that has ever been created, including removed ones. Filter with getPoolActive() if you only want live pools.

Returns
number* — Iterable of pool IDs.
What to expect
Useful for building an explorer view. For per-pair scans prefer getPairPoolAtIndex.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnpools", "getAllPoolIds", [])
  .endScript();

getContractTokenBalanceEach()

READ
getContractTokenBalanceEach(tokenSymbol: string): number

Raw on-chain token balance held by the SaturnPools contract itself — the custodial balance backing every pool's reserves.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Raw balance (not scaled).
What to expect
Used for accounting/debug. Under normal operation this should equal scaleDown(getReserveValue(symbol), symbol).
Example
const balance = await readContract("saturnpools", "getContractTokenBalanceEach", ["SOUL"]);

Pool Totals

getNextPoolId()

READ
getNextPoolId(): number

The poolId that will be assigned to the next pool created.

Returns
number — Next pool ID (starts at 1).
What to expect
Total pools created so far = getNextPoolId() - 1.
Example
const next = await readContract("saturnpools", "getNextPoolId", []);

getTotalPoolCount()

READ
getTotalPoolCount(): number

Total number of pools ever created in the protocol, including removed ones.

Returns
number — Total pool count.
What to expect
Equivalent to allPoolIds.count(). Monotonically increasing.
Example
const total = await readContract("saturnpools", "getTotalPoolCount", []);

Provider Actions

updatePoolFee()

WRITE
updatePoolFee(from: address, poolId: number, newFeePer10k: number)

Lets the pool provider change their pool's per-swap fee. The new fee must lie inside the protocol range (30–3000 basis points by default) and the pool must not be locked in any reward campaign.

Parameters
NameTypeDescription
fromaddressProvider wallet (must be witness).
poolIdnumberPool to update.
newFeePer10knumberNew fee in basis points (out of 10,000).
Returns
void — Success = fee stored.
What to expect
Reverts with: "Not authorized to change fee" (not provider), "Pool locked in campaign", "Fee too low/high", or "Pool not active".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 1500)
  .callContract("saturnpools", "updatePoolFee", [from, poolId, 250]) // 2.5%
  .spendGas(from)
  .endScript();
// sign with wallet, broadcast

pawnPool()

WRITE
pawnPool(from: address, poolId: number)

Marks the pool as pawned. A pawned pool continues to earn and swap normally, but the provider cannot transfer or remove it — useful as collateral or a commitment signal for external products.

Parameters
NameTypeDescription
fromaddressProvider wallet (must be witness).
poolIdnumberPool to pawn.
Returns
void — Success = pool marked pawned.
What to expect
Reverts if caller is not the provider, pool is not active, or pool is already pawned.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 1500)
  .callContract("saturnpools", "pawnPool", [from, poolId])
  .spendGas(from)
  .endScript();

unpawnPool()

WRITE
unpawnPool(from: address, poolId: number)

Clears the pawned flag on a pool. Either the pool provider or the protocol admin can call this.

Parameters
NameTypeDescription
fromaddressCaller wallet (provider or admin).
poolIdnumberPool to unpawn.
Returns
void — Success = pool marked free.
What to expect
Reverts if the pool is not pawned, or the caller is neither provider nor admin witness.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 1500)
  .callContract("saturnpools", "unpawnPool", [from, poolId])
  .spendGas(from)
  .endScript();
Core DEX · Contract #3

SaturnLiquidity

saturnliquidity

The single entry point for creating a new pool, adding liquidity to your own pool, and removing a pool entirely. All three methods charge a SOUL-denominated storage fee (configured by SaturnAdmin), transfer tokens to custodial storage inside the protocol, and coordinate with SaturnPools and the SATURN NFT contract. Every method acquires a reentrancy guard and requires the caller to be a wallet witness.

Pool Lifecycle

createPool()

WRITE
createPool(from: address, amountToken0: number, amountToken1: number, token0Symbol: string, token1Symbol: string, customFeePer10k: number)

Creates a brand new pool for a token pair. The caller sets the starting reserves and chooses the per-swap fee rate (in basis points — must fall inside the protocol range). On success the pool is registered, the SATURN NFT certificate for this pool is minted to the caller, and the caller's tokens plus a SOUL storage fee are transferred into protocol custody.

Parameters
NameTypeDescription
fromaddressCreator wallet (must be witness).
amountToken0numberRaw amount of the first token to seed.
amountToken1numberRaw amount of the second token to seed.
token0SymbolstringSymbol of the first token.
token1SymbolstringSymbol of the second token.
customFeePer10knumberPer-swap fee rate in basis points (30–3000 by default).
Returns
void — Success = pool is created and NFT certificate minted to from.
What to expect
Common reverts: "Same token", "Token does not exist", "Storage fee required" (needs getSOULfeeCreatePool + gas), "token0/1 needs at least N raw units" (below min pool size), "Fee too low/high". Pre-check minimums with SaturnRouter.getMinRawForPoolCreation() so the UI can clamp user inputs.
Example
// Create a 0.3% fee SOUL/KCAL pool
const soulFee = await readContract("saturnadmin", "getSOULfeeCreatePool", []);
// User must hold: amount0 + amount1 + soulFee + gas buffer

const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 3000)
  .callContract("saturnliquidity", "createPool",
    [from, 10000000000, 10000000000, "SOUL", "KCAL", 30])
  .spendGas(from)
  .endScript();
// sign & broadcast; read resulting poolId from events / getNextPoolId()

addLiquidity()

WRITE
addLiquidity(from: address, poolId: number, amountTokenA: number, maxAmountTokenB: number)

Adds proportional liquidity to an existing pool. You specify exactly how much of token A you want to add; the contract calculates the matching amount of token B based on the current reserve ratio and deposits both. maxAmountTokenB is your slippage cap — the call reverts if the required B amount exceeds it. Only the pool provider can add liquidity to their own pool.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberThe pool to deposit into.
amountTokenAnumberRaw amount of token A to add.
maxAmountTokenBnumberMaximum raw amount of token B you're willing to add.
Returns
void — Success = reserves increased and storage fee paid.
What to expect
Reverts on: "Only pool provider can add liquidity", "Pool is pawned - cannot modify", "Exceeds max: N" (slippage cap hit), "Rounds to zero" (amountTokenA too small), or "Min N raw units for TOKEN". Compute the expected B amount off-chain from current reserves before submitting.
Example
// Read reserves to project the required B amount
const resA = await readContract("saturnpools", "getPoolReserveA", [poolId]);
const resB = await readContract("saturnpools", "getPoolReserveB", [poolId]);
const tokenA = await readContract("saturnpools", "getPoolTokenA", [poolId]);
const tokenB = await readContract("saturnpools", "getPoolTokenB", [poolId]);

const scaledAddA = await readContract("saturnpools", "scaleUp", [amountA, tokenA]);
const requiredBraw = await readContract("saturnpools", "scaleDown",
  [(scaledAddA * resB) / resA, tokenB]);
const maxB = Math.floor(requiredBraw * 1.01); // 1% slippage

const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2000)
  .callContract("saturnliquidity", "addLiquidity", [from, poolId, amountA, maxB])
  .spendGas(from)
  .endScript();

removePool()

WRITE
removePool(from: address, poolId: number)

Removes a pool entirely. All scaled reserves are paid out to the provider (converted back to raw units), any pending provider fees are auto-claimed, the pool is deactivated, and the SATURN NFT certificate is burned. The pool cannot be removed while it is pawned, locked in a reward campaign, or held by any active bond / rental / option / syndicate.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberPool to remove.
Returns
void — Success = pool marked inactive and tokens returned.
What to expect
Reverts on: "Only pool provider", "Pool is pawned", "Pool locked in reward campaign", "Pool has active financial products", or "Truncation N% on TOKEN" (scale-down rounding would lose more than getMaxTruncationPercent of a reserve). Check all locks via SaturnPools getters first to give users a clear error before they sign.
Example
// Pre-flight checks
const pawned = await readContract("saturnpools", "getPoolPawned", [poolId]);
const campLocks = await readContract("saturnpools", "getPoolCampaignLockCount", [poolId]);
const finLocks = await readContract("saturnpools", "getPoolFinancialLockCount", [poolId]);
if (pawned || campLocks || finLocks) throw new Error("Pool not removable");

const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 3000)
  .callContract("saturnliquidity", "removePool", [from, poolId])
  .spendGas(from)
  .endScript();
Core DEX · Contract #4

SaturnSwap

saturnswap

The hot path of the DEX. A constant-product AMM that reads pool reserves and the pool's custom fee rate, splits the fee between reinvestment (stays in the pool), the provider (accrued in the FeeVault), and the admin (paid immediately), and sends the output to the swapper. Exposes one public write — swap() — plus a helper to retrieve this contract's address. The internal-only swapFromContract() is reserved for limit orders, flash arbitrage, and agent vaults and is not callable directly.

Swapping

swap()

WRITE
swap(from: address, poolId: number, amountIn: number, tokenIn: string, tokenOut: string, minAmountOut: number): number

Executes a swap against a specific pool. The caller sends amountIn of tokenIn; the engine runs the constant-product formula (after the pool fee, provider split, and admin split are taken), then transfers tokenOut to the caller. Set minAmountOut to protect against slippage (pass 0 to disable the check). Always query SaturnRouter first to find the best pool for a pair — passing a sub-optimal poolId here won't revert but will give you a worse rate.

Parameters
NameTypeDescription
fromaddressSwapper wallet (must be witness).
poolIdnumberThe pool to swap against.
amountInnumberRaw amount of tokenIn to send.
tokenInstringSymbol of the token being sold.
tokenOutstringSymbol of the token being bought.
minAmountOutnumberMinimum acceptable raw amount of tokenOut. 0 = no slippage check.
Returns
number — Raw amount of tokenOut actually received by the caller.
What to expect
Reverts on: "Pool not active", "Token pair mismatch" (tokens don't belong to this pool), "Below minimum swap" (use SaturnRouter.getMinRawForSwap first), "Insufficient liquidity", "Slippage exceeded", "Admin fee rounds to zero" (amount too small to pay a 1-unit admin fee), or "Cannot drain pool".
Example
// 1) Find the best pool for SOUL→KCAL
const best = await readContract("saturnrouter", "getBestPoolForSwap", ["SOUL", "KCAL", amountIn]);
// best is an object or string containing the poolId and expected output

// 2) Compute a slippage-protected minimum
const minOut = Math.floor(expectedOut * 0.99); // 1% slippage

// 3) Submit the swap
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2500)
  .callContract("saturnswap", "swap", [from, poolId, amountIn, "SOUL", "KCAL", minOut])
  .spendGas(from)
  .endScript();

Introspection

getContractAddress()

READ
getContractAddress(): address

Returns the on-chain address of the SaturnSwap contract. Useful if your frontend needs to display token balances held by the swap engine (for example to debug settled-but-unclaimed fees), or if you're integrating another contract that must transfer tokens here before calling swapFromContract().

Returns
address — The SaturnSwap contract's address.
What to expect
Never reverts. The address is fixed once the protocol is deployed.
Example
const swapAddr = await readContract("saturnswap", "getContractAddress", []);
const balance = await fetchTokenBalance(swapAddr, "SOUL");
Core DEX · Contract #5

SaturnFees

saturnfees

Holds the provider's share of every swap fee until it is claimed. Each pool has two running balances — one per token in the pair — scaled to the protocol's internal precision. The provider can claim both sides at once with claimProviderFees(). When a pool is sold via a bond or rented via a rental, fee claiming is automatically redirected; the provider can see this with getFeeRedirectActive().

Claiming

claimProviderFees()

WRITE
claimProviderFees(from: address, poolId: number)

The pool provider calls this to sweep all accumulated provider fees for their pool — both tokens at once. The amounts are scaled down to raw units before being transferred out. The call reverts if the pool currently has an active fee redirect (meaning a bond or rental owns the fee stream right now).

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberPool whose fees to claim.
Returns
void — Success = both token balances zeroed and transferred to the provider.
What to expect
Reverts on: "Only pool provider", "Fees redirected by active bond or rental". Zero-balance claims are a no-op (no revert) but still cost gas — query getProviderClaimable() first to decide whether to submit.
Example
// Check claimable on both sides before claiming
const tokenA = await readContract("saturnpools", "getPoolTokenA", [poolId]);
const tokenB = await readContract("saturnpools", "getPoolTokenB", [poolId]);
const pA = await readContract("saturnfees", "getProviderClaimable", [poolId, tokenA]);
const pB = await readContract("saturnfees", "getProviderClaimable", [poolId, tokenB]);

if (pA > 0 || pB > 0) {
  const tx = ScriptBuilder
    .begin()
    .allowGas(from, Address.Null, 100000, 1500)
    .callContract("saturnfees", "claimProviderFees", [from, poolId])
    .spendGas(from)
    .endScript();
}

Views

getProviderClaimable()

READ
getProviderClaimable(poolId: number, tokenSymbol: string): number

Returns the scaled amount of a specific token currently claimable by the pool's provider. Call this once for tokenA and once for tokenB to show a complete balance. Apply SaturnPools.scaleDown() before showing the number to users.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
tokenSymbolstringEither token in the pair.
Returns
number — Scaled pending fee amount for that token.
What to expect
Returns 0 if no fees are pending, or if the wrong token symbol is passed. Never reverts.
Example
const scaled = await readContract("saturnfees", "getProviderClaimable", [poolId, "SOUL"]);
const raw = await readContract("saturnpools", "scaleDown", [scaled, "SOUL"]);

getFeeRedirectActive()

READ
getFeeRedirectActive(poolId: number): number

Returns 1 if the pool currently has a fee redirect active (the fee stream is owned by a bond or rental), 0 otherwise. Use this to disable the "Claim Fees" button in your UI and show the right explanation to the provider.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = fees redirected, 0 = free to claim.
What to expect
Redirects are set by SaturnBonds when a bond is purchased and by SaturnRental when a rental starts; they're cleared on settlement / end.
Example
const redirected = await readContract("saturnfees", "getFeeRedirectActive", [poolId]);
if (redirected === 1) {
  // UI: "Fees are currently being earned by a bond/rental holder"
}
Core DEX · Contract #6

SATURN

SATURN

The SATURN token is a non-fungible certificate representing ownership of a pool. One unique NFT is minted to the provider when they call SaturnLiquidity.createPool() and burned when they call removePool(). Transferring the NFT transfers pool ownership — the new holder earns the provider fee split and can close the pool. Minting and burning are handled automatically by SaturnLiquidity and are not callable directly; the public methods below are the reads your frontend will need.

Views

getTotalNftMinted()

READ
getTotalNftMinted(): number

Total number of SATURN pool NFTs currently in circulation. Equivalent to the total number of active pools (burned NFTs are not counted).

Returns
number — Live SATURN NFT count.
What to expect
Increases by one on every createPool() and decreases by one on every removePool(). Useful for a protocol dashboard.
Example
const activePools = await readContract("SATURN", "getTotalNftMinted", []);

getUserPools()

READ
getUserPools(from: address): number*

Generator yielding every SATURN NFT token ID currently owned by the given address. Each token ID maps to a specific pool — read the ROM metadata of each NFT (via Phantasma's NFT API) to get the associated poolId, token pair, and display name.

Parameters
NameTypeDescription
fromaddressThe wallet to inspect.
Returns
number* — Iterable of SATURN NFT token IDs held by the address.
What to expect
Returns nothing for wallets that don't own any pools. Use this to populate a "My Pools" dashboard — resolve each token ID to a poolId by reading the NFT's ROM.poolId field.
Example
const script = ScriptBuilder
  .begin()
  .callContract("SATURN", "getUserPools", [userAddress])
  .endScript();
const { decoded } = await api.invokeRawScript("main", script);
// For each returned nftId, fetch the NFT metadata to get poolId:
for (const nftId of decoded) {
  const nft = await api.getNFT("SATURN", nftId);
  console.log("Pool #" + nft.properties.poolId, nft.properties.name);
}
Core DEX · Contract #7

SaturnRewards

saturnrewards

Anyone can fund a reward campaign for a specific token pair. Pool providers opt in by enrolling; at enrollment time the pool's scaled liquidity is snapshot and used to compute a proportional share of the reward pot at the end. Enrolled pools are locked (their fee cannot be changed and they cannot be removed) until the provider claims or withdraws. The campaign creator can reclaim any unclaimed rewards after a 30-day grace period following the end time.

Campaign Lifecycle

createCampaign()

WRITE
createCampaign(from: address, tokenA: string, tokenB: string, rewardToken: string, rewardAmount: number, durationSeconds: number)

Creates a new reward campaign targeting a specific token pair. The full rewardAmount of rewardToken is transferred up front from the creator's wallet into the contract, and will be distributed proportionally to pools that enroll during the campaign window.

Parameters
NameTypeDescription
fromaddressCampaign creator wallet (must be witness).
tokenAstringFirst token in the target pair.
tokenBstringSecond token in the target pair.
rewardTokenstringToken paid out as the reward.
rewardAmountnumberTotal raw reward amount to escrow.
durationSecondsnumberCampaign length in seconds. Min 86400 (1 day), max 31536000 (1 year).
Returns
void — Success = campaign stored and reward token deposited.
What to expect
Reverts with: "No pools exist for this pair", "Reward must be > 0", "Min duration: 1 day", "Max duration: 1 year", or "Insufficient reward token balance".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2500)
  .callContract("saturnrewards", "createCampaign",
    [from, "SOUL", "KCAL", "GHOST", 1000000000, 7 * 86400]) // 7-day GHOST campaign
  .spendGas(from)
  .endScript();

enrollInCampaign()

WRITE
enrollInCampaign(from: address, poolId: number, campaignId: number)

Called by a pool provider to enroll their pool in an active campaign. The pool's current scaled liquidity is snapshot and added to the campaign's total locked liquidity — the provider's final reward is proportional to this snapshot, not to the pool's future depth. Enrollment locks the pool (cannot change fee, cannot remove) until the reward is claimed or withdrawn.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberPool to enroll.
campaignIdnumberCampaign to enroll into.
Returns
void — Success = pool enrolled and campaign lock incremented.
What to expect
Reverts on: "Only pool provider", "Campaign already ended", "Pool pair does not match campaign pair", "Already enrolled", "Pool has no liquidity", or "Campaign not active".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2000)
  .callContract("saturnrewards", "enrollInCampaign", [from, poolId, campaignId])
  .spendGas(from)
  .endScript();

claimCampaignReward()

WRITE
claimCampaignReward(from: address, poolId: number, campaignId: number)

Called after the campaign's end time. Transfers the provider's proportional share of the reward pot (snapshotLiquidity / totalLockedLiquidity × totalReward) and removes the campaign lock from the pool. Can only be called once per enrollment.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberEnrolled pool.
campaignIdnumberCampaign being claimed.
Returns
void — Success = reward transferred and pool unlocked.
What to expect
Reverts on: "Only pool provider", "Not enrolled", "Already claimed", "Campaign not ended yet", or "Reward rounds to zero". Preview the expected reward with getExpectedReward() before submitting.
Example
const expected = await readContract("saturnrewards", "getExpectedReward", [poolId, campaignId]);
if (expected > 0) {
  const tx = ScriptBuilder
    .begin()
    .allowGas(from, Address.Null, 100000, 2000)
    .callContract("saturnrewards", "claimCampaignReward", [from, poolId, campaignId])
    .spendGas(from)
    .endScript();
}

withdrawFromCampaign()

WRITE
withdrawFromCampaign(from: address, poolId: number, campaignId: number)

Early exit for a provider who wants out before the campaign ends. Forfeits the pool's reward share — the forfeited amount is absorbed by the remaining participants — and unlocks the pool immediately.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberEnrolled pool.
campaignIdnumberCampaign to withdraw from.
Returns
void — Success = enrollment cleared and pool unlocked.
What to expect
Reverts on: "Only pool provider", "Not enrolled", or "Already claimed". Use this only if the provider really wants to give up their share.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2000)
  .callContract("saturnrewards", "withdrawFromCampaign", [from, poolId, campaignId])
  .spendGas(from)
  .endScript();

endCampaign()

WRITE
endCampaign(from: address, campaignId: number)

Called by the campaign creator at least 30 days after the end time. Marks the campaign inactive and returns any unclaimed reward tokens to the creator. The grace period gives providers plenty of time to claim before the creator can reclaim anything.

Parameters
NameTypeDescription
fromaddressOriginal campaign creator wallet (must be witness).
campaignIdnumberCampaign to end.
Returns
void — Success = campaign closed and unclaimed tokens returned.
What to expect
Reverts on: "Only campaign creator", "Campaign not active", or "Wait 30 days after end for providers to claim".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2000)
  .callContract("saturnrewards", "endCampaign", [from, campaignId])
  .spendGas(from)
  .endScript();

Campaign Views

getCampaignInfo()

READ
getCampaignInfo(campaignId: number): string

Returns a single packed string with every top-level field of a campaign. Cheap to render — one round-trip instead of eight. Parse client-side by splitting on underscores.

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
string — Format: "pair:<key>_reward:<token>_total:<N>_claimed:<N>_start:<ts>_end:<ts>_active:<0|1>_locked:<N>".
What to expect
Suitable for list views. For numeric math prefer the single-value getters.
Example
const info = await readContract("saturnrewards", "getCampaignInfo", [campaignId]);
// e.g. "pair:KCAL_SOUL_reward:GHOST_total:1000000000_claimed:0_start:1700000000_end:1700604800_active:1_locked:0"

getCampaignActive()

READ
getCampaignActive(campaignId: number): number

Returns 1 if the campaign is still active, 0 if it has been ended by the creator.

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
number — 1 = active, 0 = ended.
What to expect
"Active" refers to the campaign's lifecycle state, not whether time has expired — the creator must explicitly call endCampaign().
Example
const active = await readContract("saturnrewards", "getCampaignActive", [campaignId]);

getCampaignRewardToken()

READ
getCampaignRewardToken(campaignId: number): string

Symbol of the token being distributed as the campaign reward.

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
string — Reward token symbol.
What to expect
Set at creation time and immutable thereafter.
Example
const token = await readContract("saturnrewards", "getCampaignRewardToken", [campaignId]);

getCampaignRewardTotal()

READ
getCampaignRewardTotal(campaignId: number): number

Total reward amount that was escrowed at creation (raw units).

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
number — Raw total reward amount.
What to expect
Constant once the campaign exists.
Example
const total = await readContract("saturnrewards", "getCampaignRewardTotal", [campaignId]);

getCampaignTotalLocked()

READ
getCampaignTotalLocked(campaignId: number): number

Sum of the snapshot liquidity of every pool currently enrolled in the campaign.

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
number — Scaled sum of enrolled liquidity.
What to expect
Grows as pools enroll, shrinks when pools withdraw early. Combine with getPoolCampaignLiquidity() to compute share percentages.
Example
const totalLiq = await readContract("saturnrewards", "getCampaignTotalLocked", [campaignId]);

getCampaignEndTime()

READ
getCampaignEndTime(campaignId: number): number

Unix timestamp (seconds) at which the campaign ends and providers can begin claiming.

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
number — End timestamp.
What to expect
Set at creation = startTime + durationSeconds. Immutable.
Example
const endTs = await readContract("saturnrewards", "getCampaignEndTime", [campaignId]);
const msLeft = Math.max(0, endTs * 1000 - Date.now());

getNextCampaignId()

READ
getNextCampaignId(): number

The ID that will be assigned to the next campaign created.

Returns
number — Next campaign ID (starts at 1).
What to expect
Total campaigns so far = getNextCampaignId() - 1.
Example
const nextId = await readContract("saturnrewards", "getNextCampaignId", []);

getAllCampaignIds()

READ
getAllCampaignIds(): number*

Generator yielding every campaignId ever created, including ended ones.

Returns
number* — Iterable of campaign IDs.
What to expect
Combine with getCampaignActive() to filter to running campaigns only.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnrewards", "getAllCampaignIds", [])
  .endScript();

Pool Enrollment Views

getPoolEnrolledInCampaign()

READ
getPoolEnrolledInCampaign(poolId: number, campaignId: number): number

Returns 1 if the pool is currently enrolled in the given campaign, 0 otherwise.

Parameters
NameTypeDescription
poolIdnumberPool ID.
campaignIdnumberCampaign ID.
Returns
number — 1 = enrolled, 0 = not enrolled.
What to expect
Cleared to 0 when the provider claims or withdraws.
Example
const enrolled = await readContract("saturnrewards", "getPoolEnrolledInCampaign", [poolId, campaignId]);

getPoolCampaignLiquidity()

READ
getPoolCampaignLiquidity(poolId: number, campaignId: number): number

Returns the snapshot liquidity this pool had at the moment it enrolled. This is the weight used in the proportional reward calculation — it does NOT update as the pool's reserves change later.

Parameters
NameTypeDescription
poolIdnumberPool ID.
campaignIdnumberCampaign ID.
Returns
number — Scaled liquidity at enrollment time.
What to expect
Zero if the pool is not enrolled.
Example
const snap = await readContract("saturnrewards", "getPoolCampaignLiquidity", [poolId, campaignId]);

getPoolCampaignClaimed()

READ
getPoolCampaignClaimed(poolId: number, campaignId: number): number

Returns the raw reward amount already claimed for this (pool, campaign) pair. Zero until the provider calls claimCampaignReward().

Parameters
NameTypeDescription
poolIdnumberPool ID.
campaignIdnumberCampaign ID.
Returns
number — Amount already claimed.
What to expect
Non-zero = already claimed. Use to hide the claim button in the UI.
Example
const claimed = await readContract("saturnrewards", "getPoolCampaignClaimed", [poolId, campaignId]);

getExpectedReward()

READ
getExpectedReward(poolId: number, campaignId: number): number

Computes the raw reward amount the provider will receive if they claim right now, given the current totalLocked. The number will drift if other pools enroll or withdraw before the campaign ends — always requery just before submitting a claim.

Parameters
NameTypeDescription
poolIdnumberEnrolled pool.
campaignIdnumberCampaign the pool is enrolled in.
Returns
number — Projected reward in raw units (0 if not enrolled).
What to expect
Returns 0 if the pool isn't enrolled or if totalLocked is zero.
Example
const projected = await readContract("saturnrewards", "getExpectedReward", [poolId, campaignId]);
Core DEX · Contract #8

SaturnRouter

saturnrouter

The recommended entry point for any frontend or SDK. SaturnRouter exposes read-only helpers that pick the best pool for a swap, compute the raw minimums a user must meet before they submit a transaction, return aggregated pool info in a single call, and re-export the most useful values from SaturnAdmin and SaturnPools so your app only needs to know one contract name. Everything here is pure view — no witness, no gas, no side effects.

Swap Routing

getBestPoolForSwap()

READ
getBestPoolForSwap(tokenIn: string, tokenOut: string, amountIn: number): number

Iterates every active pool for the token pair and returns the poolId that would give the highest net output after that pool's fee. This is the first call your swap flow should make — pass the result into SaturnSwap.swap().

Parameters
NameTypeDescription
tokenInstringSymbol of the token being sold.
tokenOutstringSymbol of the token being bought.
amountInnumberRaw amount the user wants to sell.
Returns
number — Pool ID with the best output, or 0 if no viable pool exists.
What to expect
Returns 0 when no pools exist for the pair, or every candidate pool has zero reserves. Safe to run on large pair sets (loops in O(n) over pools for the pair).
Example
const poolId = await readContract("saturnrouter", "getBestPoolForSwap",
  ["SOUL", "KCAL", 10000000000]);
if (poolId === 0) throw new Error("No liquidity available");

getPoolPrice()

READ
getPoolPrice(poolId: number, tokenIn: string): number

Returns the current spot price of tokenIn expressed in the other token of the pool. The result is scaled by 1e8 so you can divide by 100_000_000 on the client to get a floating-point ratio.

Parameters
NameTypeDescription
poolIdnumberPool to quote.
tokenInstringToken you want the price of.
Returns
number — Price × 1e8 (0 if the relevant reserve is empty).
What to expect
No slippage adjustment — this is the marginal price before any trade.
Example
const scaled = await readContract("saturnrouter", "getPoolPrice", [poolId, "SOUL"]);
const price = scaled / 1e8; // display price of SOUL in the other token

getPoolCountForPair()

READ
getPoolCountForPair(tokenA: string, tokenB: string): number

Shortcut for getCanonicalPairKey → getPairPoolCount. Returns the number of pools (active or removed) that exist for a pair.

Parameters
NameTypeDescription
tokenAstringFirst token symbol.
tokenBstringSecond token symbol.
Returns
number — Number of pools for the pair.
What to expect
Includes removed pools — check getPoolActive() when iterating.
Example
const n = await readContract("saturnrouter", "getPoolCountForPair", ["SOUL", "KCAL"]);

getPoolIdForPairAtIndex()

READ
getPoolIdForPairAtIndex(tokenA: string, tokenB: string, index: number): number

Returns the poolId at a given index in the pair's pool list. Use together with getPoolCountForPair() to iterate every pool for a pair without building a canonical key yourself.

Parameters
NameTypeDescription
tokenAstringFirst token symbol.
tokenBstringSecond token symbol.
indexnumberZero-based index.
Returns
number — Pool ID at that index (0 if out of range).
What to expect
Indices are stable — removed pools keep their slot.
Example
const count = await readContract("saturnrouter", "getPoolCountForPair", ["SOUL", "KCAL"]);
for (let i = 0; i < count; i++) {
  const pid = await readContract("saturnrouter", "getPoolIdForPairAtIndex", ["SOUL", "KCAL", i]);
}

Aggregated Views

getPoolFullInfo()

READ
getPoolFullInfo(poolId: number): string

One-call view that returns every field a frontend typically needs to render a pool row: tokens, scaled reserves, fee rate, active flag, pawned flag, and campaign lock count, packed into a single underscore-delimited string.

Parameters
NameTypeDescription
poolIdnumberPool ID.
Returns
string — Format: "tokenA:<s>_tokenB:<s>_resA:<n>_resB:<n>_fee:<n>_active:<0|1>_pawned:<0|1>_locks:<n>".
What to expect
Much cheaper than eight individual getPool* round trips if you render many pools at once.
Example
const info = await readContract("saturnrouter", "getPoolFullInfo", [poolId]);
const parts = Object.fromEntries(
  info.split("_").map(p => { const [k, ...v] = p.split(":"); return [k, v.join(":")]; })
);

getProviderClaimable()

READ
getProviderClaimable(poolId: number, tokenSymbol: string): number

Convenience re-export of SaturnFees.getProviderClaimable() so your app can hit a single contract.

Parameters
NameTypeDescription
poolIdnumberPool ID.
tokenSymbolstringEither token in the pair.
Returns
number — Scaled pending provider fees for that token.
What to expect
Same return semantics as SaturnFees.getProviderClaimable.
Example
const pending = await readContract("saturnrouter", "getProviderClaimable", [poolId, "SOUL"]);

getPoolScaledLiquidity()

READ
getPoolScaledLiquidity(poolId: number): number

Re-export of SaturnPools.getPoolScaledLiquidity — cheap "depth" metric for ranking pools.

Parameters
NameTypeDescription
poolIdnumberPool ID.
Returns
number — min(reserveA, reserveB) in scaled units.
What to expect
0 if either reserve is zero.
Example
const depth = await readContract("saturnrouter", "getPoolScaledLiquidity", [poolId]);

Raw Minimums

getMinRawForPoolCreation()

READ
getMinRawForPoolCreation(tokenSymbol: string): number

Returns the minimum raw amount of a token a provider must supply when calling createPool(). Already applies the token's scale factor and the absolute floor from SaturnAdmin.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to quote.
Returns
number — Minimum raw amount for pool creation.
What to expect
Use this to pre-populate the pool-creation form and reject bad inputs before the user signs.
Example
const minA = await readContract("saturnrouter", "getMinRawForPoolCreation", ["SOUL"]);
const minB = await readContract("saturnrouter", "getMinRawForPoolCreation", ["KCAL"]);

getMinRawForSwap()

READ
getMinRawForSwap(tokenSymbol: string): number

Returns the minimum raw amount a user must send as the input of a swap. Enforces both the scaled-units minimum and the absolute floor.

Parameters
NameTypeDescription
tokenSymbolstringInput token symbol.
Returns
number — Minimum raw amount per swap.
What to expect
Show this as the "minimum swap" hint next to the input field.
Example
const minSwap = await readContract("saturnrouter", "getMinRawForSwap", ["SOUL"]);

getMinRawForAddLiquidity()

READ
getMinRawForAddLiquidity(tokenSymbol: string): number

Returns the minimum raw amount of token A a user must add in a single addLiquidity() call.

Parameters
NameTypeDescription
tokenSymbolstringToken A symbol.
Returns
number — Minimum raw amount per add-liquidity.
What to expect
Token B's required amount is derived proportionally from current reserves — no minimum is enforced on B directly.
Example
const minAdd = await readContract("saturnrouter", "getMinRawForAddLiquidity", ["SOUL"]);

Protocol Config Passthroughs

getScaleFactor()

READ
getScaleFactor(tokenSymbol: string): number

Re-export of SaturnPools.getScaleFactor — the multiplier used to go from raw units to scaled units.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Scale factor (0 = never seen).
What to expect
Same as SaturnPools.getScaleFactor.
Example
const f = await readContract("saturnrouter", "getScaleFactor", ["SOUL"]);

getTargetDecimals()

READ
getTargetDecimals(): number

Re-export of SaturnAdmin.getTargetDecimals (8 by default).

Returns
number — Internal target decimals.
What to expect
Never changes in practice.
Example
const dec = await readContract("saturnrouter", "getTargetDecimals", []);

getMaxTruncationPercent()

READ
getMaxTruncationPercent(): number

Re-export of SaturnAdmin.getMaxTruncationPercent (default 10).

Returns
number — Maximum truncation loss allowed on removePool.
What to expect
Use to warn providers when a small-decimal token would push their removal close to the limit.
Example
const maxTrunc = await readContract("saturnrouter", "getMaxTruncationPercent", []);

getFeeSplitRatios()

READ
getFeeSplitRatios(): string

Re-export of SaturnAdmin.getFeeSplitRatios. Returns the full reinvest / provider / admin breakdown in one string.

Returns
string — "reinvest:X_provider:Y_admin:Z".
What to expect
Parts always sum to 100.
Example
const split = await readContract("saturnrouter", "getFeeSplitRatios", []);

getPoolFeeRange()

READ
getPoolFeeRange(): string

Re-export of SaturnAdmin.getPoolFeeRange. Returns both min and max per-pool fee rates packed into one string.

Returns
string — "min:X_max:Y" in basis points.
What to expect
Use to clamp the fee slider in the pool-creation form.
Example
const range = await readContract("saturnrouter", "getPoolFeeRange", []);
Advanced Pools & Capital · Contract #18

SaturnCLPools

saturnclpools

Range-bound AMM that allows liquidity providers to concentrate capital within a chosen price band (priceMin–priceMax), dramatically increasing capital efficiency relative to a full-range pool. Swaps execute the standard xy=k formula but revert the moment the resulting price would exit the declared range — making the pool's behaviour predictable and composable. Build on it to offer narrow-spread stablecoin pairs, pegged-asset vaults, or any strategy that benefits from tighter spreads without the dilution of idle out-of-range reserves.

Pool Lifecycle

createClPool()

WRITE
createClPool(from: address, tokenA: string, tokenB: string, amountA: number, amountB: number, priceMin: number, priceMax: number, feePer10k: number)

Creates a new concentrated liquidity pool seeded with the caller's initial liquidity. The initial price (scaledB / scaledA × 1e8) must fall inside [priceMin, priceMax]; the call reverts otherwise. The pool is immediately active, assigned a monotonically increasing poolId, and indexed under the canonical pair key so router views can discover it. Both tokens must pass the saturnpools symbol validator. Only the creator (from) can later add or remove liquidity.

Parameters
NameTypeDescription
fromaddressWallet that owns and seeds the pool; must be the transaction signer.
tokenAstringSymbol of the first token (e.g. "SOUL").
tokenBstringSymbol of the second token (e.g. "KCAL").
amountAnumberRaw (unscaled) amount of tokenA to deposit.
amountBnumberRaw (unscaled) amount of tokenB to deposit.
priceMinnumberLower bound of the price range (scaled: tokenB per tokenA × 1e8). Must be > 0.
priceMaxnumberUpper bound of the price range. Must exceed priceMin.
feePer10knumberSwap fee in basis points out of 10,000 (e.g. 30 = 0.3%). Must be within protocol min/max.
What to expect
Reverts if tokenA == tokenB, either amount is 0, priceMin >= priceMax, the initial price is outside the range, or feePer10k is outside the protocol-configured min/max (read from saturnadmin). The reentrancy guard is held for the duration; do not call re-entrantly.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnclpools", "createClPool", [
    from, "SOUL", "KCAL",
    1000000000,   // amountA: 10 SOUL (8-decimal example)
    5000000000,   // amountB: 50 KCAL
    40000000,     // priceMin: 0.4 KCAL/SOUL (× 1e8)
    60000000,     // priceMax: 0.6 KCAL/SOUL (× 1e8)
    30            // feePer10k: 0.3%
  ])
  .spendGas(from)
  .endScript();

removeClPool()

WRITE
removeClPool(from: address, poolId: number)

Permanently deactivates a CL pool and returns all reserves plus any unclaimed fees to the pool provider. Only the original creator of the pool may call this. After removal the pool cannot be reactivated; poolId remains in storage with active=0. Any accumulated fees are bundled into the refund — a separate claimClFees call is not required.

Parameters
NameTypeDescription
fromaddressPool provider; must match the address stored at pool creation.
poolIdnumberNumeric ID of the CL pool to remove.
What to expect
Reverts if the pool is already inactive (active != 1) or if from is not the pool provider. Emits ClPoolRemoved with the exact amounts returned for both tokens plus the fee amounts bundled in.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnclpools", "removeClPool", [from, poolId])
  .spendGas(from)
  .endScript();

Liquidity Management

addClLiquidity()

WRITE
addClLiquidity(from: address, poolId: number, amountA: number, maxAmountB: number)

Adds liquidity to an existing active CL pool at the current ratio. The amount of tokenB actually deposited is calculated from the pool's current reserves and the supplied amountA; if that computed amount exceeds maxAmountB the call reverts, giving the caller a slippage guard. Only the pool provider (the original creator) can add liquidity. Both token amounts are transferred from the caller to the liquidity vault.

Parameters
NameTypeDescription
fromaddressPool provider; must be the original pool creator and transaction signer.
poolIdnumberID of the CL pool to supply.
amountAnumberRaw amount of tokenA to add.
maxAmountBnumberMaximum raw amount of tokenB the caller is willing to deposit (slippage cap).
What to expect
Reverts if pool is inactive, from is not the provider, amountA is 0, or the computed tokenB amount exceeds maxAmountB. The exact tokenB amount deposited is logged in the ClLiquidityAdded event.
Example
// Read the current price first to estimate required tokenB
const resA = await readContract("saturnclpools", "getClPoolReserveA", [poolId]);
const resB = await readContract("saturnclpools", "getClPoolReserveB", [poolId]);
const addA = 500000000n;
const estimatedB = (addA * BigInt(resB)) / BigInt(resA);
const maxB = estimatedB + (estimatedB / 100n); // +1% slippage

const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnclpools", "addClLiquidity", [from, poolId, addA, maxB])
  .spendGas(from)
  .endScript();

Swapping

swapClPool()

WRITE
swapClPool(from: address, poolId: number, amountIn: number, tokenIn: string, tokenOut: string, minAmountOut: number): number

Executes a swap against a specific CL pool using the xy=k formula. The fee (feePer10k basis points) is deducted from amountIn before the AMM math runs, and is accumulated in the pool for the provider to claim later. The swap reverts if the resulting price would fall outside the pool's declared [priceMin, priceMax] range — this is the core CL invariant. Returns the actual raw output amount delivered to the caller. Use minAmountOut to guard against slippage.

Parameters
NameTypeDescription
fromaddressTrader; must be the transaction signer.
poolIdnumberID of the CL pool to swap through.
amountInnumberRaw amount of tokenIn to sell.
tokenInstringSymbol of the input token (must be one of the pool's pair).
tokenOutstringSymbol of the output token (the other side of the pair).
minAmountOutnumberMinimum acceptable raw output; reverts if output is below this (slippage guard).
Returns
number — Raw amount of tokenOut received by the caller after fee deduction.
What to expect
Reverts if: pool inactive; tokenIn == tokenOut; token pair does not match pool; either reserve is zero; fee consumes entire input; output rounds to zero; output < minAmountOut; or resulting price would push outside [priceMin, priceMax]. Always quote via getClPoolPrice() and getClPoolInfo() before submitting to set a reasonable minAmountOut.
Example
// Preview the current price, then swap
const price = await readContract("saturnclpools", "getClPoolPrice", [poolId]);
console.log("Current price (scaled 1e8):", price);

const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnclpools", "swapClPool", [
    from, poolId, 100000000, "SOUL", "KCAL", 0
  ])
  .spendGas(from)
  .endScript();

Fee Collection

claimClFees()

WRITE
claimClFees(from: address, poolId: number)

Withdraws all accumulated swap fees from both sides of a CL pool to the pool provider. Fees are denominated in the raw token amounts that were charged at swap time. After claiming, the fee accumulators reset to zero. The pool does not need to be active — fees can be claimed even on an inactive pool (though removeClPool bundles fees into the refund anyway, so a separate claim before removal is optional).

Parameters
NameTypeDescription
fromaddressPool provider; must match the stored provider address.
poolIdnumberID of the CL pool whose fees to collect.
What to expect
Reverts if from is not the pool provider or if both fee accumulators are zero (nothing to claim). Emits ClFeesClaimed with feesA and feesB amounts.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnclpools", "claimClFees", [from, poolId])
  .spendGas(from)
  .endScript();

Pool Views

getContractVersion()

READ
getContractVersion(): string

Returns the current contract version string, useful for verifying on-chain deployment matches your SDK expectations.

Returns
string — Version string, e.g. "saturnclpools-4.2.4".
Example
const version = await readContract("saturnclpools", "getContractVersion", []);

getClPoolInfo()

READ
getClPoolInfo(poolId: number): string

Returns all core pool fields packed into a single underscore-delimited string: "tokenA:X_tokenB:Y_resA:N_resB:N_priceMin:N_priceMax:N_fee:N_active:N". Reserves are in scaled (internal) units. Use this for a single-round-trip refresh of a known pool.

Parameters
NameTypeDescription
poolIdnumberID of the CL pool to inspect.
Returns
string — Packed field string. Parse by splitting on "_" then on ":".
What to expect
Returns the storage values as-is; no validation. Reserves are scaled (internal decimals), not raw user-facing amounts.
Example
const info = await readContract("saturnclpools", "getClPoolInfo", [poolId]);
// "tokenA:SOUL_tokenB:KCAL_resA:1000000000_resB:5000000000_priceMin:40000000_priceMax:60000000_fee:30_active:1"
const fields = Object.fromEntries(info.split("_").map(f => f.split(":")));

getClPoolProvider()

READ
getClPoolProvider(poolId: number): address

Returns the address of the wallet that created and owns the specified CL pool.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
address — Pool provider address.

getClPoolTokenA()

READ
getClPoolTokenA(poolId: number): string

Returns the symbol of the first token in the pool's pair.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
string — Token symbol, e.g. "SOUL".

getClPoolTokenB()

READ
getClPoolTokenB(poolId: number): string

Returns the symbol of the second token in the pool's pair.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
string — Token symbol, e.g. "KCAL".

getClPoolReserveA()

READ
getClPoolReserveA(poolId: number): number

Returns the current scaled reserve of tokenA. Divide by the token's scale factor to get the user-facing amount.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Scaled tokenA reserve (internal units).

getClPoolReserveB()

READ
getClPoolReserveB(poolId: number): number

Returns the current scaled reserve of tokenB.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Scaled tokenB reserve (internal units).

getClPoolPriceMin()

READ
getClPoolPriceMin(poolId: number): number

Returns the lower bound of the pool's active price range. Price is stored as (reserveB / reserveA) × 1e8.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Minimum price (scaled 1e8).

getClPoolPriceMax()

READ
getClPoolPriceMax(poolId: number): number

Returns the upper bound of the pool's active price range.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Maximum price (scaled 1e8).

getClPoolFeePer10k()

READ
getClPoolFeePer10k(poolId: number): number

Returns the pool's swap fee in basis points out of 10,000. For example, 30 means 0.3%.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Fee in basis points (e.g. 30 = 0.3%).

getClPoolActive()

READ
getClPoolActive(poolId: number): number

Returns 1 if the pool is active and accepting swaps/liquidity, 0 if it has been removed.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — 1 = active, 0 = removed/inactive.

getClPoolFeesARaw()

READ
getClPoolFeesARaw(poolId: number): number

Returns the current unclaimed fee accumulator for tokenA, in raw (unscaled) units.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Unclaimed tokenA fees (raw units).

getClPoolFeesBRaw()

READ
getClPoolFeesBRaw(poolId: number): number

Returns the current unclaimed fee accumulator for tokenB, in raw (unscaled) units.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Unclaimed tokenB fees (raw units).

getClPoolPrice()

READ
getClPoolPrice(poolId: number): number

Computes and returns the current spot price of the pool as (reserveB × 1e8) / reserveA. Returns 0 if reserveA is zero (pool drained or not yet funded). Use this to display a live price quote or to estimate slippage before swapping.

Parameters
NameTypeDescription
poolIdnumberCL pool ID.
Returns
number — Current price, scaled by 1e8 (tokenB per tokenA). 0 if pool has no tokenA reserve.
Example
const price = await readContract("saturnclpools", "getClPoolPrice", [poolId]);
const humanPrice = Number(price) / 1e8; // e.g. 0.5 KCAL/SOUL

getNextClPoolId()

READ
getNextClPoolId(): number

Returns the ID that will be assigned to the next CL pool created. Pool IDs are sequential starting from 1.

Returns
number — Next available pool ID.

getClPairPoolCount()

READ
getClPairPoolCount(pairKey: string): number

Returns the number of CL pools that exist for a given canonical pair key (as produced by saturnpools.getCanonicalPairKey). Use this to paginate pair pools before fetching them with getClPairPoolAtIndex.

Parameters
NameTypeDescription
pairKeystringCanonical pair key, e.g. the value returned by saturnpools.getCanonicalPairKey("SOUL", "KCAL").
Returns
number — Number of CL pools for the pair.
Example
const count = await readContract("saturnclpools", "getClPairPoolCount", [pairKey]);
for (let i = 0; i < count; i++) {
  const pid = await readContract("saturnclpools", "getClPairPoolAtIndex", [pairKey + "_" + i]);
}

getClPairPoolAtIndex()

READ
getClPairPoolAtIndex(lookupKey: string): number

Returns the poolId stored at a specific index under a pair key. The lookupKey format is "<pairKey>_<index>", e.g. "KCAL~SOUL_0". Iterate from 0 to getClPairPoolCount(pairKey)-1 to enumerate all pools for a pair.

Parameters
NameTypeDescription
lookupKeystringCompound key formed as pairKey + "_" + index (e.g. "KCAL~SOUL_0").
Returns
number — Pool ID at that index.

getAllClPoolIds()

READ
getAllClPoolIds(): number*

Streams all ever-created CL pool IDs (including inactive ones) as a sequence. Use getActiveClPoolIds() if you only want live pools.

Returns
number* — Sequence of all CL pool IDs (active and inactive).
Example
const allIds = await readContract("saturnclpools", "getAllClPoolIds", []);

getActiveClPoolIds()

READ
getActiveClPoolIds(): number*

Streams the IDs of all currently active (non-removed) CL pools. More efficient for UI discovery than getAllClPoolIds() when removed pools should be hidden.

Returns
number* — Sequence of active CL pool IDs.
Example
const activeIds = await readContract("saturnclpools", "getActiveClPoolIds", []);

getActiveClPoolsData()

READ
getActiveClPoolsData(): string*

Batch view that streams one encoded row per active CL pool — eliminates N round-trips when building a pool-list UI. Each row is pipe-delimited: "poolId|provider|tokenA|tokenB|reserveA|reserveB|priceMin|priceMax|fee|active". Reserves are in scaled internal units.

Returns
string* — Sequence of pipe-delimited pool data rows. One row per active pool.
What to expect
Reserves are scaled (internal) units. Parse with row.split("|").
Example
const rows = await readContract("saturnclpools", "getActiveClPoolsData", []);
// rows is an array of strings, e.g.:
// ["1|S1abc...|SOUL|KCAL|1000000000|5000000000|40000000|60000000|30|1", ...]
const pools = rows.map(row => {
  const [poolId, provider, tokenA, tokenB, resA, resB, priceMin, priceMax, fee, active] = row.split("|");
  return { poolId, provider, tokenA, tokenB, resA, resB, priceMin, priceMax, fee, active };
});
Advanced Pools & Capital · Contract #19

SaturnTWAMM

saturntwamm

Sandwich-resistant streaming swap engine. A user submits a large trade as a stream — "swap N tokens over T seconds" — depositing the full input upfront. The stream is split into time-proportional chunks; anyone (including bots and automation agents) can call executeStreamingChunk to fire the next slice and earn a configurable bounty paid from each chunk's output. The target pool experiences a smooth, low-impact flow rather than a single large-impact trade, collapsing the MEV opportunity. Output accumulates inside the contract and is claimed by the stream owner at any time. Streams can be cancelled early, returning all unspent input and accumulated output in one call.

Stream Management

placeStreamingOrder()

WRITE
placeStreamingOrder(from: address, poolId: number, amountIn: number, durationSeconds: number, tokenIn: string, tokenOut: string, minChunkSeconds: number, minOutputPerChunk: number, bountyPer10k: number)

Opens a new streaming order. The full amountIn is transferred from the caller into the TWAMM contract's escrow immediately. The stream runs for durationSeconds, executing chunks no faster than one per minChunkSeconds. Each chunk executor receives bountyPer10k basis-points of that chunk's output as a bounty; set to 0 to disable executor incentives (only recommended for self-operated bots). minOutputPerChunk is a per-chunk slippage guard — if the market moves severely during the stream the chunk executor's call will revert rather than deliver bad fills.

Parameters
NameTypeDescription
fromaddressStream owner; deposits the input tokens and claims the output. Must be the transaction signer.
poolIdnumberID of the target pool (regular Saturn pool) through which each chunk will be swapped.
amountInnumberTotal raw amount of tokenIn to stream over the duration.
durationSecondsnumberTotal duration of the stream in seconds. Minimum 60, maximum 31,536,000 (1 year).
tokenInstringSymbol of the input token; must be one side of the target pool's pair.
tokenOutstringSymbol of the desired output token; must be the other side of the pool's pair.
minChunkSecondsnumberMinimum seconds that must elapse between chunk executions. At least 60; cannot exceed durationSeconds.
minOutputPerChunknumberMinimum raw output required per chunk; reverts the executeStreamingChunk call if the swap would produce less. Set 0 to disable.
bountyPer10knumberBasis-points share of each chunk's gross output paid to the executor (0–500, i.e. 0–5%).
What to expect
Reverts if amountIn == 0; durationSeconds < 60 or > 31,536,000; minChunkSeconds < 60 or > durationSeconds; bountyPer10k > 500; tokenIn == tokenOut; the target pool is not active; or tokenIn/tokenOut are not both in the pool's pair. The caller must have sufficient tokenIn balance.
Example
// Stream 100 SOUL into KCAL over 1 hour, one chunk per minute, 1% bounty
const amountIn   = 10000000000n; // 100 SOUL (8 decimals)
const duration   = 3600;         // 1 hour
const minChunk   = 60;           // chunks every 60 s minimum
const minOut     = 0;            // no per-chunk floor
const bounty     = 100;          // 1% bounty to executors

const tx = sb.begin()
  .allowGas(from)
  .callContract("saturntwamm", "placeStreamingOrder", [
    from, poolId, amountIn, duration, "SOUL", "KCAL", minChunk, minOut, bounty
  ])
  .spendGas(from)
  .endScript();

cancelStream()

WRITE
cancelStream(from: address, streamId: number)

Cancels an active stream early. Returns all remaining unstreamed input AND any accumulated output that has not yet been claimed — both in a single call. Status is set to 2 (cancelled) and the stream is removed from the active list. Only the stream owner may cancel. Once cancelled a stream cannot be resumed.

Parameters
NameTypeDescription
fromaddressStream owner; must match the address that placed the order.
streamIdnumberID of the stream to cancel.
What to expect
Reverts if from is not the stream owner or if the stream is not active (status != 0). Emits StreamClosed with the refunded input and final accumulated output amounts.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturntwamm", "cancelStream", [from, streamId])
  .spendGas(from)
  .endScript();

Chunk Execution (Bounty Path)

executeStreamingChunk()

WRITE
executeStreamingChunk(from: address, streamId: number)

Executes the next time-proportional chunk of an active stream. Permissionless — any address can call this, not just the stream owner. The chunk size is computed from (totalAmountIn × elapsedSinceStart / duration) minus what has already been streamed, so cumulative integer-rounding error stays at zero; the final chunk settles any residue. The chunk is routed through saturnswap.swapFromContract against the stream's target pool. The executor receives bountyPer10k basis-points of the chunk's gross output, paid immediately; the remainder accumulates for the stream owner to claim. When the last chunk is executed the stream status transitions to 1 (completed) automatically.

Parameters
NameTypeDescription
fromaddressExecutor address; receives the bounty from this chunk. Does not need to be the stream owner.
streamIdnumberID of the stream to advance.
What to expect
Reverts if stream is not active (status != 0); the chunk pacing interval has not elapsed since the last execution; no streamable amount has accumulated yet (can happen with very fine-grained minChunkSeconds early in a long stream); or the swap output is below the stream's minOutputPerChunk. Always check getStreamLastExecutionTime() + getStreamMinChunkSeconds() to know when the next call is valid.
Example
// Bot / agent loop: execute whenever a chunk is ready
const lastExec    = await readContract("saturntwamm", "getStreamLastExecutionTime", [streamId]);
const minInterval = await readContract("saturntwamm", "getStreamMinChunkSeconds", [streamId]);
const nextWindow  = Number(lastExec) + Number(minInterval);

if (Date.now() / 1000 >= nextWindow) {
  const tx = sb.begin()
    .allowGas(executor)
    .callContract("saturntwamm", "executeStreamingChunk", [executor, streamId])
    .spendGas(executor)
    .endScript();
}

Output Claims

claimStreamingOutput()

WRITE
claimStreamingOutput(from: address, streamId: number)

Transfers all accumulated tokenOut from completed chunk swaps to the stream owner. Can be called at any point during or after the stream — mid-stream partial claims are fully supported. Resets the accumulator to zero after payout. The stream does not need to be completed; active, completed, and cancelled streams all allow claims as long as accumulated > 0.

Parameters
NameTypeDescription
fromaddressStream owner; must match the address that placed the order.
streamIdnumberID of the stream from which to claim output.
What to expect
Reverts if from is not the stream owner or if the accumulated output is zero (nothing has been swapped yet, or everything was already claimed).
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturntwamm", "claimStreamingOutput", [from, streamId])
  .spendGas(from)
  .endScript();

Stream Views

getContractVersion()

READ
getContractVersion(): string

Returns the current TWAMM contract version string.

Returns
string — Version string, e.g. "saturntwamm-4.2.1".
Example
const version = await readContract("saturntwamm", "getContractVersion", []);

getStreamInfo()

READ
getStreamInfo(streamId: number): string

Returns all key stream fields as a single underscore-delimited string for a one-round-trip refresh. Format: "pool:N_in:TOKEN_out:TOKEN_total:N_streamed:N_remaining:N_accOut:N_start:N_end:N_status:N". All amounts in raw units; times are Unix timestamps.

Parameters
NameTypeDescription
streamIdnumberStream ID to inspect.
Returns
string — Packed field string. Parse by splitting on "_" then on ":".
What to expect
status: 0 = active, 1 = completed, 2 = cancelled.
Example
const info = await readContract("saturntwamm", "getStreamInfo", [streamId]);
const fields = Object.fromEntries(info.split("_").map(f => f.split(":")));
// fields.status === "0"  → still running

getStreamOwner()

READ
getStreamOwner(streamId: number): address

Returns the address of the wallet that placed the streaming order.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
address — Stream owner address.

getStreamPoolId()

READ
getStreamPoolId(streamId: number): number

Returns the target pool ID that chunks are swapped through.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Target pool ID.

getStreamTokenIn()

READ
getStreamTokenIn(streamId: number): string

Returns the symbol of the input token being streamed.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
string — Input token symbol.

getStreamTokenOut()

READ
getStreamTokenOut(streamId: number): string

Returns the symbol of the output token being accumulated.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
string — Output token symbol.

getStreamAmountInTotal()

READ
getStreamAmountInTotal(streamId: number): number

Returns the total input amount placed when the stream was created.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Original total raw amountIn.

getStreamAmountInRemaining()

READ
getStreamAmountInRemaining(streamId: number): number

Returns how much of the input has not yet been streamed. Use this to compute percentage completion.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Raw input amount still in escrow, not yet swapped.
Example
const total     = await readContract("saturntwamm", "getStreamAmountInTotal",     [streamId]);
const remaining = await readContract("saturntwamm", "getStreamAmountInRemaining", [streamId]);
const pctDone   = ((Number(total) - Number(remaining)) / Number(total) * 100).toFixed(1);

getStreamAmountStreamedSoFar()

READ
getStreamAmountStreamedSoFar(streamId: number): number

Returns the cumulative raw input amount that has been swapped across all executed chunks.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Cumulative swapped input amount (raw units).

getStreamAmountOutAccumulated()

READ
getStreamAmountOutAccumulated(streamId: number): number

Returns the current unclaimed output balance sitting in the TWAMM contract for this stream. Decremented to zero each time claimStreamingOutput is called.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Raw unclaimed tokenOut accumulated so far.

getStreamStartTime()

READ
getStreamStartTime(streamId: number): number

Returns the Unix timestamp when the stream was placed.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Unix start timestamp.

getStreamEndTime()

READ
getStreamEndTime(streamId: number): number

Returns the Unix timestamp at which the stream's duration expires. Chunks can still execute after this time to settle residue.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Unix end timestamp (startTime + durationSeconds).

getStreamLastExecutionTime()

READ
getStreamLastExecutionTime(streamId: number): number

Returns the Unix timestamp of the most recent chunk execution. Add getStreamMinChunkSeconds() to determine when the next chunk becomes executable.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Unix timestamp of last executeStreamingChunk call.
Example
const last    = await readContract("saturntwamm", "getStreamLastExecutionTime", [streamId]);
const minSecs = await readContract("saturntwamm", "getStreamMinChunkSeconds",    [streamId]);
const ready   = Date.now() / 1000 >= Number(last) + Number(minSecs);

getStreamMinChunkSeconds()

READ
getStreamMinChunkSeconds(streamId: number): number

Returns the minimum pacing interval (in seconds) between consecutive chunk executions for this stream.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Minimum seconds between chunks.

getStreamMinOutputPerChunk()

READ
getStreamMinOutputPerChunk(streamId: number): number

Returns the per-chunk minimum output slippage guard set when the stream was placed. Zero means no floor.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Minimum raw tokenOut per chunk (0 = disabled).

getStreamBountyPer10k()

READ
getStreamBountyPer10k(streamId: number): number

Returns the executor bounty rate in basis points out of 10,000. Multiply by chunk output and divide by 10,000 to estimate what an executor earns per call.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Bounty in basis points (0–500).

getStreamStatus()

READ
getStreamStatus(streamId: number): number

Returns the lifecycle status of a stream: 0 = active, 1 = completed (fully streamed), 2 = cancelled.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — 0 = active, 1 = completed, 2 = cancelled.

getNextStreamId()

READ
getNextStreamId(): number

Returns the ID that will be assigned to the next stream. Stream IDs are sequential starting from 1.

Returns
number — Next available stream ID.

getTotalStreamsPlaced()

READ
getTotalStreamsPlaced(): number

Returns the cumulative count of all streams ever placed, including completed and cancelled ones.

Returns
number — All-time streams placed.

getTotalStreamsCompleted()

READ
getTotalStreamsCompleted(): number

Returns the number of streams that have been fully executed to completion (all input streamed).

Returns
number — All-time streams completed.

getActiveStreamCount()

READ
getActiveStreamCount(): number

Returns the current number of active (status = 0) streams. Useful for a live dashboard counter.

Returns
number — Number of currently active streams.
Example
const active = await readContract("saturntwamm", "getActiveStreamCount", []);

getAllActiveStreamIds()

READ
getAllActiveStreamIds(): number*

Streams the IDs of all currently active (status = 0) streams. Pair with getStreamInfo() or getActiveStreamsData() to build a live order-book view.

Returns
number* — Sequence of active stream IDs.
Example
const ids = await readContract("saturntwamm", "getAllActiveStreamIds", []);

getActiveStreamIdsByOwner()

READ
getActiveStreamIdsByOwner(owner: address): number*

Returns only the active stream IDs that belong to a specific owner address. Useful for a personal dashboard showing all of a user's live streams.

Parameters
NameTypeDescription
owneraddressAddress whose active streams to query.
Returns
number* — Sequence of active stream IDs owned by the given address.
Example
const myStreams = await readContract("saturntwamm", "getActiveStreamIdsByOwner", [userAddress]);

getActiveStreamIdsByPool()

READ
getActiveStreamIdsByPool(poolId: number): number*

Returns only the active stream IDs targeting a specific pool. Useful for pool analytics pages that want to show pending TWAMM flow alongside live reserves.

Parameters
NameTypeDescription
poolIdnumberPool ID to filter streams by.
Returns
number* — Sequence of active stream IDs targeting the given pool.
Example
const poolStreams = await readContract("saturntwamm", "getActiveStreamIdsByPool", [poolId]);

getActiveStreamsData()

READ
getActiveStreamsData(): string*

Batch view that streams one encoded row per active stream, eliminating N round-trips for list UIs. Each row is pipe-delimited: "streamId|poolId|owner|tokenIn|tokenOut|amountInTotal|amountInRemaining|amountOutAccumulated|endTime|status".

Returns
string* — Sequence of pipe-delimited stream data rows. One row per active stream.
What to expect
Only includes status = 0 streams. Parse each row with row.split("|").
Example
const rows = await readContract("saturntwamm", "getActiveStreamsData", []);
const streams = rows.map(row => {
  const [streamId, poolId, owner, tokenIn, tokenOut,
         amtTotal, amtRemaining, amtOut, endTime, status] = row.split("|");
  return { streamId, poolId, owner, tokenIn, tokenOut,
           amtTotal, amtRemaining, amtOut, endTime, status };
});

getActiveStreamsDataByOwner()

READ
getActiveStreamsDataByOwner(owner: address): string*

Same pipe-delimited batch format as getActiveStreamsData() but filtered to a single owner. Use this on a wallet portfolio page to load all of a user's live streams in one call.

Parameters
NameTypeDescription
owneraddressAddress to filter active streams by.
Returns
string* — Sequence of pipe-delimited active stream rows for the given owner.
Example
const myRows = await readContract("saturntwamm", "getActiveStreamsDataByOwner", [userAddress]);
Advanced Pools & Capital · Contract #20

SaturnFlash

saturnflash

Executes atomic two-pool flash arbitrage using idle liquidity borrowed from saturnliquidity. The contract borrows a tokenStart amount, routes it through two pools (tokenStart → tokenMid → tokenStart) in a single transaction, repays the principal plus a small flash fee, and forwards the net profit to the executor. Because TOMB requires literal call targets, Aave-style open-callback flash loans are not supported; the arbitrage logic is baked in. Build bots that call executeFlashArb whenever cross-pool price discrepancies exceed the combined flash fee and swap fees.

Flash Arbitrage

executeFlashArb()

WRITE
executeFlashArb(from: address, poolIdBuy: number, poolIdSell: number, tokenStart: string, amountIn: number, minNetProfit: number): number

Core flash-arbitrage entrypoint. Borrows amountIn of tokenStart from saturnliquidity, executes leg 1 (tokenStart → tokenMid via poolIdBuy) and leg 2 (tokenMid → tokenStart via poolIdSell), repays the borrowed principal plus the flash fee, and transfers the net profit to from. The transaction reverts atomically if the round-trip yields no profit, the profit is too small to cover the flash fee, or netProfit falls below minNetProfit — so the caller's reserves are never at risk, only their gas. The tokenMid is resolved automatically from poolIdBuy's pair. poolIdBuy and poolIdSell must be different pools and both must contain tokenStart.

Parameters
NameTypeDescription
fromaddressExecutor's address; must be the transaction witness and is the recipient of net profit.
poolIdBuynumberID of the pool used for leg 1 (tokenStart → tokenMid). tokenMid is inferred as the other token in this pool.
poolIdSellnumberID of the pool used for leg 2 (tokenMid → tokenStart). Must contain both tokenStart and the resolved tokenMid.
tokenStartstringToken symbol to borrow and to denominate profit in. Must be present in both pools.
amountInnumberRaw-unit amount to borrow from saturnliquidity. Must be > 0 and ≤ available liquidity balance.
minNetProfitnumberMinimum acceptable net profit in raw units of tokenStart after deducting the flash fee. Set to 0 to accept any positive profit; set higher to protect against thin-margin executions.
Returns
number — Net profit in raw units of tokenStart delivered to the executor.
What to expect
Reverts if flashEnabled == 0 (paused), amountIn == 0, poolIdBuy == poolIdSell, tokenStart is not a valid symbol, the liquidity pool has insufficient balance, the round-trip is unprofitable, grossProfit ≤ flashFee, or netProfit < minNetProfit. On revert every state change unwinds — no fee is charged and no principal is lost.
Example
const from = "S3...myBotAddress";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnflash", "executeFlashArb",
    [from, poolIdBuy, poolIdSell, "SOUL", amountIn, minNetProfit])
  .spendGas(from)
  .endScript();
// returns netProfit (number) — raw units of tokenStart

Fee & Config Views

quoteFlashFee()

READ
quoteFlashFee(amount: number): number

Returns the flash fee that would be charged on a given borrow amount at the current flashFeePer10k rate. Use this before calling executeFlashArb to calculate whether the expected arb spread exceeds the total cost (2× swap fees + flash fee + gas).

Parameters
NameTypeDescription
amountnumberThe borrow amount in raw token units.
Returns
number — Flash fee in raw token units: (amount × flashFeePer10k) / 10000.
What to expect
Pure math, never reverts. Returns 0 if amount is too small to produce a non-zero fee at the current rate.
Example
const fee = await readContract("saturnflash", "quoteFlashFee", [amountIn]);
// fee must be < expected spread for the arb to be profitable

getMaxBorrowable()

READ
getMaxBorrowable(tokenSymbol: string): number

Returns the current raw-unit balance of tokenSymbol held by saturnliquidity — the maximum amount that can be borrowed in a single executeFlashArb call. Always query this before sizing amountIn to avoid an insufficient-liquidity revert.

Parameters
NameTypeDescription
tokenSymbolstringThe token symbol to check borrowable liquidity for (e.g. "SOUL", "KCAL").
Returns
number — Maximum borrowable amount in raw units of tokenSymbol.
What to expect
Reflects real-time liquidity. Concurrent trades may lower this between query and execution.
Example
const max = await readContract("saturnflash", "getMaxBorrowable", ["SOUL"]);
const amountIn = Math.floor(max * 0.5); // use a fraction for safety

getFlashFeePer10k()

READ
getFlashFeePer10k(): number

Returns the current flash fee rate in basis points out of 10,000 (e.g. 5 = 0.05%). This rate is applied to amountIn to determine the fee the executor pays on top of loan repayment.

Returns
number — Flash fee rate per 10,000 (range: 1–100; default: 5).
What to expect
Always between 1 (0.01%) and 100 (1%).
Example
const feePer10k = await readContract("saturnflash", "getFlashFeePer10k", []);

getFlashEnabled()

READ
getFlashEnabled(): number

Returns 1 if flash arbitrage is currently active, 0 if the admin has paused it. Check this before attempting executeFlashArb to surface a clear status in your bot or UI.

Returns
number — 1 = enabled; 0 = paused by admin.
What to expect
Returns 0 or 1 only.
Example
const enabled = await readContract("saturnflash", "getFlashEnabled", []);
if (enabled !== 1) throw new Error("Flash arb is currently paused");

getContractVersion()

READ
getContractVersion(): string

Returns the deployed version string for this contract.

Returns
string — Version identifier, e.g. "saturnflash-4.2.4".
Example
const ver = await readContract("saturnflash", "getContractVersion", []);

getContractAddress()

READ
getContractAddress(): address

Returns the on-chain address of this contract. Useful for constructing token-transfer pre-approvals or inspecting on-chain balances.

Returns
address — The deployed address of the saturnflash contract.
Example
const addr = await readContract("saturnflash", "getContractAddress", []);

Statistics Views

getTotalFlashArbs()

READ
getTotalFlashArbs(): number

Returns the cumulative count of successful flash arbitrage executions since deployment.

Returns
number — Total successful executeFlashArb calls.
Example
const total = await readContract("saturnflash", "getTotalFlashArbs", []);

getTotalFlashFeesCollected()

READ
getTotalFlashFeesCollected(): number

Returns the lifetime total of flash fees (in raw token units) collected by the protocol admin across all successful arb executions.

Returns
number — Cumulative flash fees forwarded to admin, in raw token units.
Example
const fees = await readContract("saturnflash", "getTotalFlashFeesCollected", []);

getExecutorArbCount()

READ
getExecutorArbCount(executor: address): number

Returns the number of successful flash arb executions performed by a specific executor address. Use this on leaderboards or to track your own bot's activity.

Parameters
NameTypeDescription
executoraddressThe executor address to query.
Returns
number — Number of successful executeFlashArb calls made by this address.
Example
const count = await readContract("saturnflash", "getExecutorArbCount", [botAddress]);

getExecutorTotalProfit()

READ
getExecutorTotalProfit(executor: address): number

Returns the lifetime net profit (in raw token units of tokenStart) paid out to a specific executor across all successful arb executions.

Parameters
NameTypeDescription
executoraddressThe executor address to query.
Returns
number — Lifetime net profit in raw token units paid to this executor.
Example
const profit = await readContract("saturnflash", "getExecutorTotalProfit", [botAddress]);
Advanced Pools & Capital · Contract #21

SaturnHolders

saturnholders

MasterChef-style stake-to-earn vault that distributes a slice of every swap's fee to token holders pro-rata by stake. Users deposit tokens into saturnholders and earn the holder portion of swap fees (accrued by saturnswap on each trade) plus their share of arbitrage profits from saturnstakearb — all from the same deposit, with no separate claim step required. Staking also enables idle capital to be flash-borrowed by saturnstakearb for atomic round-trip arbs that generate additional yield. Reward tokens always match the staked token: stake SOUL, earn SOUL from SOUL-side swaps.

Stake & Unstake

stake()

WRITE
stake(from: address, tokenSymbol: string, amount: number)

Deposits amount raw units of tokenSymbol into the rewards vault on behalf of from. If the user already has an open position, any pending rewards are settled first so they aren't lost. The user's bookmark is set to the current accumulator value, ensuring they only earn from future accruals. Tokens are transferred from the caller's wallet into this contract's custody and cannot be traded while staked — this custody model prevents the wash-claim attack that pure hold-to-earn is vulnerable to.

Parameters
NameTypeDescription
fromaddressStaker's address. Must be the transaction witness.
tokenSymbolstringSymbol of the token to stake (must be a valid Saturn pool token).
amountnumberRaw-unit amount to deposit. Must be > 0.
What to expect
Reverts if from is not the witness, amount == 0, or tokenSymbol is not a valid token. Pending rewards for any existing position are auto-settled before the new stake is recorded. Token must be in the from wallet in sufficient balance.
Example
const from = "S3...userAddress";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnholders", "stake", [from, "SOUL", stakeAmount])
  .spendGas(from)
  .endScript();

unstake()

WRITE
unstake(from: address, tokenSymbol: string, amount: number)

Withdraws amount raw units of tokenSymbol from the vault back to from. Pending rewards are settled automatically before the stake is decremented, so the user receives everything they've earned. Partial unstakes are supported — only the requested amount is returned and the remainder continues earning. If amount would reduce the stake to zero, the distinct-stakers count is decremented. In v4.5.0, any portion of the stake locked via a saturntaz pledge cannot be unstaked until saturntaz calls unlockPledge.

Parameters
NameTypeDescription
fromaddressStaker's address. Must be the transaction witness.
tokenSymbolstringSymbol of the token to withdraw.
amountnumberRaw-unit amount to withdraw. Must be > 0 and ≤ (stakedAmount - pledgeLocked).
What to expect
Reverts if from is not the witness, amount == 0, stake < amount, or amount exceeds unlocked stake (stake minus pledgeLocked). Pending rewards are always settled first — they are never forfeited.
Example
const from = "S3...userAddress";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnholders", "unstake", [from, "SOUL", unstakeAmount])
  .spendGas(from)
  .endScript();

Claim Rewards

claim()

WRITE
claim(from: address, tokenSymbol: string)

Pays out all pending rewards for from's tokenSymbol stake without altering the stake balance. Reward tokens are transferred from saturnliquidity (the central custodian) directly to from. The user's bookmark is bumped to the current accumulator so subsequent calls report zero pending until new swaps accrue more fees. Can be called at any time after stake().

Parameters
NameTypeDescription
fromaddressStaker's address. Must be the transaction witness and must have an active stake.
tokenSymbolstringSymbol of the staked token whose rewards to claim.
What to expect
Reverts if from is not the witness or has no stake in tokenSymbol. If pending is zero, the call succeeds silently (no transfer) and the bookmark is still updated.
Example
const from = "S3...userAddress";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnholders", "claim", [from, "SOUL"])
  .spendGas(from)
  .endScript();

Pledge Lock (v4.5.0)

getPledgeLocked()

READ
getPledgeLocked(user: address, tokenSymbol: string): number

Returns the raw-unit amount of tokenSymbol currently locked against unstaking for user by a Saturn Lending pledge (via saturntaz). The available-for-unstake amount is stakedAmount − pledgeLocked. Query this alongside getStakedAmount to surface the correct withdrawable balance in your UI.

Parameters
NameTypeDescription
useraddressThe staker's address.
tokenSymbolstringThe staked token symbol.
Returns
number — Raw-unit amount currently locked by an active saturntaz pledge. Returns 0 if no pledge is active.
Example
const locked = await readContract("saturnholders", "getPledgeLocked", [userAddr, "SOUL"]);
const staked  = await readContract("saturnholders", "getStakedAmount", [userAddr, "SOUL"]);
const available = staked - locked;

Stake-Arb Status

getLoanOpen()

READ
getLoanOpen(tokenSymbol: string): number

Returns 1 while a saturnstakearb flash-borrow of tokenSymbol's staked capital is in progress within a transaction, 0 otherwise. Under normal conditions this will always return 0 from an external query because the borrow opens and closes within a single atomic transaction. Useful for debugging or monitoring.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to check.
Returns
number — 1 = flash loan open (within an active executeArb tx); 0 = no open loan.
Example
const open = await readContract("saturnholders", "getLoanOpen", ["SOUL"]);

Individual Staker Views

getStakedAmount()

READ
getStakedAmount(user: address, tokenSymbol: string): number

Returns the raw-unit amount of tokenSymbol currently staked by user. This is the gross staked balance; subtract getPledgeLocked to get the amount available for immediate unstaking.

Parameters
NameTypeDescription
useraddressThe staker's address.
tokenSymbolstringThe staked token symbol.
Returns
number — Raw-unit staked balance. Returns 0 if user has no stake.
Example
const staked = await readContract("saturnholders", "getStakedAmount", [userAddr, "SOUL"]);

getPendingRewards()

READ
getPendingRewards(user: address, tokenSymbol: string): number

Computes and returns the unclaimed rewards (in raw token units) accrued to user's tokenSymbol stake since their last claim, stake, or unstake. This is the primary view to display in a rewards dashboard — query it before calling claim() to show the user what they'll receive.

Parameters
NameTypeDescription
useraddressThe staker's address.
tokenSymbolstringThe staked token symbol.
Returns
number — Pending reward amount in raw units of tokenSymbol. Returns 0 if user has no stake or no rewards have accrued.
Example
const pending = await readContract("saturnholders", "getPendingRewards", [userAddr, "SOUL"]);
console.log(`Claimable: ${pending} raw SOUL units`);

getUserRewardDebt()

READ
getUserRewardDebt(user: address, tokenSymbol: string): number

Returns the user's reward-debt bookmark — the accFeePerToken value at the time of their last settle (stake, unstake, or claim). The difference between the current accumulator and this bookmark, multiplied by the user's stake, gives the pending rewards. Useful for verifying MasterChef math or building advanced analytics.

Parameters
NameTypeDescription
useraddressThe staker's address.
tokenSymbolstringThe staked token symbol.
Returns
number — The scaled accumulator snapshot (1e12 precision) at the user's last settle.
Example
const debt = await readContract("saturnholders", "getUserRewardDebt", [userAddr, "SOUL"]);

getStakeInfo()

READ
getStakeInfo(user: address, tokenSymbol: string): string

Returns a packed summary string for a user's position in a single call: "stake:{n}_debt:{n}_pending:{n}". Convenient for a compact dashboard widget or a single-RPC snapshot of a user's full position.

Parameters
NameTypeDescription
useraddressThe staker's address.
tokenSymbolstringThe staked token symbol.
Returns
string — Packed string e.g. "stake:500000000_debt:1234000000000_pending:12300".
Example
const info = await readContract("saturnholders", "getStakeInfo", [userAddr, "SOUL"]);
// "stake:500000000_debt:1234000000000_pending:12300"

getUserPortfolioData()

READ
getUserPortfolioData(user: address): string*

Returns one row per token in which user has an active stake (stake > 0). Each row is "symbol|stake|pending|rewardDebt". Use this to render a user's entire staking portfolio in a single call instead of N per-token round-trips.

Parameters
NameTypeDescription
useraddressThe staker's address.
Returns
string* — Iterator of rows; each row: "symbol|stake|pending|rewardDebt". Empty if user has no stakes.
Example
const rows = await readContract("saturnholders", "getUserPortfolioData", [userAddr]);
// ["SOUL|500000000|12300|1234000000000", "KCAL|200000000|4500|987000000000"]

Pool-Level & Global Views

getTotalStaked()

READ
getTotalStaked(tokenSymbol: string): number

Returns the total raw-unit amount of tokenSymbol currently staked across all holders. This is the denominator used in the MasterChef accumulator and also the maximum flash-borrowable amount for saturnstakearb.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Total staked supply in raw token units. Returns 0 if no one has staked this token.
Example
const totalStaked = await readContract("saturnholders", "getTotalStaked", ["SOUL"]);

getAccFeePerToken()

READ
getAccFeePerToken(tokenSymbol: string): number

Returns the current global accumulated-fee-per-unit value for tokenSymbol, scaled by 1e12. This counter only ever increases. Use it alongside getUserRewardDebt to reconstruct the MasterChef pending-reward formula: pending = stake × (accNow − debt) / 1e12.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Cumulative fee-per-unit accumulator scaled by 1e12.
Example
const acc = await readContract("saturnholders", "getAccFeePerToken", ["SOUL"]);

getTotalStakers()

READ
getTotalStakers(tokenSymbol: string): number

Returns the number of distinct addresses currently holding a non-zero stake of tokenSymbol. Useful for showing participation metrics in your UI.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Count of unique stakers for this token.
Example
const stakers = await readContract("saturnholders", "getTotalStakers", ["SOUL"]);

getLifetimeAccrued()

READ
getLifetimeAccrued(tokenSymbol: string): number

Returns the total raw-unit amount of tokenSymbol that has ever been accrued into the reward pool (from both swap fees and arb profits) since deployment. Useful for APR calculation and historical analytics.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Lifetime accrued rewards in raw token units.
Example
const lifetime = await readContract("saturnholders", "getLifetimeAccrued", ["SOUL"]);

getStakedTokenSymbols()

READ
getStakedTokenSymbols(): string*

Returns an iterator of every token symbol that has ever been staked. Use this to enumerate all active staking markets without needing to know the token list in advance.

Returns
string* — Iterator of token symbol strings (e.g. "SOUL", "KCAL", ...).
Example
const symbols = await readContract("saturnholders", "getStakedTokenSymbols", []);
// ["SOUL", "KCAL", "DYT", ...]

getStakedTokensData()

READ
getStakedTokensData(): string*

Returns one packed row per staked token symbol. Each row is "symbol|totalStaked|accFeePerToken|stakers|lifetimeAccrued|loanOpen". Use this for a single-call dashboard load of all token staking metrics.

Returns
string* — Iterator of rows; each row: "symbol|totalStaked|accFeePerToken|stakers|lifetimeAccrued|loanOpen".
Example
const rows = await readContract("saturnholders", "getStakedTokensData", []);
// ["SOUL|500000000|1234000000000|42|9870000|0", ...]

registerStakedToken()

WRITE
registerStakedToken(tokenSymbol: string)

Permissionless, idempotent utility that adds tokenSymbol to the enumeration index (getStakedTokenSymbols / getStakedTokensData) if it has totalStaked > 0 but is not yet listed. Only needed for tokens staked before the v4.4.1 enumeration index was introduced. New stakes self-register automatically inside stake().

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to backfill into the enumeration index.
What to expect
No-op if tokenSymbol is already listed or has zero total stake. Never reverts.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnholders", "registerStakedToken", ["LEGACY"])
  .spendGas(from)
  .endScript();

getContractVersion()

READ
getContractVersion(): string

Returns the deployed version string for this contract.

Returns
string — Version identifier, e.g. "saturnholders-4.4.1".
Example
const ver = await readContract("saturnholders", "getContractVersion", []);

getContractAddress()

READ
getContractAddress(): address

Returns the on-chain address of this contract. Useful for token-transfer approvals or direct balance lookups.

Returns
address — The deployed address of the saturnholders contract.
Example
const addr = await readContract("saturnholders", "getContractAddress", []);
Financial Products · Contract #9

SaturnBonds

saturnbonds

Providers can sell the future provider-fee stream of a pool as a fixed-yield bond. Buyers pay an up-front purchasePrice (less than faceValue) and collect the fees the pool earns over the bond's term — up to faceValue. Two modes: PARTIAL (1) pays only what the fees actually earn, and HYBRID (2) tops the payout up to faceValue using provider-posted collateral. Buying a bond locks the pool (it can't be removed) and redirects fees until the bond is settled. Anyone can settle a matured bond — the buyer has every incentive to.

Bond Lifecycle

listBond()

WRITE
listBond(from: address, poolId: number, faceValue: number, purchasePrice: number, feeToken: string, durationSeconds: number, mode: number, collateralAmount: number)

Pool provider lists a new bond against one of their pools. faceValue is the max payout to the buyer; purchasePrice is what the buyer pays up front (must be strictly less than faceValue). In HYBRID mode (mode = 2) the provider deposits collateralAmount of feeToken now — this is held as a buffer to guarantee the buyer's payout.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberPool the bond is issued against.
faceValuenumberMax raw payout to the buyer at maturity.
purchasePricenumberRaw amount the buyer pays (< faceValue).
feeTokenstringToken the bond is denominated in.
durationSecondsnumberTerm length (min 86400, max 31536000).
modenumber1 = partial (no collateral), 2 = hybrid (collateral required).
collateralAmountnumberRaw collateral (must be 0 if mode=1, > 0 if mode=2).
Returns
void — Success = bond listing created (status = 0).
What to expect
Reverts on: "Only pool provider", "Pool already has active bond or rental", "Purchase price must be less than face value", invalid mode, missing collateral in hybrid mode, or duration out of range.
Example
// Hybrid mode bond for pool 42, 1M SOUL face, 900k purchase, 90 day term, 100k collateral
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2500)
  .callContract("saturnbonds", "listBond",
    [from, 42, 1000000, 900000, "SOUL", 90 * 86400, 2, 100000])
  .spendGas(from)
  .endScript();

cancelListing()

WRITE
cancelListing(from: address, bondId: number)

Cancel a bond listing before any buyer has purchased it. Returns any deposited collateral back to the issuer.

Parameters
NameTypeDescription
fromaddressOriginal issuer wallet (must be witness).
bondIdnumberBond to cancel.
Returns
void — Success = bond status set to 3 (cancelled).
What to expect
Reverts on: "Only bond issuer" or "Bond not in listed state".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 1500)
  .callContract("saturnbonds", "cancelListing", [from, bondId])
  .spendGas(from)
  .endScript();

purchaseBond()

WRITE
purchaseBond(from: address, bondId: number)

Buyer pays the listing's purchasePrice directly to the issuer, becomes the bond holder, starts the term clock, and triggers fee redirection — all swap fees in feeToken now flow to this contract on behalf of the buyer. The pool becomes financially locked until settlement.

Parameters
NameTypeDescription
fromaddressBuyer wallet (must be witness, cannot be the issuer).
bondIdnumberBond to purchase.
Returns
void — Success = bond status = 1 (active), fees redirected.
What to expect
Reverts on: "Cannot buy your own bond", "Bond not available for purchase", or "Insufficient balance to purchase bond".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2000)
  .callContract("saturnbonds", "purchaseBond", [from, bondId])
  .spendGas(from)
  .endScript();

settleBond()

WRITE
settleBond(from: address, bondId: number)

Called any time after the bond's maturity. Claims the accumulated fees (plus collateral in hybrid mode), pays up to faceValue to the bond holder, returns any excess to the original issuer, clears the fee redirect, and unlocks the pool. Anyone can call this — in practice the holder does because they want their payout.

Parameters
NameTypeDescription
fromaddressCaller wallet (must be witness; any wallet works).
bondIdnumberBond to settle.
Returns
void — Success = bond status = 2 (settled) and pool unlocked.
What to expect
Reverts on: "Bond not active" or "Bond has not matured yet". Also sweeps any accumulated fees in the OTHER token directly to the issuer — bonds only cover a single feeToken.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 2500)
  .callContract("saturnbonds", "settleBond", [from, bondId])
  .spendGas(from)
  .endScript();

transferBond()

WRITE
transferBond(from: address, to: address, bondId: number)

Secondary-market transfer. The current holder assigns the bond to a different address. Bond must be active; recipient cannot be the current holder. Off-chain payment is up to the two parties.

Parameters
NameTypeDescription
fromaddressCurrent holder wallet (must be witness).
toaddressNew holder address.
bondIdnumberBond being transferred.
Returns
void — Success = buyer address updated.
What to expect
Reverts on: "Only bond holder", "Bond not active", "Invalid recipient", or "Cannot transfer to self".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, Address.Null, 100000, 1500)
  .callContract("saturnbonds", "transferBond", [from, recipientAddress, bondId])
  .spendGas(from)
  .endScript();

Bond Views

getBondInfo()

READ
getBondInfo(bondId: number): string

Packed string with every top-level bond field — pool, face value, purchase price, fee token, mode, maturity timestamp, status, and collateral. Parse with String.split on underscores.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
string — Format: "pool:<n>_face:<n>_price:<n>_token:<s>_mode:<1|2>_maturity:<ts>_status:<0..3>_collateral:<n>".
What to expect
Status codes: 0 listed, 1 active, 2 settled, 3 cancelled.
Example
const info = await readContract("saturnbonds", "getBondInfo", [bondId]);

getBondPoolId()

READ
getBondPoolId(bondId: number): number

Pool the bond is issued against.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — Pool ID.
What to expect
Never changes after listing.
Example
const pid = await readContract("saturnbonds", "getBondPoolId", [bondId]);

getBondIssuer()

READ
getBondIssuer(bondId: number): address

Address of the wallet that created the bond listing.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
address — Issuer address.
What to expect
Set at list time and immutable.
Example
const issuer = await readContract("saturnbonds", "getBondIssuer", [bondId]);

getBondBuyer()

READ
getBondBuyer(bondId: number): address

Current holder of the bond. Null for listings that have not been purchased yet. Updated on transferBond().

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
address — Holder address (null if unsold).
What to expect
Compare against @null to check if still on the primary market.
Example
const holder = await readContract("saturnbonds", "getBondBuyer", [bondId]);

getBondFaceValue()

READ
getBondFaceValue(bondId: number): number

Maximum raw payout the buyer can receive at maturity.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — Face value in feeToken raw units.
What to expect
Yield for the buyer = faceValue - purchasePrice.
Example
const face = await readContract("saturnbonds", "getBondFaceValue", [bondId]);

getBondPurchasePrice()

READ
getBondPurchasePrice(bondId: number): number

Up-front raw amount the buyer paid (or pays) for the bond.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — Purchase price in feeToken raw units.
What to expect
Always strictly less than face value.
Example
const price = await readContract("saturnbonds", "getBondPurchasePrice", [bondId]);

getBondFeeToken()

READ
getBondFeeToken(bondId: number): string

Symbol of the token the bond is denominated in — purchase price, face value, and collateral are all in this token, and only swap fees accrued in this token are counted toward the payout.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
string — Fee token symbol.
What to expect
Must be one of the two tokens in the pool.
Example
const token = await readContract("saturnbonds", "getBondFeeToken", [bondId]);

getBondCollateral()

READ
getBondCollateral(bondId: number): number

Raw collateral amount posted by the issuer (always 0 in partial mode).

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — Collateral amount in feeToken raw units.
What to expect
Non-zero only for hybrid-mode bonds (mode = 2).
Example
const col = await readContract("saturnbonds", "getBondCollateral", [bondId]);

getBondMode()

READ
getBondMode(bondId: number): number

Returns 1 for PARTIAL (no collateral) or 2 for HYBRID (collateral-backed).

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — 1 = partial, 2 = hybrid.
What to expect
Set at list time and immutable.
Example
const mode = await readContract("saturnbonds", "getBondMode", [bondId]);

getBondStatus()

READ
getBondStatus(bondId: number): number

Lifecycle state of the bond.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — 0 = listed, 1 = active, 2 = settled, 3 = cancelled.
What to expect
Transitions: 0 → 1 (purchase), 1 → 2 (settle), 0 → 3 (cancel).
Example
const s = await readContract("saturnbonds", "getBondStatus", [bondId]);

getBondMaturityTime()

READ
getBondMaturityTime(bondId: number): number

Unix timestamp at which the bond can be settled. While the bond is listed (status 0), this field stores the term duration in seconds — it only becomes an absolute timestamp after purchase.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — Maturity timestamp (or duration if unsold).
What to expect
Check getBondStatus() to know how to interpret the value.
Example
const maturity = await readContract("saturnbonds", "getBondMaturityTime", [bondId]);

getNextBondId()

READ
getNextBondId(): number

ID that will be assigned to the next bond listed.

Returns
number — Next bond ID (starts at 1).
What to expect
Total bonds so far = getNextBondId() - 1.
Example
const next = await readContract("saturnbonds", "getNextBondId", []);

getAllBondIds()

READ
getAllBondIds(): number*

Generator yielding every bondId ever created.

Returns
number* — Iterable of bond IDs.
What to expect
Filter by getBondStatus() to build the primary-market, secondary-market, or settled views.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnbonds", "getAllBondIds", [])
  .endScript();
Financial Products · Contract #10

SaturnRental

saturnrental

Franchise model for pools. Providers list their pool with a daily SOUL rate, deposit, fee-bound window, and a minimum term. A renter pays the deposit plus prepaid rent and gains exclusive control of the pool's fee rate for the rental window — the renter collects every swap fee while the owner collects guaranteed rent. When the paid-through time expires, either party can settle up: remaining fees go to the renter, the deposit is refunded, the original fee rate is restored, and the pool is unlocked. While a rental is active the pool is financially locked and cannot be removed.

Rental Lifecycle

listForRent()

WRITE
listForRent(from: address, poolId: number, dailyRate: number, depositRequired: number, minFeePer10k: number, maxFeePer10k: number, minTermDays: number)

Pool provider lists their pool for rent. Saves the current fee rate so it can be restored at end, and defines the renter's operating envelope.

Parameters
NameTypeDescription
fromaddressPool provider — must be a transaction witness.
poolIdnumberThe pool you own and want to rent out. Must be active, with no bond or rental already in effect.
dailyRatenumberRent per day, in raw SOUL. Must be > 0.
depositRequirednumberRefundable deposit the renter must post, in raw SOUL. Must be > 0.
minFeePer10knumberLowest pool fee the renter may set. Must be >= protocol min (see SaturnAdmin.getPoolFeeRange).
maxFeePer10knumberHighest pool fee the renter may set. Must be <= protocol max and >= minFeePer10k.
minTermDaysnumberMinimum rental term, in days. Must be 1–365. The renter prepays dailyRate * minTermDays up-front.
What to expect
A new rentalId is created in status 0 (listed). Nothing is locked yet — no renter has stepped in. Use getAllRentalIds() + getRentalStatus() to show your listing on the marketplace.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "listForRent", [
    from,
    poolId,
    dailyRateSoul,      // raw SOUL per day
    depositSoul,        // raw SOUL deposit
    25,                 // 0.25% min fee
    300,                // 3.00% max fee
    7                   // minimum 7-day term
  ])
  .spendGas(from)
  .endScript();

cancelListing()

WRITE
cancelListing(from: address, rentalId: number)

Pool provider cancels a listing that has not yet been rented. Only works while status = 0 (listed).

Parameters
NameTypeDescription
fromaddressMust be the provider who created the listing.
rentalIdnumberThe rental to cancel.
What to expect
Status flips to 3 (cancelled). The rentalId stays in allRentalIds for history but is no longer rentable.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "cancelListing", [from, rentalId])
  .spendGas(from)
  .endScript();

rentPool()

WRITE
rentPool(from: address, rentalId: number)

Renter takes the listing. Pays deposit + (dailyRate * minTermDays) in SOUL, gains fee control, and the pool's fee stream is redirected to the rental escrow.

Parameters
NameTypeDescription
fromaddressRenter — cannot be the pool provider. Must be a transaction witness.
rentalIdnumberA listing in status 0 (listed).
What to expect
SOUL = deposit + dailyRate * minTermDays is pulled from your wallet in a single transfer. Status flips to 1 (rented), paidThroughTime = now + minTermDays*86400, and the pool's financial lock is incremented. From this point you control adjustFee() and every swap fee accrues to you until endRental().
Example
// Always preflight: read dailyRate + deposit + minTermDays first
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "rentPool", [from, rentalId])
  .spendGas(from)
  .endScript();

extendRental()

WRITE
extendRental(from: address, rentalId: number, additionalDays: number)

Renter prepays additional days before the current paid-through time. Avoids losing fee control when the term expires.

Parameters
NameTypeDescription
fromaddressMust be the current renter.
rentalIdnumberAn active rental (status 1).
additionalDaysnumberExtra days to prepay. Must be >= 1.
What to expect
dailyRate * additionalDays SOUL is transferred from the renter to the rental escrow. paidThroughTime is pushed out by additionalDays * 86400 seconds. rentalRentAccrued rises, so the owner will be able to collectRent() the additional amount.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "extendRental", [from, rentalId, 30]) // +30 days
  .spendGas(from)
  .endScript();

adjustFee()

WRITE
adjustFee(from: address, rentalId: number, newFeePer10k: number)

Renter changes the pool's fee rate, within the [minFee, maxFee] window set at list time. This is the core lever of the franchise model — raise fees to earn more per swap, lower them to attract volume.

Parameters
NameTypeDescription
fromaddressMust be the current renter.
rentalIdnumberAn active rental that has not yet passed paidThroughTime.
newFeePer10knumberNew pool fee, per 10,000. Must be within [rentalMinFee, rentalMaxFee].
What to expect
The pool's active fee rate updates immediately; the next swap uses the new rate. Call reverts if the rental term has expired — extend first, or end the rental.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "adjustFee", [from, rentalId, 150]) // 1.50%
  .spendGas(from)
  .endScript();

claimRentalFees()

WRITE
claimRentalFees(from: address, rentalId: number)

Renter harvests the swap fees accumulated on both sides of the pool while the rental has been active.

Parameters
NameTypeDescription
fromaddressMust be the current renter.
rentalIdnumberAn active rental (status 1).
What to expect
Both tokenA and tokenB redirected fee balances are transferred to the renter in one call. Safe to call frequently — if nothing has accrued since last claim, it's a no-op.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "claimRentalFees", [from, rentalId])
  .spendGas(from)
  .endScript();

collectRent()

WRITE
collectRent(from: address, rentalId: number)

Pool provider withdraws rent that the renter has already prepaid. Can be called at any time while the rental is active.

Parameters
NameTypeDescription
fromaddressMust be the listing owner (pool provider).
rentalIdnumberAn active rental (status 1).
What to expect
available = rentalRentAccrued - rentalRentCollected SOUL is sent to the owner. Reverts if available is zero.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "collectRent", [from, rentalId])
  .spendGas(from)
  .endScript();

endRental()

WRITE
endRental(from: address, rentalId: number)

Either party settles the rental once the paid-through time has passed. Finalizes fees to the renter, pays remaining rent to the owner, refunds the deposit, restores the original fee rate, and unlocks the pool.

Parameters
NameTypeDescription
fromaddressMust be either the owner or the renter.
rentalIdnumberAn active rental whose paidThroughTime has elapsed.
What to expect
Any unclaimed swap fees go to the renter, remaining rent is swept to the owner, the deposit is returned to the renter, the pool's fee is set back to rentalOriginalFee, fee redirect is cleared, and the pool's financial lock count decrements. Status becomes 2 (ended). Reverts with 'Rental term not expired' if you call it too early.
Example
// Check readiness first
const pt = await readContract("saturnrental", "getRentalPaidThroughTime", [rentalId]);
if (Date.now() / 1000 < pt) throw new Error("Not expired yet");

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "endRental", [from, rentalId])
  .spendGas(from)
  .endScript();

Rental Views

getRentalInfo()

READ
getRentalInfo(rentalId: number): string

One-shot status snapshot used by marketplace UIs. Returns an underscore-delimited string with the key fields.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
string — pool:<poolId>_rate:<dailyRate>_deposit:<deposit>_minFee:<minFee>_maxFee:<maxFee>_status:<status>_paidThrough:<paidThrough>
What to expect
Split on '_' and then on ':' to extract each field. Status decodes as 0=listed, 1=rented, 2=ended, 3=cancelled.
Example
const raw = await readContract("saturnrental", "getRentalInfo", [rentalId]);
const parts = Object.fromEntries(
  raw.split("_").map(kv => kv.split(":"))
);
// parts.pool, parts.rate, parts.deposit, parts.minFee, parts.maxFee, parts.status, parts.paidThrough

getRentalPoolId()

READ
getRentalPoolId(rentalId: number): number

Returns the poolId this rental is attached to.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
number — Underlying pool ID.
What to expect
Pair with SaturnRouter.getPoolFullInfo() to show the pool's current state next to the rental listing.
Example
const poolId = await readContract("saturnrental", "getRentalPoolId", [rentalId]);

getRentalOwner()

READ
getRentalOwner(rentalId: number): address

Returns the pool provider who listed this rental.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
address — Listing owner.
What to expect
Use this for the 'collect rent' UI permission check — only this address can call collectRent.
Example
const owner = await readContract("saturnrental", "getRentalOwner", [rentalId]);

getRentalOperator()

READ
getRentalOperator(rentalId: number): address

Returns the address currently renting the pool, or @null if the listing has not been rented yet.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
address — Current renter address, or @null when still in listed state.
What to expect
This is the address authorized to call adjustFee, extendRental, and claimRentalFees.
Example
const renter = await readContract("saturnrental", "getRentalOperator", [rentalId]);

getRentalDailyRate()

READ
getRentalDailyRate(rentalId: number): number

Returns the daily rent, in raw SOUL.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
number — SOUL per day.
What to expect
Multiply by minTermDays (from getRentalInfo) to show 'total due' before calling rentPool.
Example
const perDay = await readContract("saturnrental", "getRentalDailyRate", [rentalId]);

getRentalStatus()

READ
getRentalStatus(rentalId: number): number

Returns the lifecycle status code.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
number — 0=listed, 1=rented, 2=ended, 3=cancelled.
What to expect
Filter getAllRentalIds() on status 0 to build the open marketplace, status 1 for the active rentals tab.
Example
const status = await readContract("saturnrental", "getRentalStatus", [rentalId]);

getRentalPaidThroughTime()

READ
getRentalPaidThroughTime(rentalId: number): number

Unix timestamp up to which rent has been prepaid. endRental cannot be called before this time.

Parameters
NameTypeDescription
rentalIdnumberThe rental to inspect.
Returns
number — Unix seconds.
What to expect
Render a countdown in the renter dashboard so they know when to extendRental or expect settlement.
Example
const pt = await readContract("saturnrental", "getRentalPaidThroughTime", [rentalId]);
const secondsLeft = pt - Math.floor(Date.now() / 1000);

getNextRentalId()

READ
getNextRentalId(): number

Returns the rentalId that will be assigned to the next listing.

Returns
number — Next rental ID (starts at 1).
What to expect
Total rentals ever listed = getNextRentalId() - 1.
Example
const next = await readContract("saturnrental", "getNextRentalId", []);

getAllRentalIds()

READ
getAllRentalIds(): number*

Generator yielding every rentalId ever created.

Returns
number* — Iterable of rental IDs.
What to expect
Combine with getRentalStatus() to partition listings into listed / rented / ended / cancelled buckets.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnrental", "getAllRentalIds", [])
  .endScript();
Financial Products · Contract #11

SaturnFeeOptions

saturnfeeopts

Derivatives on pool fee rates. A pool provider writes an option — a contract that gives a buyer the right, for a fixed duration, to set that pool's fee to a specific target rate. The buyer pays a premium up-front (in any valid token) and earns premium income for the writer. Once bought, the option locks the pool and the buyer can exerciseOption() at any time during the window to snap the fee to the target. Buyers can releaseOption() early to return fee control; after expiry, the writer calls expireOption() to reclaim it. A single pool can carry several options at once, so structured strategies (collars, straddles) are possible.

Option Lifecycle

writeOption()

WRITE
writeOption(from: address, poolId: number, targetFeePer10k: number, premium: number, premiumToken: string, durationSeconds: number)

Pool provider creates and lists a new option. Captures the pool's current fee rate so it can be restored when the option unwinds.

Parameters
NameTypeDescription
fromaddressPool provider — must be a transaction witness.
poolIdnumberThe active pool you own.
targetFeePer10knumberFee rate the buyer can snap the pool to. Must sit within SaturnAdmin.getPoolFeeRange.
premiumnumberUp-front price the buyer pays, in raw premiumToken units. Must be > 0.
premiumTokenstringToken symbol the premium is paid in. Must be a validated symbol.
durationSecondsnumberOption window in seconds once bought. Must be between 3,600 (1h) and 2,592,000 (30d).
What to expect
A new optionId is created in status 0 (listed). The pool is NOT yet locked — it only locks when someone buys the option. Listing is free.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "writeOption", [
    from,
    poolId,
    50,              // target 0.50% fee
    premiumRaw,      // e.g. 100 SOUL raw
    "SOUL",
    604800           // 7 day window
  ])
  .spendGas(from)
  .endScript();

cancelListing()

WRITE
cancelListing(from: address, optionId: number)

Option writer cancels a listing that has not yet been bought. Only works while status = 0 (listed).

Parameters
NameTypeDescription
fromaddressMust be the writer who created the listing.
optionIdnumberThe option to cancel.
What to expect
Status flips to 3 (cancelled). The optionId remains in allOptionIds for history but is no longer purchasable.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "cancelListing", [from, optionId])
  .spendGas(from)
  .endScript();

buyOption()

WRITE
buyOption(from: address, optionId: number)

Buyer pays the premium to the writer and activates the option. The duration window starts now and the pool gets a financial lock.

Parameters
NameTypeDescription
fromaddressBuyer — cannot be the option writer.
optionIdnumberA listing in status 0 (listed).
What to expect
premium tokens are transferred from buyer → writer in a single Token.transfer. Status flips to 1 (active), startTime = now, endTime = now + durationSeconds. The underlying pool's financial lock increments — it cannot be removed while the option is active.
Example
// Always preflight to show the buyer total cost
const premium = await readContract("saturnfeeopts", "getOptionPremium", [optionId]);

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "buyOption", [from, optionId])
  .spendGas(from)
  .endScript();

exerciseOption()

WRITE
exerciseOption(from: address, optionId: number)

Buyer snaps the pool's fee rate to the option's target. Can be called any time before the option expires, even multiple times (each call just re-applies the same rate).

Parameters
NameTypeDescription
fromaddressMust be the option buyer.
optionIdnumberAn active option (status 1) whose endTime is still in the future.
What to expect
The pool's active fee rate updates to targetFeePer10k immediately; the next swap uses the new rate. exercised is set to 1 so that releaseOption/expireOption know to restore the writer's original fee when the option unwinds.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "exerciseOption", [from, optionId])
  .spendGas(from)
  .endScript();

releaseOption()

WRITE
releaseOption(from: address, optionId: number)

Buyer voluntarily gives up fee control before expiry. Useful if the buyer is done with the position and wants to unlock the pool so the writer can list again.

Parameters
NameTypeDescription
fromaddressMust be the option buyer.
optionIdnumberAn active option (status 1).
What to expect
If the option had been exercised, the pool's fee is restored to the writer's original rate. The pool's financial lock decrements and status becomes 4 (released). Premium is NOT refunded — it was consumed at buy time.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "releaseOption", [from, optionId])
  .spendGas(from)
  .endScript();

expireOption()

WRITE
expireOption(from: address, optionId: number)

Anyone can trigger expiry after the option's endTime has passed. Typically called by the writer to reclaim full fee control and unlock the pool.

Parameters
NameTypeDescription
fromaddressAny witness — usually the writer.
optionIdnumberAn active option whose endTime has already passed.
What to expect
If the option had been exercised, the pool's fee snaps back to the writer's original rate. The pool's financial lock decrements and status becomes 2 (expired). Reverts with 'Option not expired yet' if called early — poll getOptionEndTime first.
Example
const endTime = await readContract("saturnfeeopts", "getOptionEndTime", [optionId]);
if (Date.now() / 1000 < endTime) throw new Error("Not expired yet");

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "expireOption", [from, optionId])
  .spendGas(from)
  .endScript();

Option Views

getOptionInfo()

READ
getOptionInfo(optionId: number): string

One-shot status snapshot used by options marketplace UIs. Returns an underscore-delimited string with the key fields.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
string — pool:<poolId>_targetFee:<targetFee>_premium:<premium>_token:<premiumToken>_end:<endTime>_exercised:<0|1>_status:<status>
What to expect
Split on '_' and then on ':' to extract each field. Status decodes as 0=listed, 1=active, 2=expired, 3=cancelled, 4=released.
Example
const raw = await readContract("saturnfeeopts", "getOptionInfo", [optionId]);
const parts = Object.fromEntries(raw.split("_").map(kv => kv.split(":")));
// parts.pool, parts.targetFee, parts.premium, parts.token, parts.end, parts.exercised, parts.status

getOptionPoolId()

READ
getOptionPoolId(optionId: number): number

Returns the poolId the option is written on.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
number — Underlying pool ID.
What to expect
Pair with SaturnRouter.getPoolFullInfo() to show current reserves alongside the option.
Example
const poolId = await readContract("saturnfeeopts", "getOptionPoolId", [optionId]);

getOptionWriter()

READ
getOptionWriter(optionId: number): address

Returns the pool provider who wrote (sold) the option.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
address — Option writer.
What to expect
Use this to permission-gate the cancelListing and expireOption buttons in the provider dashboard.
Example
const writer = await readContract("saturnfeeopts", "getOptionWriter", [optionId]);

getOptionBuyer()

READ
getOptionBuyer(optionId: number): address

Returns the address holding the option, or @null while still in listed state.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
address — Current option holder, or @null if unbought.
What to expect
This is the only address authorized to call exerciseOption and releaseOption.
Example
const buyer = await readContract("saturnfeeopts", "getOptionBuyer", [optionId]);

getOptionTargetFee()

READ
getOptionTargetFee(optionId: number): number

Returns the fee rate (per 10,000) that exerciseOption will set on the pool.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
number — Target fee, per 10k.
What to expect
Show alongside the pool's current fee so users can see what exercising would do.
Example
const target = await readContract("saturnfeeopts", "getOptionTargetFee", [optionId]);
const pct = (target / 100).toFixed(2) + "%";

getOptionPremium()

READ
getOptionPremium(optionId: number): number

Returns the premium price, in raw units of premiumToken.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
number — Premium amount (raw).
What to expect
Call getOptionInfo to pull the token symbol; use SaturnPools scale helpers to format human-readable.
Example
const premium = await readContract("saturnfeeopts", "getOptionPremium", [optionId]);

getOptionStatus()

READ
getOptionStatus(optionId: number): number

Returns the lifecycle status code.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
number — 0=listed, 1=active, 2=expired, 3=cancelled, 4=released.
What to expect
Filter getAllOptionIds() on status 0 for the open marketplace, status 1 for the active book.
Example
const status = await readContract("saturnfeeopts", "getOptionStatus", [optionId]);

getOptionEndTime()

READ
getOptionEndTime(optionId: number): number

Returns the Unix timestamp when the option expires. Before buy, this field holds the duration in seconds — only meaningful once status = 1.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
number — Unix seconds (while active) or duration (while listed).
What to expect
Once active, render a live countdown to expiry so buyers know how long they still have to exercise.
Example
const end = await readContract("saturnfeeopts", "getOptionEndTime", [optionId]);
const secondsLeft = end - Math.floor(Date.now() / 1000);

getNextOptionId()

READ
getNextOptionId(): number

Returns the optionId that will be assigned to the next writeOption call.

Returns
number — Next option ID (starts at 1).
What to expect
Total options ever written = getNextOptionId() - 1.
Example
const next = await readContract("saturnfeeopts", "getNextOptionId", []);

getAllOptionIds()

READ
getAllOptionIds(): number*

Generator yielding every optionId ever created.

Returns
number* — Iterable of option IDs.
What to expect
Combine with getOptionStatus() to partition into listed / active / expired / cancelled / released buckets.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnfeeopts", "getAllOptionIds", [])
  .endScript();
Financial Products · Contract #12

SaturnSyndicate

saturnsyndicate

Pool crowdfunding. Anyone can open a syndicate that names a token pair, target amounts, and a pool fee rate. Contributors deposit both tokens (up to each remaining target); once the creator is satisfied, they call activateSyndicate to deploy a real pool from the pooled capital. The syndicate contract itself becomes the pool provider, and each member's share is computed from their tokenA contribution ratio. Members claim their proportional cut of swap fees as long as the syndicate is active, and claim back proportional reserves after the creator dissolves it. During the funding phase or after cancellation, contributors can pull their tokens back with withdrawContribution.

Syndicate Lifecycle

createSyndicate()

WRITE
createSyndicate(from: address, tokenA: string, tokenB: string, targetA: number, targetB: number, feePer10k: number)

Anyone can open a new syndicate funding round for a given token pair. Sets the fundraising targets and the eventual pool's fee rate.

Parameters
NameTypeDescription
fromaddressCreator — the address that will be allowed to activate, cancel, and dissolve the syndicate. Must be a witness.
tokenAstringFirst token symbol. Validated by saturnpools.
tokenBstringSecond token symbol. Must be different from tokenA.
targetAnumberRaw funding goal for tokenA. Must be > 0.
targetBnumberRaw funding goal for tokenB. Must be > 0.
feePer10knumberFee rate of the resulting pool, per 10,000. Must be within SaturnAdmin.getPoolFeeRange.
What to expect
A new syndicateId is created in status 0 (funding). No tokens are moved yet — contributors call contribute() next. Creator retains permission to cancelSyndicate, activateSyndicate, and dissolveSyndicate.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "createSyndicate", [
    from,
    "SOUL",
    "KCAL",
    100000_00000000,   // target 100000 SOUL raw
    500000_00000000,   // target 500000 KCAL raw
    30                 // 0.30% pool fee
  ])
  .spendGas(from)
  .endScript();

cancelSyndicate()

WRITE
cancelSyndicate(from: address, syndicateId: number)

Creator cancels a syndicate before it has been activated. Contributors must then call withdrawContribution to pull their tokens back.

Parameters
NameTypeDescription
fromaddressMust be the syndicate creator.
syndicateIdnumberA syndicate in status 0 (funding).
What to expect
Status flips to 3 (cancelled). No tokens are returned automatically — every contributor calls withdrawContribution individually to reclaim their deposits.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "cancelSyndicate", [from, syndicateId])
  .spendGas(from)
  .endScript();

contribute()

WRITE
contribute(from: address, syndicateId: number, amountA: number, amountB: number)

Investor deposits both tokens into an open syndicate. Each contribution may not exceed the remaining distance to its target; repeated contributions from the same address are aggregated.

Parameters
NameTypeDescription
fromaddressContributor. Must be a witness.
syndicateIdnumberA syndicate in status 0 (funding).
amountAnumberRaw tokenA to deposit. Must be > 0 and <= remaining targetA.
amountBnumberRaw tokenB to deposit. Must be > 0 and <= remaining targetB.
What to expect
Both tokens are transferred from you to the syndicate contract in two Token.transfer calls. memberContribA/B grow, raisedA/B grow, and memberCount increments on your first contribution. Your final share of fees is proportional to your tokenA contribution vs totalRaisedA.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "contribute", [
    from,
    syndicateId,
    amountARaw,
    amountBRaw
  ])
  .spendGas(from)
  .endScript();

withdrawContribution()

WRITE
withdrawContribution(from: address, syndicateId: number)

Contributor reclaims their deposit. Works while the syndicate is still in funding (status 0) or after it has been cancelled (status 3). Withdraws the entire outstanding contribution in one call.

Parameters
NameTypeDescription
fromaddressMust be a member with a non-zero recorded contribution.
syndicateIdnumberA syndicate in status 0 or 3.
What to expect
Your full contribA and contribB are transferred back, your member entry is zeroed out, the syndicate's raisedA/B and memberCount decrement. Reverts if you have nothing to withdraw.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "withdrawContribution", [from, syndicateId])
  .spendGas(from)
  .endScript();

activateSyndicate()

WRITE
activateSyndicate(from: address, syndicateId: number)

Creator converts the pooled capital into a real pool. The syndicate contract itself becomes the pool provider, and the pool is immediately financial-locked so nobody can accidentally remove it.

Parameters
NameTypeDescription
fromaddressMust be the syndicate creator.
syndicateIdnumberA syndicate in status 0 with raisedA > 0 AND raisedB > 0.
What to expect
saturnpools.registerPool is called with $THIS_ADDRESS as the provider; the returned poolId is stored. Status becomes 1 (active) and the pool's financialLockCount increments by 1. From now on members can call claimSyndicateReward to harvest their share of swap fees. The creator does NOT have to fully hit targets — whatever has been raised becomes the initial reserves.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "activateSyndicate", [from, syndicateId])
  .spendGas(from)
  .endScript();

claimSyndicateReward()

WRITE
claimSyndicateReward(from: address, syndicateId: number)

Member harvests their proportional share of fees accrued on the syndicate pool. Share is (contribA * 10000) / totalRaisedA basis points.

Parameters
NameTypeDescription
fromaddressMust be a member of the syndicate.
syndicateIdnumberAn active syndicate (status 1).
What to expect
The syndicate pulls all pending provider fees for the pool (in both tokens), scales them down to raw units, and transfers your sharePer10k / 10000 slice to you in both tokens. Callable repeatedly as more fees accrue.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "claimSyndicateReward", [from, syndicateId])
  .spendGas(from)
  .endScript();

dissolveSyndicate()

WRITE
dissolveSyndicate(from: address, syndicateId: number)

Creator winds the syndicate down. Pool's financial lock is released, any pending fees are swept into the syndicate contract, and the pool reserves are cleared. Members then call claimDissolution to get their share of what remains.

Parameters
NameTypeDescription
fromaddressMust be the syndicate creator.
syndicateIdnumberAn active syndicate (status 1).
What to expect
Pool financialLockCount decrements, remaining provider fees are claimed, pool reserves are cleared (pool becomes inactive), and status becomes 2 (dissolved). Reserves now sit as the syndicate contract's token balance awaiting individual claimDissolution calls.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "dissolveSyndicate", [from, syndicateId])
  .spendGas(from)
  .endScript();

claimDissolution()

WRITE
claimDissolution(from: address, syndicateId: number)

After a syndicate has been dissolved, each member claims their proportional share of the remaining reserves in both tokens. Single-claim only — the member entry is zeroed on success.

Parameters
NameTypeDescription
fromaddressMust be a member with a non-zero recorded contribution.
syndicateIdnumberA dissolved syndicate (status 2).
What to expect
Your slice = (syndicateContractBalance * sharePer10k) / 10000 of each token. Your contribA/B record is cleared to prevent double-claim. Any rounding dust is left in the contract balance.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "claimDissolution", [from, syndicateId])
  .spendGas(from)
  .endScript();

Syndicate Views

getSyndicateInfo()

READ
getSyndicateInfo(syndicateId: number): string

One-shot status snapshot for marketplace UIs. Returns an underscore-delimited string with the key fields.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
string — tokenA:<sym>_tokenB:<sym>_targetA:<raw>_targetB:<raw>_raisedA:<raw>_raisedB:<raw>_fee:<per10k>_pool:<poolId>_status:<status>_members:<count>
What to expect
Split on '_' and then on ':' to decode. Status: 0=funding, 1=active, 2=dissolved, 3=cancelled. Pool field is 0 until activation.
Example
const raw = await readContract("saturnsyndicate", "getSyndicateInfo", [syndicateId]);
const parts = Object.fromEntries(raw.split("_").map(kv => kv.split(":")));

getSyndicateTokenA()

READ
getSyndicateTokenA(syndicateId: number): string

Returns tokenA symbol for this syndicate.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
string — tokenA symbol.
What to expect
Pair with getSyndicateRaisedA and getSyndicateTokenB for contribution UIs.
Example
const tA = await readContract("saturnsyndicate", "getSyndicateTokenA", [syndicateId]);

getSyndicateTokenB()

READ
getSyndicateTokenB(syndicateId: number): string

Returns tokenB symbol for this syndicate.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
string — tokenB symbol.
What to expect
Used for the second side of contribute() input validation.
Example
const tB = await readContract("saturnsyndicate", "getSyndicateTokenB", [syndicateId]);

getSyndicatePoolId()

READ
getSyndicatePoolId(syndicateId: number): number

Returns the poolId created by activateSyndicate, or 0 if not yet active.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
number — Underlying pool ID or 0.
What to expect
Once non-zero, chain to SaturnRouter.getPoolFullInfo to show live pool stats.
Example
const poolId = await readContract("saturnsyndicate", "getSyndicatePoolId", [syndicateId]);

getSyndicateStatus()

READ
getSyndicateStatus(syndicateId: number): number

Returns the lifecycle status code.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
number — 0=funding, 1=active, 2=dissolved, 3=cancelled.
What to expect
Drive different UI states — funding shows contribute button, active shows claim rewards, dissolved shows claim dissolution.
Example
const status = await readContract("saturnsyndicate", "getSyndicateStatus", [syndicateId]);

getSyndicateRaisedA()

READ
getSyndicateRaisedA(syndicateId: number): number

Returns total raw tokenA raised so far.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
number — Raised tokenA (raw).
What to expect
Render raisedA / targetA as a progress bar; use to compute your personal share ratio for fee estimates.
Example
const raisedA = await readContract("saturnsyndicate", "getSyndicateRaisedA", [syndicateId]);

getSyndicateRaisedB()

READ
getSyndicateRaisedB(syndicateId: number): number

Returns total raw tokenB raised so far.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
number — Raised tokenB (raw).
What to expect
Render raisedB / targetB as a progress bar.
Example
const raisedB = await readContract("saturnsyndicate", "getSyndicateRaisedB", [syndicateId]);

getSyndicateMemberCount()

READ
getSyndicateMemberCount(syndicateId: number): number

Returns the number of distinct contributors with currently non-zero recorded contributions.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
Returns
number — Active member count.
What to expect
Decrements when a member fully withdraws; goes to zero if everyone withdraws from a cancelled syndicate.
Example
const members = await readContract("saturnsyndicate", "getSyndicateMemberCount", [syndicateId]);

getNextSyndicateId()

READ
getNextSyndicateId(): number

Returns the syndicateId that will be assigned to the next createSyndicate call.

Returns
number — Next syndicate ID (starts at 1).
What to expect
Total syndicates ever created = getNextSyndicateId() - 1.
Example
const next = await readContract("saturnsyndicate", "getNextSyndicateId", []);

getMemberContribution()

READ
getMemberContribution(syndicateId: number, member: address): string

Returns a single member's outstanding recorded contribution in both tokens. Used by the 'My contribution' panel.

Parameters
NameTypeDescription
syndicateIdnumberThe syndicate to inspect.
memberaddressThe member address to look up.
Returns
string — contribA:<raw>_contribB:<raw>
What to expect
Both fields are zero after a member fully withdraws or after claimDissolution. Compute share = contribA * 10000 / totalRaisedA for reward estimates.
Example
const raw = await readContract("saturnsyndicate", "getMemberContribution", [syndicateId, member]);
const [a, b] = raw.split("_").map(p => p.split(":")[1]);

getAllSyndicateIds()

READ
getAllSyndicateIds(): number*

Generator yielding every syndicateId ever created.

Returns
number* — Iterable of syndicate IDs.
What to expect
Combine with getSyndicateStatus() to build funding / active / dissolved / cancelled tabs in the explorer.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnsyndicate", "getAllSyndicateIds", [])
  .endScript();
Financial Products · Contract #17

SaturnLaunchpad

saturnlaunchpad

SaturnLaunchpad lets any developer or project creator run a fixed-price token sale (launchpad) that, on success, bootstraps a Saturn DEX liquidity pool from the raised funds. Buyers commit quote tokens at a fixed price during a capped funding window; when the minimum fill threshold is met the creator activates the launch, the proceeds seed the pool, and all participants earn trading fees as co-liquidity-providers. If the launch fails or buyers vote to dissolve, every participant can reclaim their funds through dedicated refund paths.

Create & Configure Launch

createLaunchpad()

WRITE
createLaunchpad(from: address, tokenA: string, tokenQuote: string, tokensForSale: number, quotePerA: number, poolFeePer10k: number, minFillPer10k: number, minCommitQuote: number, endTime: number)

Creates a new fixed-price launchpad. The creator deposits the full `tokensForSale` amount of `tokenA` into escrow at creation time. The sale runs until `endTime` (max 14 days from now) or until the allocation sells out. On activation the sale proceeds and unsold token A seed a Saturn pool at the fee tier set by `poolFeePer10k`. Call `saturnadmin.getMinPoolFeePer10k()` and `getMaxPoolFeePer10k()` to clamp the fee input before submitting. `minFillPer10k` controls the minimum fill required to activate (1000 = 10%, 10000 = 100% sellout required). `minCommitQuote` is the smallest individual commitment accepted.

Parameters
NameTypeDescription
fromaddressCreator's address; must be the transaction witness. Receives unsold tokenA back at activation.
tokenAstringSymbol of the token being sold.
tokenQuotestringSymbol of the token buyers pay with (e.g. SOUL, KCAL, USDC).
tokensForSalenumberTotal raw units of tokenA being offered. Must be > 0 and available in creator's balance.
quotePerAnumberFixed price: how many raw units of tokenQuote buy exactly 1 raw unit of tokenA. Must be > 0.
poolFeePer10knumberTrading fee for the post-launch pool, in basis points per 10,000 (e.g. 30 = 0.3%). Must be within admin-configured min/max.
minFillPer10knumberMinimum fill needed to activate, per 10,000 of tokensForSale. Range: 1000–10000 (10%–100%).
minCommitQuotenumberMinimum quote amount per individual commitment. Must be > 0 and a multiple of quotePerA makes clean allocations.
endTimenumberUnix timestamp when the funding window closes. Must be in the future and at most 14 days (1209600 s) from now.
What to expect
Reverts if: creator lacks `tokensForSale` of tokenA; endTime is not in the future or exceeds the 14-day cap; minFillPer10k is outside [1000, 10000]; poolFeePer10k is outside admin range; tokenA == tokenQuote. Emits LaunchpadCreated. Returns the new launchpad ID implicitly — read `getNextLaunchpadId()` before calling to know the upcoming ID, or watch the LaunchpadCreated event.
Example
const from = "S3myAddress...";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "createLaunchpad", [
    from,
    "MYTOKEN",     // tokenA
    "SOUL",        // tokenQuote
    1000000,       // 1,000,000 MYTOKEN for sale
    100,           // 100 SOUL per 1 MYTOKEN
    30,            // 0.3% pool fee
    5000,          // 50% minimum fill to activate
    1000,          // min 1,000 SOUL per commit
    Math.floor(Date.now() / 1000) + 86400 * 7  // 7-day window
  ])
  .spendGas(from)
  .endScript();

cancelLaunchpad()

WRITE
cancelLaunchpad(from: address, launchpadId: number)

Allows the creator to abort their launchpad before any buyers have committed. Immediately returns all escrowed tokenA to the creator and sets the launchpad status to Cancelled (3). Buyers can still call `withdrawCommit` after cancellation, but because no one can commit once the sale ends this is primarily a pre-buyer cleanup path. If buyers have already committed use `proposeDissolve` / `executeDissolve` instead.

Parameters
NameTypeDescription
fromaddressCreator's address; must be the transaction witness.
launchpadIdnumberID of the launchpad to cancel.
What to expect
Reverts if: `from` is not the creator; launchpad is not in funding state (status 0); buyer count > 0. Emits LaunchpadCancelled. After this call `getLaunchpadStatus` returns 3 and `getLaunchpadCreatorReclaimed` returns 1.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "cancelLaunchpad", [from, launchpadId])
  .spendGas(from)
  .endScript();

Contribute

commit()

WRITE
commit(from: address, launchpadId: number, amountQuote: number)

Commits quote tokens to a live launchpad, reserving the equivalent tokenA allocation at the fixed price. `amountQuote` must be a multiple of `quotePerA` so the allocation is exact. Multiple calls from the same address accumulate; the buyer's total commitment determines their pool share weight if the launch activates. Buyers cannot commit after `endTime` even if the launchpad status is still 0.

Parameters
NameTypeDescription
fromaddressBuyer's address; must be the transaction witness. Cannot be the launchpad creator.
launchpadIdnumberID of the launchpad to commit to.
amountQuotenumberRaw units of tokenQuote to commit. Must be >= minCommitQuote and a multiple of quotePerA. Must not push soldA beyond tokensForSale.
What to expect
Reverts if: launchpad status != 0; now >= endTime; from == creator; amountQuote < minCommitQuote; amountQuote % quotePerA != 0; commit would oversell the allocation; buyer lacks sufficient tokenQuote balance. Emits CommitMade with the updated total raisedQuote.
Example
const from = "S3buyerAddress...";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "commit", [from, 3, 5000])
  .spendGas(from)
  .endScript();

withdrawCommit()

WRITE
withdrawCommit(from: address, launchpadId: number)

Withdraws the caller's entire committed quote amount and re-opens their reserved tokenA allocation for other buyers. Can be called while the launchpad is in funding state (status 0) — allowing buyers to back out before activation — or after cancellation (status 3). Clears vote state and reward debt so the address is fully reset. If the launchpad is already active (status 1) use `proposeDissolve` instead.

Parameters
NameTypeDescription
fromaddressBuyer's address; must be the transaction witness.
launchpadIdnumberID of the launchpad to withdraw from.
What to expect
Reverts if: launchpad status is not 0 (funding) or 3 (cancelled); the caller has no committed quote. Returns the full committed quote token amount. Emits CommitWithdrawn.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "withdrawCommit", [from, launchpadId])
  .spendGas(from)
  .endScript();

Finalize / Lifecycle

activateLaunchpad()

WRITE
activateLaunchpad(from: address, launchpadId: number)

Called by the creator to finalize a successful launch. Transfers the raised quote tokens and the sold tokenA into a new Saturn pool, registers the pool, and locks it under the launchpad's financial lock. The creator's share weight is set to equal the total raised quote (matching buyers 1:1 so the creator holds ~50% of pool fees). Any unsold tokenA is returned to the creator immediately. After activation all participants earn trading fees proportional to their share weight.

Parameters
NameTypeDescription
fromaddressCreator's address; must be the transaction witness.
launchpadIdnumberID of the launchpad to activate.
What to expect
Reverts if: from is not the creator; status != 0; soldA == 0; fill ratio soldA/tokensForSale < minFillPer10k/10000 (and it is not a full sellout); either soldA or raisedQuote is below the protocol's minimum pool size. Emits LaunchpadActivated with poolId, soldA, raisedQuote, and unsoldReturned. After success `getLaunchpadStatus` returns 1 and `getLaunchpadPoolId` returns the new pool ID.
Example
const tx = sb.begin()
  .allowGas(creatorAddress)
  .callContract("saturnlaunchpad", "activateLaunchpad", [creatorAddress, launchpadId])
  .spendGas(creatorAddress)
  .endScript();

dissolveViaTimeout()

WRITE
dissolveViaTimeout(from: address, launchpadId: number)

Anyone can call this once `endTime` has passed and the launchpad is still in funding state (status 0) without having been activated. Marks the launchpad as dissolved (status 2) and triggers the refund path. Useful when the creator is absent or a buyer wants to recover their funds immediately after the window expires. Does not require a proposal or vote since the timeout is the objective trigger.

Parameters
NameTypeDescription
fromaddressAny witness address — typically a buyer triggering the cleanup.
launchpadIdnumberID of the launchpad to dissolve.
What to expect
Reverts if: launchpad status != 0; now < endTime. Emits LaunchpadDissolved with reasonCode 2. After this call participants use `claimFundingRefund` to recover funds.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "dissolveViaTimeout", [from, launchpadId])
  .spendGas(from)
  .endScript();

proposeDissolve()

WRITE
proposeDissolve(from: address, launchpadId: number)

Opens a dissolution proposal for an active or funding launchpad. The proposer must be a buyer (committed quote > 0); the creator cannot propose. The proposer's committed quote is automatically counted as the first vote. A 72-hour timelock (259,200 seconds) starts from proposal time before `executeDissolve` can be called. Only one proposal can be open at a time.

Parameters
NameTypeDescription
fromaddressA buyer's address; must be the transaction witness. Creator is excluded.
launchpadIdnumberID of the launchpad to target.
What to expect
Reverts if: status is not 0 or 1; a proposal is already open; from is the creator; from has no committed quote. Emits LaunchpadDissolveProposed with the earliest execution timestamp (proposedAt + 259200).
Example
const tx = sb.begin()
  .allowGas(buyerAddress)
  .callContract("saturnlaunchpad", "proposeDissolve", [buyerAddress, launchpadId])
  .spendGas(buyerAddress)
  .endScript();

voteDissolve()

WRITE
voteDissolve(from: address, launchpadId: number)

Adds the caller's committed quote weight to the open dissolution vote. Each buyer can vote once. Votes are weighted by the buyer's committed quote amount so larger positions carry more weight. A strict majority of total buyer stake (> 50%) is needed for `executeDissolve` to succeed.

Parameters
NameTypeDescription
fromaddressA buyer who has not yet voted; must be the transaction witness.
launchpadIdnumberID of the launchpad with the open proposal.
What to expect
Reverts if: status is not 0 or 1; no proposal is open; from is the creator; from has no committed quote; from has already voted. Emits LaunchpadDissolveVoted with the new cumulative vote total.
Example
const tx = sb.begin()
  .allowGas(buyerAddress)
  .callContract("saturnlaunchpad", "voteDissolve", [buyerAddress, launchpadId])
  .spendGas(buyerAddress)
  .endScript();

executeDissolve()

WRITE
executeDissolve(from: address, launchpadId: number)

Executes an approved dissolution proposal once the 72-hour timelock has elapsed and a strict majority of buyer stake has voted yes. For an active launchpad (status 1) this harvests any pending fees, removes pool reserves, and makes the funds claimable via `claimDissolution`. For a funding-state launchpad it simply marks it dissolved, and buyers use `claimFundingRefund`.

Parameters
NameTypeDescription
fromaddressAny buyer (not the creator); must be the transaction witness.
launchpadIdnumberID of the launchpad to dissolve.
What to expect
Reverts if: status is not 0 or 1; no proposal is open; now < proposedAt + 259200 (72h timelock); from is the creator; from has no committed quote; cumulative votes * 2 <= totalBuyerStake (majority not reached). If the launchpad is active, additionally reverts if the pool has more than one financial lock or any active reward campaign. Emits LaunchpadDissolved with reasonCode 1.
Example
// Check readiness first
const earliestExec = await readContract("saturnlaunchpad", "getDissolveEarliestExecute", [launchpadId]);
const votes = await readContract("saturnlaunchpad", "getDissolveVotes", [launchpadId]);
const raised = await readContract("saturnlaunchpad", "getLaunchpadRaisedQuote", [launchpadId]);
// votes * 2 > raised => majority confirmed

const tx = sb.begin()
  .allowGas(buyerAddress)
  .callContract("saturnlaunchpad", "executeDissolve", [buyerAddress, launchpadId])
  .spendGas(buyerAddress)
  .endScript();

Claim & Refund

claimLaunchpadReward()

WRITE
claimLaunchpadReward(from: address, launchpadId: number)

Harvests accumulated trading-fee rewards for a participant in an active launchpad (status 1). Both the creator and buyers earn fees proportional to their share weight (buyers: their committed quote; creator: an equal weight to the total raised quote, set at activation). Triggers a fee harvest from the underlying pool before computing pending amounts, so the payout reflects fees earned up to this block. Can be called as often as desired.

Parameters
NameTypeDescription
fromaddressParticipant's address (creator or buyer); must be the transaction witness.
launchpadIdnumberID of the active launchpad.
What to expect
Reverts if: launchpad status != 1; from has no share weight (not a participant). Transfers pending tokenA and tokenQuote rewards. Emits LaunchpadRewardsClaimed. Pending amounts may be 0 if no new fees have accrued since the last claim.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "claimLaunchpadReward", [from, launchpadId])
  .spendGas(from)
  .endScript();

claimDissolution()

WRITE
claimDissolution(from: address, launchpadId: number)

Claims a participant's share of the pool reserves after a post-activation dissolution (status 2 with creatorShareWeight > 0). Each participant receives their proportional share of dissolved tokenA and tokenQuote reserves plus any uncollected fee rewards. The creator and each buyer call this independently. This path is only valid for launchpads that were activated before being dissolved; use `claimFundingRefund` for pre-activation dissolutions.

Parameters
NameTypeDescription
fromaddressParticipant's address (creator or buyer); must be the transaction witness.
launchpadIdnumberID of the dissolved launchpad.
What to expect
Reverts if: status != 2; creatorShareWeight == 0 (pre-activation dissolve — use claimFundingRefund instead); from has no share weight or has already claimed. Emits LaunchpadDissolutionClaimed with payoutA and payoutB amounts.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnlaunchpad", "claimDissolution", [from, launchpadId])
  .spendGas(from)
  .endScript();

claimFundingRefund()

WRITE
claimFundingRefund(from: address, launchpadId: number)

Refunds participants after a pre-activation dissolution (status 2, creatorShareWeight == 0) — i.e. the launchpad was dissolved by timeout or vote before `activateLaunchpad` was ever called. The creator reclaims their full escrowed tokenA; each buyer reclaims their full committed tokenQuote. The original `tokensForSale` value is preserved for indexers (v4.1.1 audit fix) via a dedicated reclaim flag rather than zeroing the field.

Parameters
NameTypeDescription
fromaddressCreator or buyer address; must be the transaction witness.
launchpadIdnumberID of the dissolved launchpad.
What to expect
Reverts if: status != 2; creatorShareWeight > 0 (launchpad was activated — use claimDissolution); caller is the creator and has already reclaimed (launchpadCreatorReclaimed == 1); caller is a buyer with no committed quote. Emits LaunchpadDissolutionClaimed.
Example
// Creator reclaims their tokenA:
const creatorTx = sb.begin()
  .allowGas(creatorAddress)
  .callContract("saturnlaunchpad", "claimFundingRefund", [creatorAddress, launchpadId])
  .spendGas(creatorAddress)
  .endScript();

// Buyer reclaims their tokenQuote:
const buyerTx = sb.begin()
  .allowGas(buyerAddress)
  .callContract("saturnlaunchpad", "claimFundingRefund", [buyerAddress, launchpadId])
  .spendGas(buyerAddress)
  .endScript();

Views — Launchpad Info

getContractVersion()

READ
getContractVersion(): string

Returns the contract version string. Use to verify which deployment you are talking to.

Returns
string — e.g. "saturnlaunchpad-4.1.2"
Example
const version = await readContract("saturnlaunchpad", "getContractVersion", []);

getLaunchpadInfo()

READ
getLaunchpadInfo(launchpadId: number): string

Returns a packed string summary of a launchpad's key parameters in one call, avoiding N individual round-trips. Format: `tokenA:<sym>_tokenQuote:<sym>_forSale:<n>_price:<n>_sold:<n>_raised:<n>_status:<n>_pool:<n>_buyers:<n>`. Status codes: 0 = Funding, 1 = Active, 2 = Dissolved, 3 = Cancelled.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad to query.
Returns
string — Underscore-delimited key:value summary string.
Example
const info = await readContract("saturnlaunchpad", "getLaunchpadInfo", [3]);
// "tokenA:MYTOKEN_tokenQuote:SOUL_forSale:1000000_price:100_sold:600000_raised:60000000_status:0_pool:0_buyers:4"

getLaunchpadCreator()

READ
getLaunchpadCreator(launchpadId: number): address

Returns the address that created the launchpad.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
address — Creator's address.

getLaunchpadTokenA()

READ
getLaunchpadTokenA(launchpadId: number): string

Returns the symbol of the token being sold.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
string — Token symbol (e.g. "MYTOKEN").

getLaunchpadTokenQuote()

READ
getLaunchpadTokenQuote(launchpadId: number): string

Returns the symbol of the quote token buyers pay with.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
string — Quote token symbol (e.g. "SOUL").

getLaunchpadTokensForSale()

READ
getLaunchpadTokensForSale(launchpadId: number): number

Returns the original total tokenA amount offered. This field retains its original value even after dissolution (v4.1.1 audit fix).

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Raw units of tokenA originally for sale.

getLaunchpadQuotePerA()

READ
getLaunchpadQuotePerA(launchpadId: number): number

Returns the fixed price: how many raw units of tokenQuote purchase one raw unit of tokenA.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Quote units per tokenA unit.

getLaunchpadPoolFeePer10k()

READ
getLaunchpadPoolFeePer10k(launchpadId: number): number

Returns the trading fee rate (basis points per 10,000) configured for the post-launch pool.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Fee in bps/10k (e.g. 30 = 0.3%).

getLaunchpadMinFillPer10k()

READ
getLaunchpadMinFillPer10k(launchpadId: number): number

Returns the minimum fill threshold per 10,000 of tokensForSale required to activate.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Min fill ratio (1000–10000).

getLaunchpadMinCommitQuote()

READ
getLaunchpadMinCommitQuote(launchpadId: number): number

Returns the minimum quote amount accepted per individual commit call.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Raw units of tokenQuote.

getLaunchpadEndTime()

READ
getLaunchpadEndTime(launchpadId: number): number

Returns the Unix timestamp at which the funding window closes. Commits are rejected at or after this time.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Unix timestamp (seconds).

getLaunchpadSoldA()

READ
getLaunchpadSoldA(launchpadId: number): number

Returns how many raw units of tokenA have been reserved by buyers so far.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Raw units of tokenA sold/reserved.

getLaunchpadRaisedQuote()

READ
getLaunchpadRaisedQuote(launchpadId: number): number

Returns the total raw units of tokenQuote committed by all buyers. This doubles as the total buyer share weight.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Total tokenQuote committed.

getLaunchpadBuyerCount()

READ
getLaunchpadBuyerCount(launchpadId: number): number

Returns the number of distinct buyer addresses that have an active commitment.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Number of unique buyers with non-zero commitments.

getLaunchpadStatus()

READ
getLaunchpadStatus(launchpadId: number): number

Returns the launchpad lifecycle status. 0 = Funding, 1 = Active (pool live), 2 = Dissolved, 3 = Cancelled.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Status code: 0 Funding | 1 Active | 2 Dissolved | 3 Cancelled.

getLaunchpadPoolId()

READ
getLaunchpadPoolId(launchpadId: number): number

Returns the Saturn pool ID that was created when the launchpad activated. Returns 0 if not yet activated.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Pool ID, or 0 if not yet active.

getLaunchpadCreatorShareWeight()

READ
getLaunchpadCreatorShareWeight(launchpadId: number): number

Returns the creator's fee-sharing weight. Set to `raisedQuote` at activation (equal to total buyer stake) and zeroed when the creator claims dissolution. A value of 0 on a dissolved launchpad means the creator has either already claimed or the launchpad dissolved pre-activation.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Creator share weight in quote units.

getLaunchpadCreatorReclaimed()

READ
getLaunchpadCreatorReclaimed(launchpadId: number): number

Returns 1 if the creator has already reclaimed their escrowed tokenA after a pre-activation dissolution or cancellation, 0 if not yet reclaimed.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — 0 = not yet reclaimed; 1 = already reclaimed.

getNextLaunchpadId()

READ
getNextLaunchpadId(): number

Returns the ID that will be assigned to the next launchpad. Read this before `createLaunchpad` to predict the upcoming ID for event matching.

Returns
number — Next launchpad ID (starts at 1, increments by 1).
Example
const nextId = await readContract("saturnlaunchpad", "getNextLaunchpadId", []);

getBuyerCommittedQuote()

READ
getBuyerCommittedQuote(launchpadId: number, buyer: address): number

Returns the total raw tokenQuote amount currently committed by a specific buyer in a launchpad. Returns 0 if the buyer has not committed or has fully withdrawn.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
buyeraddressAddress to query.
Returns
number — Committed tokenQuote amount, or 0.
Example
const committed = await readContract("saturnlaunchpad", "getBuyerCommittedQuote", [3, "S3buyerAddress..."]);

getLaunchpadAccFeePerShareA()

READ
getLaunchpadAccFeePerShareA(launchpadId: number): number

Returns the accumulated fee-per-share accumulator for tokenA (scaled by 1e12). Used internally to compute pending rewards; expose in UIs for advanced fee accounting.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Accumulated tokenA fee per share * 1e12.

getLaunchpadAccFeePerShareB()

READ
getLaunchpadAccFeePerShareB(launchpadId: number): number

Returns the accumulated fee-per-share accumulator for tokenQuote (tokenB), scaled by 1e12.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Accumulated tokenQuote fee per share * 1e12.

getLaunchpadDissolvedResA()

READ
getLaunchpadDissolvedResA(launchpadId: number): number

Returns the raw tokenA reserves captured at dissolution. Used to compute each participant's claimDissolution payout.

Parameters
NameTypeDescription
launchpadIdnumberID of the dissolved launchpad.
Returns
number — Raw tokenA dissolved reserve; 0 if not dissolved or pre-activation dissolve.

getLaunchpadDissolvedResB()

READ
getLaunchpadDissolvedResB(launchpadId: number): number

Returns the raw tokenQuote reserves captured at dissolution.

Parameters
NameTypeDescription
launchpadIdnumberID of the dissolved launchpad.
Returns
number — Raw tokenQuote dissolved reserve; 0 if not dissolved or pre-activation dissolve.

Views — Dissolution & Governance

getDissolveProposed()

READ
getDissolveProposed(launchpadId: number): number

Returns 1 if a dissolution proposal is currently open, 0 otherwise.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — 1 = proposal open; 0 = none.

getDissolveVotes()

READ
getDissolveVotes(launchpadId: number): number

Returns the total committed-quote weight of all votes cast for the current dissolution proposal. Compare to `getLaunchpadRaisedQuote` to gauge majority: votes * 2 > raisedQuote means the vote has passed.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Cumulative vote weight in raw tokenQuote units.
Example
const votes = await readContract("saturnlaunchpad", "getDissolveVotes", [launchpadId]);
const raised = await readContract("saturnlaunchpad", "getLaunchpadRaisedQuote", [launchpadId]);
const majorityReached = votes * 2 > raised;

getDissolveProposedAt()

READ
getDissolveProposedAt(launchpadId: number): number

Returns the Unix timestamp when the current dissolution proposal was opened. Returns 0 if no proposal exists.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Unix timestamp of proposal, or 0.

getDissolveEarliestExecute()

READ
getDissolveEarliestExecute(launchpadId: number): number

Returns the earliest Unix timestamp at which `executeDissolve` can be called (proposedAt + 72 hours). Returns 0 if no proposal exists. Use this to show a countdown timer in your UI.

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
Returns
number — Earliest executable timestamp, or 0 if no proposal.
Example
const earliest = await readContract("saturnlaunchpad", "getDissolveEarliestExecute", [launchpadId]);
const secondsRemaining = Math.max(0, earliest - Math.floor(Date.now() / 1000));

getBuyerHasVoted()

READ
getBuyerHasVoted(launchpadId: number, buyer: address): number

Returns 1 if the given buyer has already cast their dissolution vote for the open proposal, 0 otherwise. Also returns 0 after the buyer's commitment is cleared (e.g. after withdrawCommit).

Parameters
NameTypeDescription
launchpadIdnumberID of the launchpad.
buyeraddressBuyer address to check.
Returns
number — 1 = voted; 0 = not voted.
Example
const hasVoted = await readContract("saturnlaunchpad", "getBuyerHasVoted", [launchpadId, "S3buyerAddress..."]);

Views — Lists & Batch Data

getAllLaunchpadIds()

READ
getAllLaunchpadIds(): number*

Returns all launchpad IDs ever created, in creation order. The return type `number*` is a Tomb generator/stream; the Phantasma SDK deserializes it as an array. Use to build a full launchpad registry.

Returns
number* — Stream of all launchpad IDs.
Example
const ids = await readContract("saturnlaunchpad", "getAllLaunchpadIds", []);

getActiveLaunchpadIds()

READ
getActiveLaunchpadIds(): number*

Returns only IDs of launchpads in Active state (status 1 — pool is live). Useful for listing post-launch pools that are still earning fees.

Returns
number* — Stream of active launchpad IDs.
Example
const activeIds = await readContract("saturnlaunchpad", "getActiveLaunchpadIds", []);

getFundingLaunchpadIds()

READ
getFundingLaunchpadIds(): number*

Returns only IDs of launchpads currently in Funding state (status 0). Use to populate an "open sales" listing.

Returns
number* — Stream of funding launchpad IDs.
Example
const fundingIds = await readContract("saturnlaunchpad", "getFundingLaunchpadIds", []);

getActiveLaunchpadsData()

READ
getActiveLaunchpadsData(): string*

Returns a stream of encoded rows for all active launchpads (status 1) in one call, eliminating N per-field round-trips. Each row is pipe-delimited: launchpadId|tokenA|tokenQuote|tokensForSale|quotePerA|soldA|raisedQuote|buyerCount|status|poolId|endTime.

Returns
string* — Stream of pipe-delimited launchpad summary rows.
Example
const rows = await readContract("saturnlaunchpad", "getActiveLaunchpadsData", []);
// rows[0] => "3|MYTOKEN|SOUL|1000000|100|1000000|100000000|12|1|42|1704067200"

getUserLaunchpadsData()

READ
getUserLaunchpadsData(user: address): string*

Returns a stream of encoded launchpad rows for every launchpad in which the given user address currently has a non-zero committed quote. Same row format as getActiveLaunchpadsData. Use to show a user's portfolio of active commitments.

Parameters
NameTypeDescription
useraddressAddress to look up.
Returns
string* — Stream of pipe-delimited rows for launchpads the user has committed to.
Example
const myLaunchpads = await readContract("saturnlaunchpad", "getUserLaunchpadsData", ["S3myAddress..."]);
Agent Automation · Contract #13

SaturnArbitrage

saturnarb

Atomic cross-pool arbitrage for agents. No flash loans needed — the executor provides the starting capital in tokenStart, the contract routes through two pools that share the same pair (buy cheap on one, sell expensive on the other), and the net gain is split between the executor and the protocol treasury. If the round-trip does not produce strictly more tokenStart than it started with, or if the profit is below minProfit, the entire transaction reverts — there is no downside beyond gas. Agents monitor on-chain prices, compute opportunities off-chain, and fire executeArbitrage when they find a profitable path.

Arbitrage Execution

executeArbitrage()

WRITE
executeArbitrage(from: address, poolIdBuy: number, poolIdSell: number, tokenStart: string, amountIn: number, minProfit: number)

Atomic two-hop arbitrage. Swaps amountIn of tokenStart into the intermediate token on poolIdBuy (the cheaper pool), then swaps the intermediate back to tokenStart on poolIdSell (the more expensive pool). The intermediate token is inferred from poolIdBuy — whichever side isn't tokenStart. Both pools must contain the exact same pair. On success you receive your original capital plus your share of the profit in a single transfer; the protocol's share goes to the admin treasury.

Parameters
NameTypeDescription
fromaddressExecutor address — must be a witness and must hold amountIn of tokenStart.
poolIdBuynumberPool where tokenStart is cheap — the first hop swaps tokenStart into the intermediate token here.
poolIdSellnumberPool where tokenStart is expensive — the second hop swaps the intermediate back to tokenStart here. Must share the same pair as poolIdBuy and must be different from it.
tokenStartstringToken you provide and receive back. Must be one of the two tokens in both pools.
amountInnumberRaw amount of tokenStart to commit to the arb. Must be > 0 and <= your wallet balance.
minProfitnumberMinimum acceptable profit, in raw tokenStart units (pre-split). Set to your gas cost + desired buffer; the transaction reverts with 'Profit below minimum' if not met.
What to expect
amountIn tokenStart is moved from your wallet to the SwapEngine, then routed through two swaps against the SwapEngine. If finalAmount > amountIn and profit >= minProfit, you receive (amountIn + executorSharePer100% of profit) back and the protocol receives the remainder — both in a single tx. If either condition fails the whole call reverts and nothing moves. Your personal stats (executorArbCount, executorTotalProfit) update on success.
Example
// Off-chain: compare prices and pick the direction
const executorShare = await readContract("saturnarb", "getExecutorSharePer100", []);

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnarb", "executeArbitrage", [
    from,
    cheapPoolId,   // buy side
    richPoolId,    // sell side
    "SOUL",        // tokenStart
    amountInRaw,
    minProfitRaw   // bail if profit < this
  ])
  .spendGas(from)
  .endScript();

Arbitrage Stats

getExecutorSharePer100()

READ
getExecutorSharePer100(): number

Returns the percentage of each arb's profit that goes to the executor (default 80). The remainder goes to the protocol treasury.

Returns
number — Executor share as a whole-number percent (50..95).
What to expect
Read before firing an arb to compute your expected take-home profit: executorProfit = rawProfit * share / 100.
Example
const share = await readContract("saturnarb", "getExecutorSharePer100", []);
// net expected = rawProfit * share / 100

getTotalArbsExecuted()

READ
getTotalArbsExecuted(): number

Returns the total number of successful arbitrage executions across all executors since deploy.

Returns
number — Cumulative successful arbs.
What to expect
Use for protocol activity dashboards — increments once per successful executeArbitrage call.
Example
const total = await readContract("saturnarb", "getTotalArbsExecuted", []);

getTotalProfitGenerated()

READ
getTotalProfitGenerated(): number

Returns the total gross profit (pre-split) generated by arbitrage, summed across all tokens in raw units.

Returns
number — Cumulative raw profit.
What to expect
Note: this is summed across whatever tokenStart each arb used, so it mixes units and is only meaningful as a momentum metric, not a USD figure.
Example
const raw = await readContract("saturnarb", "getTotalProfitGenerated", []);

getExecutorArbCount()

READ
getExecutorArbCount(executor: address): number

Returns the number of successful arbs executed by a specific executor address.

Parameters
NameTypeDescription
executoraddressThe agent address to look up.
Returns
number — Per-executor successful arb count.
What to expect
Drive a leaderboard of top agents, or gate additional agent features behind a minimum count.
Example
const mine = await readContract("saturnarb", "getExecutorArbCount", [me]);

getExecutorTotalProfit()

READ
getExecutorTotalProfit(executor: address): number

Returns the total profit (post-split, i.e. the executor's take-home) earned by a specific executor.

Parameters
NameTypeDescription
executoraddressThe agent address to look up.
Returns
number — Cumulative executor-take-home profit, raw.
What to expect
Like getTotalProfitGenerated, this sums across whatever tokenStart was used per arb, so it's a momentum metric.
Example
const earned = await readContract("saturnarb", "getExecutorTotalProfit", [me]);
Agent Automation · Contract #14

SaturnLimit

saturnlimit

Set-and-forget limit orders. A user deposits the token they want to sell, names the pool, the token they want back, the minimum acceptable output (their limit price), a bounty percentage the executor agent will earn, and an optional expiry. The order sits on-chain until an agent notices the pool's price has crossed the limit and calls executeOrder — the swap fires atomically, the user receives tokenOut minus the bounty, the agent pockets the bounty as their reward for finding the trigger. Users can cancelOrder at any time for a full refund, and anyone can mark expired orders so the tokens flow back to the owner.

Order Lifecycle

placeOrder()

WRITE
placeOrder(from: address, poolId: number, tokenIn: string, tokenOut: string, amountIn: number, minAmountOut: number, bountyPer10k: number, expiryTime: number)

Deposit tokenIn and create a limit order. The swap will fire when an agent triggers executeOrder and the pool's current output meets minAmountOut (the constant-product slippage check enforces this).

Parameters
NameTypeDescription
fromaddressOrder owner — must be a witness and must hold amountIn of tokenIn.
poolIdnumberThe pool the eventual swap will route through. Must be active.
tokenInstringToken you are selling. Must differ from tokenOut.
tokenOutstringToken you want in return.
amountInnumberRaw amount of tokenIn to deposit. Must be > 0.
minAmountOutnumberMinimum acceptable raw tokenOut — this is your limit price. Must be > 0.
bountyPer10knumberAgent bounty in basis points of tokenOut. Must be between 10 (0.10%) and 500 (5%).
expiryTimenumberUnix timestamp when the order auto-expires. Pass 0 for no expiry. If non-zero, must be in the future.
What to expect
amountIn tokenIn is pulled from your wallet into the limit-order escrow. A new orderId is assigned in status 0 (active) and added to activeOrderIds. totalOrdersPlaced increments. No swap happens yet — you wait for an agent.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlimit", "placeOrder", [
    from,
    poolId,
    "SOUL",
    "KCAL",
    amountInRaw,
    minOutRaw,        // your limit price
    50,               // 0.50% bounty for the executor agent
    0                 // no expiry
  ])
  .spendGas(from)
  .endScript();

cancelOrder()

WRITE
cancelOrder(from: address, orderId: number)

Order owner cancels an active order and gets the full deposit refunded. Only works while status = 0 (active).

Parameters
NameTypeDescription
fromaddressMust be the order owner.
orderIdnumberAn active order.
What to expect
Status flips to 2 (cancelled) and your full amountIn of tokenIn is transferred back to you in a single Token.transfer.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlimit", "cancelOrder", [from, orderId])
  .spendGas(from)
  .endScript();

executeOrder()

WRITE
executeOrder(from: address, orderId: number)

Agent triggers an active order. The contract forwards tokenIn to the SwapEngine with minAmountOut enforced — if the current pool output is below the limit, the swap reverts and the whole transaction rolls back so nothing is lost. On success the owner is paid (amountOut - bounty) and the executor pockets the bounty.

Parameters
NameTypeDescription
fromaddressExecutor agent — any witness. Does not need to own the order.
orderIdnumberAn active order whose expiryTime has not passed.
What to expect
The limit-order escrow sends amountIn to the SwapEngine, swapFromContract returns amountOut. bounty = amountOut * bountyPer10k / 10000 goes to the executor; ownerPayout = amountOut - bounty goes to the owner. Status becomes 1 (executed) and totalOrdersExecuted increments. Reverts cleanly if the pool can't honor minAmountOut.
Example
// Off-chain agent loop:
//   - fetch getAllActiveOrderIds
//   - for each, simulate the swap and check if output >= minAmountOut
//   - fire executeOrder only when profitable
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlimit", "executeOrder", [from, orderId])
  .spendGas(from)
  .endScript();

expireOrder()

WRITE
expireOrder(from: address, orderId: number)

Anyone can mark an order as expired once its expiryTime has passed. The deposit is refunded to the owner in the same call — a convenience for long-tail cleanup.

Parameters
NameTypeDescription
fromaddressAny caller — no witness check on from.
orderIdnumberAn active order with expiry > 0 and now >= expiry.
What to expect
Status flips to 3 (expired) and the owner's full tokenIn deposit is returned in one transfer. Reverts if the order has no expiry set or if it hasn't actually expired yet.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlimit", "expireOrder", [from, orderId])
  .spendGas(from)
  .endScript();

Order Views

getOrderInfo()

READ
getOrderInfo(orderId: number): string

One-shot status snapshot. Returns an underscore-delimited string with the key fields.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
string — pool:<poolId>_in:<tokenIn>_out:<tokenOut>_amtIn:<raw>_minOut:<raw>_bounty:<per10k>_expiry:<unix>_status:<status>
What to expect
Split on '_' and then on ':' to decode. Status: 0=active, 1=executed, 2=cancelled, 3=expired.
Example
const raw = await readContract("saturnlimit", "getOrderInfo", [orderId]);
const parts = Object.fromEntries(raw.split("_").map(kv => kv.split(":")));

getOrderOwner()

READ
getOrderOwner(orderId: number): address

Returns the address that placed the order.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
address — Order owner.
What to expect
Only this address can call cancelOrder.
Example
const owner = await readContract("saturnlimit", "getOrderOwner", [orderId]);

getOrderPoolId()

READ
getOrderPoolId(orderId: number): number

Returns the poolId the order will route through.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
number — Target pool ID.
What to expect
Use SaturnRouter.getPoolFullInfo to show the pool's current state next to the order.
Example
const poolId = await readContract("saturnlimit", "getOrderPoolId", [orderId]);

getOrderTokenIn()

READ
getOrderTokenIn(orderId: number): string

Returns the symbol of the token the owner deposited.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
string — tokenIn symbol.
What to expect
Used to label the 'selling' side of the order row.
Example
const sym = await readContract("saturnlimit", "getOrderTokenIn", [orderId]);

getOrderTokenOut()

READ
getOrderTokenOut(orderId: number): string

Returns the symbol of the token the owner wants to receive.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
string — tokenOut symbol.
What to expect
Used to label the 'buying' side of the order row.
Example
const sym = await readContract("saturnlimit", "getOrderTokenOut", [orderId]);

getOrderAmountIn()

READ
getOrderAmountIn(orderId: number): number

Returns the raw amount of tokenIn locked in the order.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
number — Raw deposited amount.
What to expect
Pair with SaturnPools scaling helpers to format human-readable.
Example
const raw = await readContract("saturnlimit", "getOrderAmountIn", [orderId]);

getOrderMinAmountOut()

READ
getOrderMinAmountOut(orderId: number): number

Returns the limit price as a raw minimum tokenOut amount.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
number — Raw minAmountOut.
What to expect
Divide by getOrderAmountIn (after scaling both sides) to display the effective price.
Example
const minOut = await readContract("saturnlimit", "getOrderMinAmountOut", [orderId]);

getOrderStatus()

READ
getOrderStatus(orderId: number): number

Returns the lifecycle status code.

Parameters
NameTypeDescription
orderIdnumberThe order to inspect.
Returns
number — 0=active, 1=executed, 2=cancelled, 3=expired.
What to expect
Active orders are the only ones worth scanning for execution triggers.
Example
const status = await readContract("saturnlimit", "getOrderStatus", [orderId]);

getNextOrderId()

READ
getNextOrderId(): number

Returns the orderId that will be assigned to the next placeOrder call.

Returns
number — Next order ID (starts at 1).
What to expect
Total orders ever placed = getNextOrderId() - 1 (matches getTotalOrdersPlaced).
Example
const next = await readContract("saturnlimit", "getNextOrderId", []);

getTotalOrdersPlaced()

READ
getTotalOrdersPlaced(): number

Returns the cumulative number of orders ever placed.

Returns
number — Cumulative placed count.
What to expect
Use for activity / volume dashboards.
Example
const placed = await readContract("saturnlimit", "getTotalOrdersPlaced", []);

getTotalOrdersExecuted()

READ
getTotalOrdersExecuted(): number

Returns the cumulative number of orders that have been successfully executed.

Returns
number — Cumulative executed count.
What to expect
Fill ratio = getTotalOrdersExecuted() / getTotalOrdersPlaced().
Example
const executed = await readContract("saturnlimit", "getTotalOrdersExecuted", []);

getAllActiveOrderIds()

READ
getAllActiveOrderIds(): number*

Generator yielding every orderId ever added to the active list. Note: once an order transitions out of active, this list is not pruned — filter by getOrderStatus() to see only status 0.

Returns
number* — Iterable of order IDs.
What to expect
Agent loops read this, filter on status 0, simulate the swap, and fire executeOrder when profitable. UI 'My orders' views filter additionally by getOrderOwner.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnlimit", "getAllActiveOrderIds", [])
  .endScript();
Agent Automation · Contract #15

SaturnPredict

saturnpredict

Bet on whether a pool's on-chain metric will exceed a threshold by a set deadline. At market creation the contract snapshots the current metric value (either accumulated provider fees across both tokens, or reserveA + reserveB). Users bet OVER or UNDER with a token of the market's choosing. After the endTime, the first caller of claimWinnings triggers self-resolution — the contract re-reads the metric, compares delta vs threshold, marks the winning side, and pays every caller their proportional share of the pot minus the protocol fee. No oracles, no keepers. If one side of the market ends up empty (no opponents), the populated side uses claimRefund to pull their bets back.

Market Lifecycle

createMarket()

WRITE
createMarket(from: address, poolId: number, metricType: number, threshold: number, endTime: number, betToken: string, minBet: number)

Open a new prediction market on a pool metric. The current metric value is snapshotted into storage so resolution at endTime can compute the delta.

Parameters
NameTypeDescription
fromaddressMarket creator — must be a witness. Has permission to cancelMarket before bets are placed.
poolIdnumberThe active pool whose metric is being forecast.
metricTypenumber1 = pool's accumulated provider fees (getAccruedFees summed across both tokens). 2 = reserve growth (reserveA + reserveB).
thresholdnumberHow much the metric must INCREASE over the snapshot for OVER to win. Must be > 0 and is in raw metric units.
endTimenumberUnix timestamp when betting closes and resolution is allowed. Must be at least 3600 seconds in the future.
betTokenstringToken symbol used for all bets and payouts. Must be a validated symbol.
minBetnumberMinimum raw bet amount any single wager must meet.
What to expect
A new marketId is assigned in status 0 (open). The metric is read and stored in marketSnapshotValue. totalMarketsCreated increments. Now both betOver and betUnder are open until endTime.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "createMarket", [
    from,
    poolId,
    2,                     // 1=fees, 2=reserves
    thresholdRaw,          // must grow by at least this much
    Math.floor(Date.now()/1000) + 86400, // 24h
    "SOUL",
    1_00000000             // min bet 1 SOUL raw
  ])
  .spendGas(from)
  .endScript();

betOver()

WRITE
betOver(from: address, marketId: number, amount: number)

Place (or add to) a bet that the pool's metric delta will meet or exceed the threshold by endTime.

Parameters
NameTypeDescription
fromaddressBettor. Must be a witness and hold amount of betToken.
marketIdnumberAn open market (status 0).
amountnumberRaw betToken to wager. Must be >= marketMinBet.
What to expect
amount of betToken is pulled from you into the market escrow. Your overBetAmount entry grows (supports adding to an existing bet), marketTotalOver grows, totalBetsPlaced increments. Reverts if now >= endTime.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "betOver", [from, marketId, amountRaw])
  .spendGas(from)
  .endScript();

betUnder()

WRITE
betUnder(from: address, marketId: number, amount: number)

Place (or add to) a bet that the pool's metric delta will NOT meet the threshold by endTime.

Parameters
NameTypeDescription
fromaddressBettor. Must be a witness and hold amount of betToken.
marketIdnumberAn open market (status 0).
amountnumberRaw betToken to wager. Must be >= marketMinBet.
What to expect
Symmetric to betOver — your underBetAmount grows, marketTotalUnder grows, totalBetsPlaced increments. Reverts if now >= endTime.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "betUnder", [from, marketId, amountRaw])
  .spendGas(from)
  .endScript();

claimWinnings()

WRITE
claimWinnings(from: address, marketId: number)

After endTime, claim your share of the pot. The very first caller on a still-open market triggers self-resolution — the metric is re-read, delta computed, status flipped to resolved_over (1) or resolved_under (2). Every caller (first or later) then receives their proportional slice of the total pot if they bet on the winning side.

Parameters
NameTypeDescription
fromaddressCaller. Must be a witness and must not have claimed this market before.
marketIdnumberA market whose endTime has passed and which is not cancelled (status 3).
What to expect
hasClaimed is flipped so you can only claim once. If you bet on the losing side the call still completes — it just pays nothing. If you won: grossPayout = yourBet * totalPot / winningSideTotal, a protocolFeePer10k slice is sent to the admin treasury, and the net reaches your wallet in one transfer.
Example
// Check readiness first
const endT = await readContract("saturnpredict", "getMarketEndTime", [marketId]);
if (Date.now()/1000 < endT) throw new Error("Not ended yet");

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "claimWinnings", [from, marketId])
  .spendGas(from)
  .endScript();

cancelMarket()

WRITE
cancelMarket(from: address, marketId: number)

Creator cancels a market, but only before anyone has placed a bet. Once there's money on either side, cancelling is blocked — use the natural flow instead.

Parameters
NameTypeDescription
fromaddressMust be the market creator.
marketIdnumberAn open market with zero totalOver AND zero totalUnder.
What to expect
Status flips to 3 (cancelled). Nothing is transferred because no funds had been collected.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "cancelMarket", [from, marketId])
  .spendGas(from)
  .endScript();

claimRefund()

WRITE
claimRefund(from: address, marketId: number)

If one side of the book has zero bets at expiry, there is no opponent to play against — the populated side can simply withdraw everything they put in. This path bypasses resolution and protocol fees.

Parameters
NameTypeDescription
fromaddressBettor with non-zero bets on the populated side.
marketIdnumberA market whose endTime has passed where at least one of totalOver / totalUnder is zero.
What to expect
Your full (overBet + underBet) is transferred back to you. hasClaimed is flipped to prevent double-refund. Reverts with 'Both sides have bets' if you should be calling claimWinnings instead.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "claimRefund", [from, marketId])
  .spendGas(from)
  .endScript();

Market Views

getMarketInfo()

READ
getMarketInfo(marketId: number): string

One-shot snapshot for the market page. Returns an underscore-delimited string with the key fields.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
string — pool:<poolId>_metric:<1|2>_threshold:<raw>_end:<unix>_snapshot:<raw>_over:<totalOver>_under:<totalUnder>_status:<status>
What to expect
Split on '_' and then on ':' to decode. Status: 0=open, 1=resolved_over, 2=resolved_under, 3=cancelled. Compute implied odds with totalOver vs totalUnder.
Example
const raw = await readContract("saturnpredict", "getMarketInfo", [marketId]);
const parts = Object.fromEntries(raw.split("_").map(kv => kv.split(":")));

getMarketPoolId()

READ
getMarketPoolId(marketId: number): number

Returns the poolId being forecasted.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Underlying pool ID.
What to expect
Pair with SaturnRouter.getPoolFullInfo to render pool context next to the bet slip.
Example
const poolId = await readContract("saturnpredict", "getMarketPoolId", [marketId]);

getMarketMetricType()

READ
getMarketMetricType(marketId: number): number

Returns the metric this market is tracking.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — 1 = provider fee earnings, 2 = reserve sum growth.
What to expect
Use this to label the bet question: '... fees grow by at least X?' vs '... reserves grow by at least X?'.
Example
const kind = await readContract("saturnpredict", "getMarketMetricType", [marketId]);

getMarketThreshold()

READ
getMarketThreshold(marketId: number): number

Returns the required delta (in raw metric units) for OVER to win.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Threshold value.
What to expect
OVER wins iff (current metric - snapshot) >= threshold.
Example
const t = await readContract("saturnpredict", "getMarketThreshold", [marketId]);

getMarketEndTime()

READ
getMarketEndTime(marketId: number): number

Returns the Unix timestamp when betting closes and resolution unlocks.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Unix seconds.
What to expect
Drives the countdown and the enable/disable state of the bet buttons.
Example
const endT = await readContract("saturnpredict", "getMarketEndTime", [marketId]);

getMarketSnapshotValue()

READ
getMarketSnapshotValue(marketId: number): number

Returns the raw metric value captured at market creation.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Snapshot value.
What to expect
Subtract from the current live metric value to display 'delta so far' progress.
Example
const snap = await readContract("saturnpredict", "getMarketSnapshotValue", [marketId]);

getMarketTotalOver()

READ
getMarketTotalOver(marketId: number): number

Returns the total amount wagered on OVER.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Raw total.
What to expect
Combine with getMarketTotalUnder for implied odds: p(over) = over / (over + under).
Example
const over = await readContract("saturnpredict", "getMarketTotalOver", [marketId]);

getMarketTotalUnder()

READ
getMarketTotalUnder(marketId: number): number

Returns the total amount wagered on UNDER.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Raw total.
What to expect
If either side is zero once the market ends, the populated side claims via claimRefund.
Example
const under = await readContract("saturnpredict", "getMarketTotalUnder", [marketId]);

getMarketStatus()

READ
getMarketStatus(marketId: number): number

Returns the lifecycle status code.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — 0=open, 1=resolved_over, 2=resolved_under, 3=cancelled.
What to expect
0 means bets still accepted (if before endTime), 1 / 2 mean claim winnings now.
Example
const status = await readContract("saturnpredict", "getMarketStatus", [marketId]);

getNextMarketId()

READ
getNextMarketId(): number

Returns the marketId that will be assigned to the next createMarket call.

Returns
number — Next market ID (starts at 1).
What to expect
Total markets ever created = getNextMarketId() - 1.
Example
const next = await readContract("saturnpredict", "getNextMarketId", []);

getTotalMarketsCreated()

READ
getTotalMarketsCreated(): number

Returns the cumulative number of markets created.

Returns
number — Cumulative market count.
What to expect
Use for activity dashboards.
Example
const n = await readContract("saturnpredict", "getTotalMarketsCreated", []);

getTotalBetsPlaced()

READ
getTotalBetsPlaced(): number

Returns the cumulative number of betOver + betUnder calls across all markets.

Returns
number — Cumulative bet count.
What to expect
Note this counts calls, not distinct addresses — a user adding to a bet increments the counter again.
Example
const total = await readContract("saturnpredict", "getTotalBetsPlaced", []);

getProtocolFeePer10k()

READ
getProtocolFeePer10k(): number

Returns the protocol fee taken from winning payouts, in basis points (default 200 = 2%).

Returns
number — Fee, per 10,000.
What to expect
Use to estimate net payout: netPayout = grossPayout * (10000 - feePer10k) / 10000.
Example
const fee = await readContract("saturnpredict", "getProtocolFeePer10k", []);

getUserOverBet()

READ
getUserOverBet(marketId: number, user: address): number

Returns a user's total raw wager on OVER for a specific market.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
useraddressThe address to look up.
Returns
number — Raw OVER bet.
What to expect
Use for the 'My position' panel and to compute the user's expected payout.
Example
const mine = await readContract("saturnpredict", "getUserOverBet", [marketId, me]);

getUserUnderBet()

READ
getUserUnderBet(marketId: number, user: address): number

Returns a user's total raw wager on UNDER for a specific market.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
useraddressThe address to look up.
Returns
number — Raw UNDER bet.
What to expect
Use for the 'My position' panel and to compute the user's expected payout.
Example
const mine = await readContract("saturnpredict", "getUserUnderBet", [marketId, me]);

getUserHasClaimed()

READ
getUserHasClaimed(marketId: number, user: address): number

Returns 1 if the user has already called claimWinnings or claimRefund for this market, 0 otherwise.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
useraddressThe address to look up.
Returns
number — 0 or 1.
What to expect
Gate the claim button in the UI: show 'Claimed' if 1, 'Claim winnings' / 'Claim refund' if 0.
Example
const claimed = await readContract("saturnpredict", "getUserHasClaimed", [marketId, me]);
Agent Automation · Contract #16

SaturnVaults

saturnvaults

Robo-advisor vaults for DeFi. An AI agent creates a vault denominated in a single base token and publishes a performance fee. Any user can deposit that base token and receive proportional shares of the vault's NAV. The agent — and only the agent — routes vault funds through the SwapEngine to execute its strategy; when the agent swaps back to base, the contract automatically marks-to-market, cheks the high-water mark, and pays the agent a performance fee only on NEW all-time-high NAV per share (no double-dipping). Depositors can withdraw their proportional slice at any time. Agent (or admin in an emergency) can close a vault; withdrawals still work after closing.

Vault Lifecycle

createVault()

WRITE
createVault(from: address, baseToken: string, perfFeePer10k: number, minDeposit: number)

Agent creates a new vault. Locks in the base token, the performance fee taken from high-water-mark profits, and the minimum deposit size.

Parameters
NameTypeDescription
fromaddressAgent address — becomes the vault's exclusive manager. Must be a witness.
baseTokenstringToken the vault is denominated in. Deposits, withdrawals, and NAV are all measured in this token. Must be a validated symbol.
perfFeePer10knumberPerformance fee in basis points, taken from gains above the high-water mark. Must be between 100 (1%) and 3000 (30%).
minDepositnumberMinimum raw deposit size per deposit() call. Must be > 0.
What to expect
A new vaultId is assigned in status 0 (active). HWM is initialized to 10000 (== 1.0000 NAV per share, scaled by 10000). totalVaultsCreated increments and the vaultId is pushed to allVaultIds.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "createVault", [
    from,
    "SOUL",
    1500,            // 15% performance fee
    10_00000000      // min 10 SOUL deposit
  ])
  .spendGas(from)
  .endScript();

deposit()

WRITE
deposit(from: address, vaultId: number, amount: number)

User deposits baseToken into an active vault and receives freshly minted shares proportional to the vault's current NAV. First deposit: 1 token = 10,000 shares (precision scaling). Subsequent deposits: amount * totalShares / totalDeposits.

Parameters
NameTypeDescription
fromaddressDepositor. Must be a witness and hold amount of baseToken.
vaultIdnumberAn active vault (status 0).
amountnumberRaw baseToken to deposit. Must be >= minDeposit.
What to expect
amount baseToken is pulled into the vault. newShares are minted and credited to your userShares entry; vaultTotalShares and vaultTotalDeposits both grow. Your userDepositAmount tracks your lifetime cost basis for display purposes (not used for withdrawal math). Reverts if the share calculation rounds to zero.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "deposit", [from, vaultId, amountRaw])
  .spendGas(from)
  .endScript();

withdraw()

WRITE
withdraw(from: address, vaultId: number, sharesToRedeem: number)

User burns shares for a proportional slice of the vault's current baseToken balance. Works in any vault status — even closed vaults let users pull out.

Parameters
NameTypeDescription
fromaddressMust hold at least sharesToRedeem in userShares for this vault.
vaultIdnumberTarget vault.
sharesToRedeemnumberNumber of shares to burn. Must be > 0 and <= your balance.
What to expect
payout = sharesToRedeem * totalDeposits / totalShares. That many baseToken are transferred to you, your userShares and vaultTotalShares decrement, vaultTotalDeposits decrement. Reverts if the payout calculation rounds to zero.
Example
// Full exit:
const myShares = await readContract("saturnvaults", "getUserShares", [vaultId, me]);

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "withdraw", [from, vaultId, myShares])
  .spendGas(from)
  .endScript();

agentSwap()

WRITE
agentSwap(from: address, vaultId: number, poolId: number, amountIn: number, tokenIn: string, tokenOut: string, minAmountOut: number)

Only the vault's agent can call this. Routes vault-held tokenIn through a specific pool on the SwapEngine with standard minAmountOut slippage protection. When the agent swaps back into the base token, the contract marks-to-market: vaultTotalDeposits is updated to reflect the realized delta, and if new NAV/share is strictly above the high-water mark, a performance fee is auto-sent to the agent and the high-water mark is raised to the post-fee NAV.

Parameters
NameTypeDescription
fromaddressMust be the vault agent.
vaultIdnumberAn active vault (status 0).
poolIdnumberThe pool to route through.
amountInnumberRaw amount of tokenIn the vault holds to swap in.
tokenInstringToken the vault is selling. Vault must hold >= amountIn of this token.
tokenOutstringToken the vault is buying.
minAmountOutnumberStandard slippage guard. Swap reverts if the pool can't honor this.
What to expect
amountIn tokenIn is transferred from the vault to the SwapEngine and swapped; the output tokenOut is received back by the vault. When tokenOut == baseToken the contract runs the mark-to-market + HWM + performance fee cycle. When tokenOut != baseToken the vault simply holds the new non-base token until a later agentSwap cycles it back. Note: with the vault holding non-base tokens, getVaultTotalDeposits does NOT reflect the full market value until the next conversion back to base.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "agentSwap", [
    from,
    vaultId,
    poolId,
    amountInRaw,
    "SOUL",       // base, going out
    "KCAL",       // target
    minOutRaw
  ])
  .spendGas(from)
  .endScript();

closeVault()

WRITE
closeVault(from: address, vaultId: number)

Agent marks the vault as closed. No new deposits accepted and no further agentSwap calls, but existing depositors can still withdraw their proportional slice.

Parameters
NameTypeDescription
fromaddressMust be the vault agent.
vaultIdnumberAn active vault.
What to expect
Status flips to 1 (closed). Users holding shares should call withdraw to exit.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "closeVault", [from, vaultId])
  .spendGas(from)
  .endScript();

Vault Views

getVaultInfo()

READ
getVaultInfo(vaultId: number): string

One-shot dashboard snapshot. Returns an underscore-delimited string with the key fields.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
string — base:<symbol>_deposits:<raw>_shares:<total>_hwm:<navPerShare*10000>_perfFee:<per10k>_status:<0|1>
What to expect
Split on '_' and then on ':' to decode. Current NAV per share = deposits * 10000 / shares. Compare against hwm to know how close the agent is to earning fees.
Example
const raw = await readContract("saturnvaults", "getVaultInfo", [vaultId]);
const parts = Object.fromEntries(raw.split("_").map(kv => kv.split(":")));

getVaultAgent()

READ
getVaultAgent(vaultId: number): address

Returns the agent address authorized to call agentSwap and closeVault.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
address — Managing agent.
What to expect
Use for permission gating and to render the 'managed by' badge.
Example
const agent = await readContract("saturnvaults", "getVaultAgent", [vaultId]);

getVaultBaseToken()

READ
getVaultBaseToken(vaultId: number): string

Returns the base token symbol the vault is denominated in.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
string — Base token symbol.
What to expect
All deposit/withdraw/NAV calculations use this token.
Example
const base = await readContract("saturnvaults", "getVaultBaseToken", [vaultId]);

getVaultTotalDeposits()

READ
getVaultTotalDeposits(vaultId: number): number

Returns the vault's current base-token-denominated NAV as tracked by the contract (does not include the raw value of non-base tokens the vault may temporarily hold between agent swaps).

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw NAV in base token.
What to expect
Divide by getVaultTotalShares to get the accounting NAV per share. Note: between the two legs of an agent swap, this lags reality — the definitive mark happens only when tokenOut == baseToken.
Example
const nav = await readContract("saturnvaults", "getVaultTotalDeposits", [vaultId]);

getVaultTotalShares()

READ
getVaultTotalShares(vaultId: number): number

Returns the total shares outstanding across all depositors.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Total share supply.
What to expect
Used as the denominator when computing a depositor's ownership percentage: mine / total.
Example
const total = await readContract("saturnvaults", "getVaultTotalShares", [vaultId]);

getVaultHighWaterMark()

READ
getVaultHighWaterMark(vaultId: number): number

Returns the vault's high-water mark as NAV per share * 10000. Initialized to 10000 (= 1.0000).

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — HWM * 10000.
What to expect
The agent earns performance fees only when current NAV per share strictly exceeds this value. Divide by 10000 to display as a decimal.
Example
const hwm = await readContract("saturnvaults", "getVaultHighWaterMark", [vaultId]);
const display = (hwm / 10000).toFixed(4);

getVaultPerfFeePer10k()

READ
getVaultPerfFeePer10k(vaultId: number): number

Returns the performance fee charged against new high-water-mark profits, in basis points.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Performance fee, per 10,000.
What to expect
Show as a percent in the vault card: (fee / 100) + '%'.
Example
const fee = await readContract("saturnvaults", "getVaultPerfFeePer10k", [vaultId]);

getVaultStatus()

READ
getVaultStatus(vaultId: number): number

Returns the vault's lifecycle status.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — 0=active, 1=closed.
What to expect
Gate the deposit button on status 0. Withdraw remains available in both statuses.
Example
const status = await readContract("saturnvaults", "getVaultStatus", [vaultId]);

getNextVaultId()

READ
getNextVaultId(): number

Returns the vaultId that will be assigned to the next createVault call.

Returns
number — Next vault ID (starts at 1).
What to expect
Total vaults ever created = getNextVaultId() - 1 (matches getTotalVaultsCreated).
Example
const next = await readContract("saturnvaults", "getNextVaultId", []);

getTotalVaultsCreated()

READ
getTotalVaultsCreated(): number

Returns the cumulative number of vaults ever created.

Returns
number — Cumulative vault count.
What to expect
Use for protocol growth dashboards.
Example
const n = await readContract("saturnvaults", "getTotalVaultsCreated", []);

getUserShares()

READ
getUserShares(vaultId: number, user: address): number

Returns a specific user's share balance in a vault.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
useraddressThe depositor address to look up.
Returns
number — Raw shares held.
What to expect
Pass to withdraw() for a full exit. Compute user NAV = shares * totalDeposits / totalShares.
Example
const mine = await readContract("saturnvaults", "getUserShares", [vaultId, me]);

getUserDepositAmount()

READ
getUserDepositAmount(vaultId: number, user: address): number

Returns the user's lifetime gross deposit amount into a vault. This is a display-only cost basis — it is NOT decremented on withdraw.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
useraddressThe depositor address to look up.
Returns
number — Raw cumulative deposit amount.
What to expect
Use for the 'total invested' field. To compute realized PnL you'll need to track withdrawals off-chain separately.
Example
const invested = await readContract("saturnvaults", "getUserDepositAmount", [vaultId, me]);

getAllVaultIds()

READ
getAllVaultIds(): number*

Generator yielding every vaultId ever created.

Returns
number* — Iterable of vault IDs.
What to expect
Combine with getVaultStatus() to partition the explorer into active and closed tabs.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnvaults", "getAllVaultIds", [])
  .endScript();
Agent Automation · Contract #22

SaturnStakeArb

saturnstakearb

Permissionless arbitrage engine that lets any bot atomically borrow idle capital from saturnholders' staked pool, round-trip it across two Saturn pools (token → riskToken → token), return 100% of the borrowed principal, and split the net profit 50/50 between the executing bot and all stakers of that token. Stakers earn arbitrage yield on top of their normal swap-fee share from the exact same deposit, with zero additional capital required. If the round-trip is unprofitable the entire transaction reverts — the stake is never decreased. This contract is designed for MEV bots and AI agents.

Arbitrage Execution

executeArb()

WRITE
executeArb(from: address, tokenSymbol: string, amountIn: number, riskToken: string, poolBuy: number, poolSell: number): number

Executes an atomic stake-arbitrage round-trip. Borrows amountIn of tokenSymbol from saturnholders (via flashLendStake), executes leg 1 (tokenSymbol → riskToken via poolBuy) and leg 2 (riskToken → tokenSymbol via poolSell), requires back > amountIn, then returns the principal to saturnholders, banks 50% of the gross profit at saturnliquidity as the holders' share (accrued into the same MasterChef accumulator stakers already use), and pays the remaining 50% to from. The trade reverts atomically if it is unprofitable — the staked principal is mathematically guaranteed to be intact before the transaction commits, because settleArbLoan re-asserts saturnholders' balance >= totalStaked.

Parameters
NameTypeDescription
fromaddressExecutor's address. Must be the transaction witness and receives 50% of gross profit.
tokenSymbolstringSymbol of the token to borrow and denominate profit in; must be staked in saturnholders.
amountInnumberRaw-unit borrow amount. Must be > 0 and ≤ totalStaked for tokenSymbol.
riskTokenstringIntermediate token for the round-trip. Must differ from tokenSymbol and appear in both poolBuy and poolSell.
poolBuynumberPool ID for leg 1 (tokenSymbol → riskToken). Must be active and contain both tokenSymbol and riskToken.
poolSellnumberPool ID for leg 2 (riskToken → tokenSymbol). Must be active and contain both riskToken and tokenSymbol. Must differ from poolBuy.
Returns
number — The holders' share (50% of gross profit) in raw units of tokenSymbol banked at saturnliquidity.
What to expect
Reverts if from is not the witness, amountIn == 0, poolBuy == poolSell, riskToken == tokenSymbol, either pool is inactive, token pairs do not match, amountIn exceeds totalStaked, or back ≤ amountIn (no profit). On revert the staked principal is untouched and only gas is spent.
Example
const from = "S3...botAddress";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnstakearb", "executeArb",
    [from, "SOUL", amountIn, "KCAL", poolBuy, poolSell])
  .spendGas(from)
  .endScript();
// returns holderShare — 50% of (back - amountIn) in raw SOUL units

Statistics Views

getTotalArbs()

READ
getTotalArbs(): number

Returns the cumulative count of successful stake-arbitrage executions since deployment.

Returns
number — Total successful executeArb calls.
Example
const total = await readContract("saturnstakearb", "getTotalArbs", []);

getTotalHolderProfit()

READ
getTotalHolderProfit(tokenSymbol: string): number

Returns the lifetime raw-unit amount of tokenSymbol banked at saturnliquidity as holders' profit share from all successful executeArb calls. Use this to display total arb yield earned by stakers of a given token.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Lifetime holder profit in raw units of tokenSymbol.
Example
const holderProfit = await readContract("saturnstakearb", "getTotalHolderProfit", ["SOUL"]);

getExecutorArbCount()

READ
getExecutorArbCount(executor: address): number

Returns the number of successful executeArb calls made by a specific executor address. Use this on leaderboards or for per-bot performance tracking.

Parameters
NameTypeDescription
executoraddressThe executor address to query.
Returns
number — Number of successful executeArb calls made by this executor.
Example
const count = await readContract("saturnstakearb", "getExecutorArbCount", [botAddr]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed version string for this contract.

Returns
string — Version identifier, e.g. "saturnstakearb-4.4.0".
Example
const ver = await readContract("saturnstakearb", "getContractVersion", []);
Lending Protocol · Contract #1

SaturnLendCfg

saturnlendcfg

The single source-of-truth for every tunable parameter in the Saturn Lending protocol, which is built on top of Saturn DEX v3 and v4. All other lending contracts (saturncredit, saturnvault, saturnloans, saturnauto, saturnmarket) read their limits, thresholds, and fee rates directly from this contract — making it the first place to query before rendering any loan form or dashboard. Collateral and loan token prices are always denominated against RA-paired pools; pools that do not include RA are never read by the lending system.

Ownership & Access

getAdmin()

READ
getAdmin(): address

Returns the current protocol admin address for the lending layer. Use this to verify upgrade authority or to display governance ownership in your UI.

Returns
address — Current lending config admin address.
What to expect
Always a valid non-null address. Never reverts.
Example
const admin = await readContract("saturnlendcfg", "getAdmin", []);

Loan Term & Rate Limits

getMinLoanDuration()

READ
getMinLoanDuration(): number

Minimum loan duration in seconds. Any P2P quote or auto-loan request with a shorter duration will be rejected. Default is 604,800 (7 days).

Returns
number — Minimum loan duration in seconds (default: 604800 = 7 days).
What to expect
Always ≥ 86400 (1 day). Always < getMaxLoanDuration().
Example
const minDur = await readContract("saturnlendcfg", "getMinLoanDuration", []);
// 604800 → 7 days

getMaxLoanDuration()

READ
getMaxLoanDuration(): number

Maximum loan duration in seconds. Default is 31,536,000 (365 days). Show this as the upper bound on your loan-term slider.

Returns
number — Maximum loan duration in seconds (default: 31536000 = 365 days).
What to expect
Always ≤ 63,072,000 (2 years). Always > getMinLoanDuration().
Example
const maxDur = await readContract("saturnlendcfg", "getMaxLoanDuration", []);
// 31536000 → 365 days

getGracePeriod()

READ
getGracePeriod(): number

Extra time in seconds granted to a borrower after a missed installment before the loan enters default. Default is 259,200 (3 days). Display this so borrowers know their window to cure a late payment.

Returns
number — Grace period in seconds (default: 259200 = 3 days).
What to expect
Always in range 0–604800 (7 days).
Example
const grace = await readContract("saturnlendcfg", "getGracePeriod", []);
// 259200 → 3 days

getInstallmentInterval()

READ
getInstallmentInterval(): number

Interval in seconds between installment payment due-dates. Default is 2,592,000 (30 days). Used by getInstallmentCount() to compute how many payments a loan will have.

Returns
number — Installment interval in seconds (default: 2592000 = 30 days).
What to expect
Positive. Divides loan duration to determine payment count.
Example
const interval = await readContract("saturnlendcfg", "getInstallmentInterval", []);

getAutoBaseDuration()

READ
getAutoBaseDuration(): number

Base loan duration (seconds) used in the auto-lending algorithm before credit-score extensions are added. Default is 2,592,000 (30 days).

Returns
number — Auto-lending base duration in seconds (default: 2592000 = 30 days).
What to expect
Positive. Used only by the auto-lending path (disabled in v1.0).
Example
const base = await readContract("saturnlendcfg", "getAutoBaseDuration", []);

getAutoMaxExtension()

READ
getAutoMaxExtension(): number

Maximum additional seconds a perfect credit score (1000) can add to the auto-lending base duration. Default is 12,960,000 (150 days). The full formula is autoBaseDuration + (creditScore * autoMaxExtension / maxScore).

Returns
number — Maximum duration extension in seconds for auto-lending (default: 12960000).
What to expect
Used only by the auto-lending path (disabled in v1.0).
Example
const ext = await readContract("saturnlendcfg", "getAutoMaxExtension", []);

getBaseInterestRate()

READ
getBaseInterestRate(): number

The starting annual interest rate in basis points (per 10,000) before any credit-score discount is applied. Default is 2,000 (20% APR). A borrower with zero credit score pays this rate (clamped to maxInterestRate).

Returns
number — Base annual interest rate in bps/10000 (default: 2000 = 20%).
What to expect
Always ≥ 100. Always ≤ 5000.
Example
const baseRate = await readContract("saturnlendcfg", "getBaseInterestRate", []);
// 2000 → 20% APR

getCreditRateDiscount()

READ
getCreditRateDiscount(): number

Per-unit discount applied per credit score point. The effective rate is: baseInterestRate − (creditScore × creditRateDiscount / 1000), clamped to [minInterestRate, maxInterestRate]. Default is 15.

Returns
number — Rate discount factor (default: 15; applied as discount = creditScore * 15 / 1000).
What to expect
Combined with getInterestRateForScore() to compute the borrower-specific rate.
Example
const discount = await readContract("saturnlendcfg", "getCreditRateDiscount", []);

getMinInterestRate()

READ
getMinInterestRate(): number

Floor for the computed annual interest rate regardless of how high a borrower's credit score is. Default is 300 (3% APR). Show this as the best-case rate achievable.

Returns
number — Minimum annual interest rate in bps/10000 (default: 300 = 3%).
What to expect
Always ≥ 100 and < getMaxInterestRate().
Example
const minRate = await readContract("saturnlendcfg", "getMinInterestRate", []);
// 300 → 3% APR

getMaxInterestRate()

READ
getMaxInterestRate(): number

Ceiling for the computed annual interest rate. Default is 3,000 (30% APR). Show this as the worst-case rate a borrower can be assigned.

Returns
number — Maximum annual interest rate in bps/10000 (default: 3000 = 30%).
What to expect
Always ≤ 5000 and > getMinInterestRate().
Example
const maxRate = await readContract("saturnlendcfg", "getMaxInterestRate", []);
// 3000 → 30% APR

getSecondsPerYear()

READ
getSecondsPerYear(): number

Returns 31,536,000 — the constant used by calculateInterest() to annualise the rate. Use when reproducing the interest formula client-side.

Returns
number — Seconds per year constant (31536000).
What to expect
Always 31536000. Never changes.
Example
const spy = await readContract("saturnlendcfg", "getSecondsPerYear", []);

getSecondsPerDay()

READ
getSecondsPerDay(): number

Returns 86,400 — the constant used by the credit score time-bonus accrual. Use when computing daily time-bonus increments client-side.

Returns
number — Seconds per day constant (86400).
What to expect
Always 86400. Never changes.
Example
const spd = await readContract("saturnlendcfg", "getSecondsPerDay", []);

Collateral & LTV

getLtvTier1()

READ
getLtvTier1(): number

Maximum LTV (in bps per 10,000) for borrowers with a credit score of 0–199. Default is 2,500 (25%). This is the most restrictive tier.

Returns
number — LTV for score 0–199 in bps/10000 (default: 2500 = 25%).
What to expect
Always ≥ 1000 and < getLtvTier2().
Example
const ltv1 = await readContract("saturnlendcfg", "getLtvTier1", []);
// 2500 → 25% LTV

getLtvTier2()

READ
getLtvTier2(): number

Maximum LTV for credit score 200–399. Default is 3,500 (35%).

Returns
number — LTV for score 200–399 in bps/10000 (default: 3500 = 35%).
What to expect
Always > getLtvTier1() and < getLtvTier3().
Example
const ltv2 = await readContract("saturnlendcfg", "getLtvTier2", []);

getLtvTier3()

READ
getLtvTier3(): number

Maximum LTV for credit score 400–599. Default is 5,000 (50%).

Returns
number — LTV for score 400–599 in bps/10000 (default: 5000 = 50%).
What to expect
Always > getLtvTier2() and < getLtvTier4().
Example
const ltv3 = await readContract("saturnlendcfg", "getLtvTier3", []);

getLtvTier4()

READ
getLtvTier4(): number

Maximum LTV for credit score 600–799. Default is 6,500 (65%).

Returns
number — LTV for score 600–799 in bps/10000 (default: 6500 = 65%).
What to expect
Always > getLtvTier3() and < getLtvTier5().
Example
const ltv4 = await readContract("saturnlendcfg", "getLtvTier4", []);

getLtvTier5()

READ
getLtvTier5(): number

Maximum LTV for credit score 800–1000. Default is 8,000 (80%). This is the best LTV tier, reserved for borrowers with an established repayment history.

Returns
number — LTV for score 800–1000 in bps/10000 (default: 8000 = 80%).
What to expect
Always ≤ 9000 and > getLtvTier4().
Example
const ltv5 = await readContract("saturnlendcfg", "getLtvTier5", []);
// 8000 → 80% LTV

getMinCollateralValue()

READ
getMinCollateralValue(): number

Minimum RA-denominated value (in the protocol's internal scaled units) that collateral must be worth before a loan can be created. Default is 100,000,000. Display this as the minimum collateral requirement on your loan form.

Returns
number — Minimum collateral value in RA-anchor scaled units (default: 100000000).
What to expect
Positive. Enforced at loan-creation time by saturnloans.
Example
const minColl = await readContract("saturnlendcfg", "getMinCollateralValue", []);

getLiquidationThreshold()

READ
getLiquidationThreshold(): number

The LTV ratio (bps per 10,000) above which collateral becomes eligible for liquidation. When the current loan-to-value exceeds this threshold the position can be liquidated. Default is 9,000 (90%).

Returns
number — Liquidation LTV threshold in bps/10000 (default: 9000 = 90%).
What to expect
Always in range (5000, 9800]. Always > any ltvTier value.
Example
const liqThresh = await readContract("saturnlendcfg", "getLiquidationThreshold", []);
// 9000 → collateral liquidatable when position reaches 90% LTV

getMaxLoansPerUser()

READ
getMaxLoansPerUser(): number

Maximum number of active loans a single borrower address may hold simultaneously. Default is 5. Check this before showing a borrow button to a user who may already be at capacity.

Returns
number — Maximum concurrent active loans per address (default: 5).
What to expect
Always in range [1, 20].
Example
const maxLoans = await readContract("saturnlendcfg", "getMaxLoansPerUser", []);

Credit Score Params

getBaseScore()

READ
getBaseScore(): number

The credit score assigned to a brand-new borrower with no history. Default is 200. New users start in Tier 2 LTV. Display this when onboarding first-time borrowers.

Returns
number — Starting credit score for new borrowers (default: 200).
What to expect
Always ≥ 0 and ≤ getMaxScore().
Example
const base = await readContract("saturnlendcfg", "getBaseScore", []);
// 200 → qualifies for Tier 2 LTV (35%)

getMaxScore()

READ
getMaxScore(): number

The maximum achievable credit score. Default is 1,000. Use this as the upper bound when rendering a score progress bar.

Returns
number — Maximum credit score (default: 1000).
What to expect
Always in range [100, 10000]. Default: 1000.
Example
const max = await readContract("saturnlendcfg", "getMaxScore", []);

getTimeBonusPerDay()

READ
getTimeBonusPerDay(): number

Credit score points added per day of account age. Default is 1 point/day. A wallet that has existed for 100 days earns up to 100 points from this alone (capped by getTimeBonusCap()).

Returns
number — Score points accrued per day of wallet age (default: 1).
What to expect
Positive. Accumulation is capped by getTimeBonusCap().
Example
const tpd = await readContract("saturnlendcfg", "getTimeBonusPerDay", []);

getTimeBonusCap()

READ
getTimeBonusCap(): number

Maximum credit score points that wallet-age time bonuses can contribute. Default is 150. Even a very old wallet cannot earn more than 150 points from age alone.

Returns
number — Maximum time-bonus contribution (default: 150).
What to expect
Always ≥ 0.
Example
const tcap = await readContract("saturnlendcfg", "getTimeBonusCap", []);

getOnTimeRepayBonus()

READ
getOnTimeRepayBonus(): number

Credit score points awarded per on-time installment or full repayment. Default is 50. Display this in the incentive copy next to the repayment button.

Returns
number — Score points awarded per on-time repayment (default: 50).
What to expect
Positive. Accumulation is capped by getOnTimeRepayCap().
Example
const otrb = await readContract("saturnlendcfg", "getOnTimeRepayBonus", []);

getOnTimeRepayCap()

READ
getOnTimeRepayCap(): number

Maximum credit score points that on-time repayments can contribute in total. Default is 350.

Returns
number — Maximum on-time repayment bonus contribution (default: 350).
What to expect
Always ≥ 0.
Example
const otrcap = await readContract("saturnlendcfg", "getOnTimeRepayCap", []);

getLateRepayPenalty()

READ
getLateRepayPenalty(): number

Credit score points deducted when a payment is made during the grace period (late, but not defaulted). Default is 30.

Returns
number — Score penalty for a late (but not defaulted) payment (default: 30).
What to expect
Non-negative.
Example
const lrp = await readContract("saturnlendcfg", "getLateRepayPenalty", []);

getDefaultPenalty()

READ
getDefaultPenalty(): number

Credit score points deducted when a loan defaults (grace period exhausted without payment). Default is 150. Show this prominently on loan health dashboards.

Returns
number — Score penalty for a loan default (default: 150).
What to expect
Non-negative. Larger than getLateRepayPenalty().
Example
const dp = await readContract("saturnlendcfg", "getDefaultPenalty", []);

getStreakBonus()

READ
getStreakBonus(): number

Bonus credit score points added per consecutive on-time repayment in a streak. Default is 10.

Returns
number — Score bonus per streak increment (default: 10).
What to expect
Non-negative. Accumulation is capped by getStreakBonusCap().
Example
const sb = await readContract("saturnlendcfg", "getStreakBonus", []);

getStreakBonusCap()

READ
getStreakBonusCap(): number

Maximum credit score contribution from repayment streaks. Default is 100.

Returns
number — Maximum streak bonus contribution (default: 100).
What to expect
Always ≥ 0.
Example
const scap = await readContract("saturnlendcfg", "getStreakBonusCap", []);

getPartialRepayBonus()

READ
getPartialRepayBonus(): number

Credit score points awarded for making a partial early repayment before an installment due-date. Default is 15. Use this to encourage pro-active debt reduction in your UX.

Returns
number — Score bonus for a partial early repayment (default: 15).
What to expect
Non-negative.
Example
const prb = await readContract("saturnlendcfg", "getPartialRepayBonus", []);

Fees & Computed Values

getOriginationFeeBps()

READ
getOriginationFeeBps(): number

Origination fee in basis points (per 10,000) charged on the loan principal at creation. Default is 100 (1%). Use getOriginationFee() for the actual computed amount.

Returns
number — Origination fee rate in bps/10000 (default: 100 = 1%).
What to expect
Always ≤ 500 (5%).
Example
const origFee = await readContract("saturnlendcfg", "getOriginationFeeBps", []);
// 100 → 1% of principal

getLiquidationPenaltyBps()

READ
getLiquidationPenaltyBps(): number

Liquidation penalty in basis points (per 10,000) applied to outstanding debt when a position is liquidated. Default is 500 (5%). Use getLiquidationPenalty() for the computed amount.

Returns
number — Liquidation penalty rate in bps/10000 (default: 500 = 5%).
What to expect
Always ≤ 2000 (20%).
Example
const liqPen = await readContract("saturnlendcfg", "getLiquidationPenaltyBps", []);

getProtocolFeeShare()

READ
getProtocolFeeShare(): number

The share of interest/fee revenue (in bps per 10,000) routed to the protocol treasury rather than the lender. Default is 1,000 (10%). Useful for computing a lender's net yield.

Returns
number — Protocol's share of fee revenue in bps/10000 (default: 1000 = 10%).
What to expect
Always ≤ 3000 (30%).
Example
const protoShare = await readContract("saturnlendcfg", "getProtocolFeeShare", []);

getMaxLtvForScore()

READ
getMaxLtvForScore(creditScore: number): number

Returns the maximum LTV ratio (bps per 10,000) for a given credit score by mapping it to the correct tier. This is the primary helper used by saturnloans and saturnmarket; call it before presenting a borrow amount cap to the user.

Parameters
NameTypeDescription
creditScorenumberBorrower's current credit score (0–1000).
Returns
number — Max LTV in bps/10000 (e.g. 5000 = 50%) for the given score.
What to expect
Score 0–199 → tier1; 200–399 → tier2; 400–599 → tier3; 600–799 → tier4; 800–1000 → tier5. Never reverts.
Example
const ltv = await readContract("saturnlendcfg", "getMaxLtvForScore", [400]);
// 5000 → 50% LTV cap for a score of 400

getInterestRateForScore()

READ
getInterestRateForScore(creditScore: number): number

Computes the annual interest rate (bps per 10,000) for a given credit score using the formula: baseInterestRate − (creditScore × creditRateDiscount / 1000), clamped to [minInterestRate, maxInterestRate]. Use this to preview the APR on the loan form before the borrower submits.

Parameters
NameTypeDescription
creditScorenumberBorrower's current credit score (0–1000).
Returns
number — Annual interest rate in bps/10000 (e.g. 1200 = 12% APR).
What to expect
Result always in [getMinInterestRate(), getMaxInterestRate()]. Never reverts.
Example
const rate = await readContract("saturnlendcfg", "getInterestRateForScore", [600]);
// e.g. 1100 → 11% APR for a score of 600

getAutoDurationForScore()

READ
getAutoDurationForScore(creditScore: number): number

Computes the auto-lending loan duration (seconds) for a given credit score: autoBaseDuration + (creditScore × autoMaxExtension / maxScore), clamped to [minLoanDuration, maxLoanDuration]. Used by the auto-lending algorithm (disabled in v1.0).

Parameters
NameTypeDescription
creditScorenumberBorrower's credit score (0–1000).
Returns
number — Loan duration in seconds for the auto-lending path.
What to expect
Always in [getMinLoanDuration(), getMaxLoanDuration()]. Never reverts.
Example
const dur = await readContract("saturnlendcfg", "getAutoDurationForScore", [500]);

calculateInterest()

READ
calculateInterest(principal: number, ratePer10k: number, durationSeconds: number): number

Computes total simple interest owed on a loan: (principal × ratePer10k / 10000) × durationSeconds / secondsPerYear. Use this to show the total cost of a loan before the borrower confirms.

Parameters
NameTypeDescription
principalnumberLoan principal in the token's raw units.
ratePer10knumberAnnual interest rate in bps per 10,000 (e.g. 1500 = 15%).
durationSecondsnumberLoan duration in seconds.
Returns
number — Total interest amount in the same raw units as principal.
What to expect
Truncated integer arithmetic. For small principals or short durations the result may be 0. Never reverts.
Example
// 1 000 000 units, 20% APR, 30 days
const interest = await readContract("saturnlendcfg", "calculateInterest", [
  1000000, 2000, 2592000
]);

getInstallmentCount()

READ
getInstallmentCount(durationSeconds: number): number

Returns the number of installments for a loan of a given duration by dividing by installmentInterval, rounding up. Minimum 1. Use this to compute the payment schedule grid.

Parameters
NameTypeDescription
durationSecondsnumberLoan duration in seconds.
Returns
number — Number of installment payments (always ≥ 1).
What to expect
Always ≥ 1. A 30-day loan = 1 installment; a 60-day loan = 2 installments.
Example
const count = await readContract("saturnlendcfg", "getInstallmentCount", [5184000]);
// 2 installments for a 60-day loan

getInstallmentAmount()

READ
getInstallmentAmount(totalOwed: number, installmentCount: number): number

Computes the per-installment payment amount from total owed divided by count, rounded up to prevent underpayment from truncation. Pair with calculateInterest() and getInstallmentCount() to build a full repayment schedule.

Parameters
NameTypeDescription
totalOwednumberTotal amount owed (principal + interest) in raw units.
installmentCountnumberNumber of installments (from getInstallmentCount()).
Returns
number — Per-installment payment amount, rounded up.
What to expect
installmentCount must be ≥ 1. Always returns a value that covers at least totalOwed / installmentCount.
Example
const perPayment = await readContract("saturnlendcfg", "getInstallmentAmount", [
  1050000, // total owed
  3         // 3 installments
]);
// 350000 per installment

getOriginationFee()

READ
getOriginationFee(principal: number): number

Computes the origination fee amount for a given principal: principal × originationFeeBps / 10000. Display this as an upfront cost on the borrow confirmation screen.

Parameters
NameTypeDescription
principalnumberLoan principal in raw token units.
Returns
number — Origination fee in raw token units.
What to expect
Result is ≥ 0. Truncated integer arithmetic.
Example
const fee = await readContract("saturnlendcfg", "getOriginationFee", [5000000]);
// 50000 → 1% origination on a 5 000 000 unit loan

getLiquidationPenalty()

READ
getLiquidationPenalty(outstandingDebt: number): number

Computes the liquidation penalty amount for a given outstanding debt: outstandingDebt × liquidationPenaltyBps / 10000. Use this to show liquidators their expected profit and borrowers the cost of liquidation.

Parameters
NameTypeDescription
outstandingDebtnumberOutstanding debt amount in raw token units.
Returns
number — Liquidation penalty amount in raw token units.
What to expect
Result ≥ 0. Truncated integer arithmetic.
Example
const pen = await readContract("saturnlendcfg", "getLiquidationPenalty", [2000000]);

Reentrancy Guard (Read)

getGuardLocked()

READ
getGuardLocked(user: address): number

Returns 1 if the lending reentrancy guard is currently locked for the given user address, 0 otherwise. Use for debugging hung transactions; under normal conditions this should always return 0 between transactions.

Parameters
NameTypeDescription
useraddressUser address to check.
Returns
number — 1 if guard is active (locked), 0 if free.
What to expect
Should always be 0 outside an active lending transaction. A non-zero value indicates an incomplete or stuck transaction.
Example
const locked = await readContract("saturnlendcfg", "getGuardLocked", [userAddress]);
if (locked) console.warn("Guard locked — pending tx in flight");

getGuardOwner()

READ
getGuardOwner(user: address): string

Returns the name of the lending contract that currently holds the reentrancy lock for a given user (e.g. "saturnloans", "saturnmarket"). Returns an empty string when the guard is free.

Parameters
NameTypeDescription
useraddressUser address to check.
Returns
string — Contract name that owns the lock, or empty string if unlocked.
What to expect
Non-empty only when getGuardLocked() returns 1.
Example
const owner = await readContract("saturnlendcfg", "getGuardOwner", [userAddress]);
// "" → unlocked; "saturnloans" → loan creation in progress

Deprecated / Removed

getSOULfeeLoan()

READ
getSOULfeeLoan(): number

Always returns 0. The SOUL loan fee was removed in v4-04x; lending entrypoints no longer charge SOUL. The storage key is retained at zero for ABI compatibility with older integrations. Do not use this value for any pricing calculation.

Returns
number — Always 0.
What to expect
Always 0. Key is frozen.
Example
const fee = await readContract("saturnlendcfg", "getSOULfeeLoan", []);
// Always 0

getStorageFee()

READ
getStorageFee(): number

Always returns 0. Gen3 storage is funded by the transaction's SOUL data escrow (maxData on the payer), not by any staking or SOUL charge through this contract. Signature retained for ABI/upgrade compatibility only.

Returns
number — Always 0.
What to expect
Always 0. Funding model changed in v4-03x.
Example
const sf = await readContract("saturnlendcfg", "getStorageFee", []);
// Always 0
Lending Protocol · Contract #2

SaturnCredit

saturncredit

Tracks every borrower's on-chain credit profile — a composite score from 0 to 1000 built from time in the system, on-time repayments, late payments, defaults, streak bonuses, and partial-repayment incentives. Scores are cached on every loan event and can be recomputed on demand. Integrate this contract to gate loan eligibility, display credit tiers in your UI, or build credit-aware analytics dashboards — no off-chain oracle required.

User Registration

registerUser()

WRITE
registerUser(from: address): void

Registers a wallet as a borrower, locking in the credit-history start time. The score clock starts here: time-based bonuses accrue from the registration timestamp. Call this before a user's first loan application so their time-in-system points build up. Reverts if the user is already registered. The protocol also auto-registers users on first loan attempt via an internal path, but explicit registration lets users build tenure earlier.

Parameters
NameTypeDescription
fromaddressThe borrower's wallet address. Must be the transaction signer.
What to expect
Reverts if `from` is already registered. Reverts if the reentrancy guard (saturnlendcfg.acquireGuard) fires. Initializes all counters to 0 and sets cachedScore to the protocol base score.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturncredit", "registerUser", [from])
  .spendGas(from)
  .endScript();

Score Computation

computeScore()

WRITE
computeScore(user: address): number

Recomputes and persists the user's credit score on-chain, then returns it. The formula: baseScore + timeBonusCap(daysInSystem × timeBonusPerDay) + onTimeRepayCap(onTimeRepays × onTimeRepayBonus) + streakBonusCap(bestStreak × streakBonus) + min(partialRepays × partialRepayBonus, 50) − (lateRepays × lateRepayPenalty) − (defaults × defaultPenalty), clamped to [0, maxScore]. Call this after significant credit events to force a fresh calculation, or call it as a write-then-read to surface the latest score in your dApp. The result is also stored in userCachedScore and timestamped.

Parameters
NameTypeDescription
useraddressThe borrower whose score to recompute.
Returns
number — Updated credit score in the range [0, 1000].
What to expect
Reverts if the user is not registered. Score is always clamped to [0, maxScore] (typically 1000). Each component is individually capped before summing.
Example
// Force a score refresh, then read back the cached value
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturncredit", "computeScore", [userAddr])
  .spendGas(from)
  .endScript();
// After confirmation, read the fresh score:
const score = await readContract("saturncredit", "getUserCachedScore", [userAddr]);

Credit Score & Status Reads

getUserCachedScore()

READ
getUserCachedScore(user: address): number

Returns the most recently persisted credit score for this user. Updated on every loan lifecycle event (origination, repayment, default) and by explicit computeScore calls. Use this for low-latency reads — it costs no gas and reflects the last event.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cached score in [0, 1000]. Returns 0 if the user is unregistered.
What to expect
Returns 0 for unregistered users (no storage entry exists). Never exceeds maxScore.
Example
const score = await readContract("saturncredit", "getUserCachedScore", [userAddr]);
// e.g. 720

getUserScoreTimestamp()

READ
getUserScoreTimestamp(user: address): number

Returns the Unix timestamp (seconds) when the cached score was last written. Use this alongside getUserCachedScore to tell users how fresh the displayed score is, and to decide whether to call computeScore for an up-to-date value.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Unix timestamp of the last score update.
What to expect
Returns 0 for unregistered users.

getUserRegistered()

READ
getUserRegistered(user: address): number

Returns 1 if the address is registered in the credit system, 0 otherwise. Gate any credit-dependent UI on this check before displaying a score.

Parameters
NameTypeDescription
useraddressThe address to check.
Returns
number — 1 = registered, 0 = not registered.
Example
const isReg = await readContract("saturncredit", "getUserRegistered", [userAddr]);
if (isReg === 1) { /* show score */ }

getUserRegisteredAt()

READ
getUserRegisteredAt(user: address): number

Returns the Unix timestamp when the user first registered. The time-bonus component of the credit score accrues from this date, so earlier registration means higher potential score.

Parameters
NameTypeDescription
useraddressThe registered borrower.
Returns
number — Unix timestamp of registration.

Repayment History Reads

getUserOnTimeRepays()

READ
getUserOnTimeRepays(user: address): number

Count of installments or full repayments made on time. Each contributes onTimeRepayBonus points to the credit score, capped at onTimeRepayCap.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cumulative on-time repayment count.

getUserLateRepays()

READ
getUserLateRepays(user: address): number

Count of late payments received. Each deducts lateRepayPenalty points from the credit score (uncapped penalty).

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cumulative late repayment count.

getUserDefaults()

READ
getUserDefaults(user: address): number

Count of loan defaults. Defaults carry the heaviest penalty in the scoring formula and permanently reset the consecutive-repayment streak to zero.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cumulative default count.

getUserPartialRepays()

READ
getUserPartialRepays(user: address): number

Count of early or partial repayment events. Each adds partialRepayBonus points, capped at 50 total across all partials. Encourages borrowers to pay ahead of schedule.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cumulative early/partial repayment count.

getUserTotalRepaid()

READ
getUserTotalRepaid(user: address): number

Total principal repaid by this user across all loans, in scaled (8-decimal) units. Useful for displaying lifetime repayment volume on a borrower dashboard.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Sum of all repaid principal in 8-decimal scaled units.

getUserLastActivity()

READ
getUserLastActivity(user: address): number

Unix timestamp of the most recent loan lifecycle event (loan created, repayment, default). Use this to show how recently a borrower was active.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Unix timestamp of last event.

Streak & Loan Count Reads

getUserConsecutiveOnTime()

READ
getUserConsecutiveOnTime(user: address): number

The current unbroken run of on-time repayments. Resets to 0 on a late payment or default. Useful for showing a user their active streak as a retention / gamification signal.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Active consecutive on-time repayment count.

getUserBestStreak()

READ
getUserBestStreak(user: address): number

The all-time longest consecutive on-time repayment streak for this user. This is the streak value that feeds the credit score formula (not the current streak), so it is never reset by a missed payment.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Best-ever on-time repayment streak.
Example
const best = await readContract("saturncredit", "getUserBestStreak", [userAddr]);
const current = await readContract("saturncredit", "getUserConsecutiveOnTime", [userAddr]);

getUserTotalLoans()

READ
getUserTotalLoans(user: address): number

Lifetime count of loans ever originated by this borrower, regardless of outcome.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Total loans originated.

getUserActiveLoans()

READ
getUserActiveLoans(user: address): number

Count of currently open (unpaid) loans. Increments on loan creation, decrements on full repayment or close. Lending contracts may gate new loan requests while this exceeds a threshold.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Number of currently active loans.

Protocol-Level Stats

getTotalRegisteredUsers()

READ
getTotalRegisteredUsers(): number

Total number of distinct addresses that have ever registered in the credit system. Useful for protocol analytics and growth dashboards.

Returns
number — Cumulative registered-user count.
Example
const total = await readContract("saturncredit", "getTotalRegisteredUsers", []);

getTotalLoansIssued()

READ
getTotalLoansIssued(): number

Cumulative count of all loans ever created across the protocol. Incremented by markLoanCreated on every loan origination.

Returns
number — Total loans ever issued.

getTotalDefaults()

READ
getTotalDefaults(): number

Cumulative default count across all borrowers. Track this alongside getTotalLoansIssued to compute the protocol-wide default rate.

Returns
number — Cumulative default events.

Credit Report

getCreditReport()

READ
getCreditReport(user: address): string

Returns a packed summary string of the user's full credit profile in a single call — score, repayment counts, streak, and loan totals. Format: `score:N_onTime:N_late:N_defaults:N_bestStreak:N_totalLoans:N_active:N`. Ideal for displaying a compact credit card in lending UIs without making seven individual read calls.

Parameters
NameTypeDescription
useraddressThe registered borrower's address.
Returns
string — Packed credit profile string, e.g. "score:720_onTime:14_late:1_defaults:0_bestStreak:12_totalLoans:15_active:1".
What to expect
Reverts with 'User not registered' if the address has not registered. Score shown is the cached value — call computeScore first to force a refresh.
Example
const report = await readContract("saturncredit", "getCreditReport", [userAddr]);
// "score:720_onTime:14_late:1_defaults:0_bestStreak:12_totalLoans:15_active:1"
const parts = Object.fromEntries(report.split("_").map(p => p.split(":")));
console.log(`Score: ${parts.score}, Defaults: ${parts.defaults}`);
Lending Protocol · Contract #3

SaturnVault

saturnvault

Custodian for all loan collateral on the Saturn lending protocol. Accepts two active collateral types: v4 DEX LP pools (locked in-place via FinancialLock so swaps continue accruing fees while the loan is live) and v3 SATRN LP NFTs (held in vault custody). Every collateral position is assigned a numeric ID and priced on demand in any RA-paired token via the saturndexadapt routing layer. Integrators read this contract to render collateral cards, check valuations, and build liquidation monitors.

Collateral Views — Position Info

getCollateralType()

READ
getCollateralType(colId: number): number

Returns the collateral type flag for a given position: 1 = single token (disabled), 2 = v4 LP pool, 3 = v3 LP NFT.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
number — 1 = token (disabled), 2 = v4 pool, 3 = v3 LP NFT.
Example
const cType = await readContract("saturnvault", "getCollateralType", [colId]);
// 2 = v4 pool, 3 = v3 LP NFT

getCollateralOwner()

READ
getCollateralOwner(colId: number): address

Returns the borrower address that deposited this collateral position.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
address — Address of the depositing borrower.

getCollateralLoanId()

READ
getCollateralLoanId(colId: number): number

Returns the loan ID this collateral is linked to, or 0 if the position is not yet linked to any loan. A position may be deposited before a loan is formally opened.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
number — Linked loan ID, or 0 if unlinked.

getCollateralStatus()

READ
getCollateralStatus(colId: number): number

Returns the lifecycle status of the collateral: 1 = locked (active), 2 = released (returned to borrower on repayment), 3 = liquidated (transferred to lender after default).

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
number — 1 = locked, 2 = released, 3 = liquidated.
Example
const status = await readContract("saturnvault", "getCollateralStatus", [colId]);
const labels = { 1: "Locked", 2: "Released", 3: "Liquidated" };
console.log(labels[status]);

getCollateralDexVersion()

READ
getCollateralDexVersion(colId: number): number

Returns which DEX version underpins this collateral: 1 = v3 (SATRN), 2 = v4 (saturnpools). For type-2 collateral this is always 2; for type-3 always 1.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
number — 1 = v3 SATRN, 2 = v4 saturnpools.

getCollateralSummary()

READ
getCollateralSummary(colId: number): string

Returns a packed single-string summary of any collateral position — type, DEX version, status, linked loan, and the key asset identifiers. Format varies by type: token positions include symbol/amount; v4 pool positions include poolId and pair; v3 NFT positions include nftId and pair key. Use this for concise collateral cards without multiple round-trips.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
string — Packed string, e.g. "type:v4pool_dex:2_status:1_loan:7_poolId:3_pair:KCAL_RA" or "type:v3lpnft_dex:1_status:1_loan:7_nftId:42_pair:SOUL_RA".
Example
const summary = await readContract("saturnvault", "getCollateralSummary", [colId]);
const fields = Object.fromEntries(summary.split("_").map(p => p.split(":")));

getNextCollateralId()

READ
getNextCollateralId(): number

Returns the ID that will be assigned to the next deposited collateral position. Collateral IDs are auto-incrementing from 1. Use this to predict the incoming ID before a deposit, or to iterate all positions from 1 to nextCollateralId - 1.

Returns
number — Next collateral ID (1-based, auto-incremented).
Example
const nextId = await readContract("saturnvault", "getNextCollateralId", []);

Collateral Views — User Index

getUserCollateralCount()

READ
getUserCollateralCount(user: address): number

Returns how many collateral positions a user has ever deposited (all statuses: locked, released, and liquidated). Use this as the loop bound when calling getUserCollateralAtIndex to enumerate a borrower's full collateral history.

Parameters
NameTypeDescription
useraddressBorrower's address.
Returns
number — Total collateral positions registered for this user.
Example
const count = await readContract("saturnvault", "getUserCollateralCount", [borrower]);
for (let i = 0; i < count; i++) {
  const colId = await readContract("saturnvault", "getUserCollateralAtIndex", [borrower, i]);
  const summary = await readContract("saturnvault", "getCollateralSummary", [colId]);
}

getUserCollateralAtIndex()

READ
getUserCollateralAtIndex(user: address, index: number): number

Returns the collateral ID at a specific index in a user's collateral list. Indexes are 0-based and ordered by deposit time. Combine with getUserCollateralCount to page through a borrower's complete collateral history.

Parameters
NameTypeDescription
useraddressBorrower's address.
indexnumber0-based index into the user's collateral list.
Returns
number — Collateral position ID at the given index.
What to expect
Returns 0 (default map value) if index is out of range — check against getUserCollateralCount first.

Collateral Views — V4 Pool Fields

getCollateralPoolId()

READ
getCollateralPoolId(colId: number): number

Returns the v4 DEX pool ID locked as collateral. Use with saturnpools to look up live reserves and pool state.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (must be type 2).
Returns
number — v4 pool ID.

getCollateralPoolTokenA()

READ
getCollateralPoolTokenA(colId: number): string

Returns the symbol of token A in the locked v4 pool.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 2).
Returns
string — Token A symbol (e.g. "KCAL").

getCollateralPoolTokenB()

READ
getCollateralPoolTokenB(colId: number): string

Returns the symbol of token B in the locked v4 pool.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 2).
Returns
string — Token B symbol (e.g. "RA").

getCollateralPoolReserveAAtLock()

READ
getCollateralPoolReserveAAtLock(colId: number): number

Returns the snapshot of token A's reserve recorded at the moment the pool was locked. Compare against current reserves to quantify fee accrual and impermanent loss since collateral was posted.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 2).
Returns
number — Token A reserve at lock time (scaled units).

getCollateralPoolReserveBAtLock()

READ
getCollateralPoolReserveBAtLock(colId: number): number

Returns the snapshot of token B's reserve at lock time. Pair with getCollateralPoolReserveAAtLock to reconstruct the pool's price and depth at the time collateral was posted.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 2).
Returns
number — Token B reserve at lock time (scaled units).

Collateral Views — V3 LP NFT Fields

getCollateralNftId()

READ
getCollateralNftId(colId: number): number

Returns the SATRN NFT ID held in vault custody for a v3 LP NFT collateral position.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (must be type 3).
Returns
number — SATRN LP NFT ID.

getCollateralNftPairKey()

READ
getCollateralNftPairKey(colId: number): string

Returns the cached pair key string for the v3 LP NFT (format: "TOKENA_TOKENB"). Cached at deposit time from saturndexadapt.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 3).
Returns
string — Pair key string, e.g. "SOUL_RA".

getCollateralNftTokenA()

READ
getCollateralNftTokenA(colId: number): string

Returns the symbol of token A in the v3 LP NFT pair.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 3).
Returns
string — Token A symbol.

getCollateralNftTokenB()

READ
getCollateralNftTokenB(colId: number): string

Returns the symbol of token B in the v3 LP NFT pair.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 3).
Returns
string — Token B symbol.

getCollateralNftLiquidityAtLock()

READ
getCollateralNftLiquidityAtLock(colId: number): number

Returns the liquidity snapshot recorded when the SATRN LP NFT was deposited into the vault. Use this as the baseline to assess how the pool's depth has changed since the NFT was locked.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 3).
Returns
number — NFT liquidity at deposit time.

Collateral Valuation

getCollateralValue()

READ
getCollateralValue(colId: number, baseToken: string, baseDex: number): number

Generic valuation dispatcher: reads the collateral type and routes to the correct type-specific valuation. Returns the position's current value denominated in baseToken, scaled to 8 decimals. The baseToken must be RA-paired on the chosen baseDex. This is the primary call for LTV monitoring — use it to compute the collateral-to-debt ratio at any time.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
baseTokenstringToken symbol to denominate the value in (e.g. "RA").
baseDexnumberDEX version (1 = v3, 2 = v4) hosting the RA pricing pool for baseToken.
Returns
number — Current collateral value in baseToken, 8-decimal scaled.
What to expect
Reverts with 'Unknown collateral type' if the stored type is not 1/2/3. Reverts if the underlying pool lookup fails. For type-1 (disabled) collateral, routes through getTokenCollateralValue which reads live DEX state.
Example
// Get current value of collateral position #5 denominated in RA (v4 DEX)
const value = await readContract("saturnvault", "getCollateralValue", [5, "RA", 2]);
// Scaled 8-decimal number — divide by 1e8 for display

getV4PoolCollateralValue()

READ
getV4PoolCollateralValue(colId: number, baseToken: string, baseDex: number): number

Directly values a type-2 (v4 pool) collateral position by calling saturndexadapt.v4PoolValueInBase with the locked pool ID. Use when you already know the position is a v4 pool and want to skip the type-dispatch overhead.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 2).
baseTokenstringToken symbol to denominate the value in.
baseDexnumberDEX version for baseToken's RA pricing pool.
Returns
number — Pool value in baseToken, 8-decimal scaled.
What to expect
Reverts with 'Not v4 pool collateral' if colId is not type 2.
Example
const poolVal = await readContract("saturnvault", "getV4PoolCollateralValue", [colId, "RA", 2]);

getV3LpNftCollateralValue()

READ
getV3LpNftCollateralValue(colId: number, baseToken: string, baseDex: number): number

Values a type-3 (v3 LP NFT) collateral position by delegating to saturndexadapt.v3LpNftValueInBase using the stored NFT ID. Reflects live reserve state of the v3 pool as fee accrual continues while the NFT is in custody.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 3).
baseTokenstringToken symbol to denominate the value in.
baseDexnumberDEX version for baseToken's RA pricing pool.
Returns
number — NFT LP value in baseToken, 8-decimal scaled.
What to expect
Reverts with 'Not v3 LP NFT collateral' if colId is not type 3.
Example
const nftVal = await readContract("saturnvault", "getV3LpNftCollateralValue", [colId, "RA", 1]);

getTokenCollateralValue()

READ
getTokenCollateralValue(colId: number, baseToken: string, baseDex: number): number

Values a type-1 (single token) collateral position. Returns the stored raw amount scaled up and converted to baseToken via the RA-pair pricing path. Although depositTokenCollateral is disabled, this read path remains active for positions created before the v1.0 cutoff.

Parameters
NameTypeDescription
colIdnumberCollateral position ID (type 1).
baseTokenstringToken symbol to denominate the value in.
baseDexnumberDEX version for baseToken's RA pricing pool.
Returns
number — Token value in baseToken, 8-decimal scaled.
What to expect
Reverts with 'Not token collateral' if colId is not type 1.

Single-Token Collateral (Disabled in v1.0)

depositTokenCollateral()

WRITE
depositTokenCollateral(from: address, tokenSymbol: string, amount: number, dexVersion: number): number

Deposits a single ERC-20-style token as loan collateral. The token must be RA itself or have an RA-paired pool on the chosen DEX for LTV pricing. Disabled in v1.0 — the body immediately reverts with "Token collateral disabled in v1.0 - use v4 LP pool or v3 LP NFT". Use depositV4PoolCollateral (via saturnmarket) for an active v4 LP pool, or depositV3LpNftCollateral (via saturnmarket) for a v3 LP NFT. The ABI and storage layout are preserved on-chain so in-flight pre-upgrade positions retain their full lifecycle.

Parameters
NameTypeDescription
fromaddressBorrower's address and transaction signer.
tokenSymbolstringSymbol of the token to post as collateral.
amountnumberRaw (unscaled) token amount to deposit.
dexVersionnumberDEX version (1 = v3, 2 = v4) hosting the token's RA pricing pool.
Returns
number — Would return the new collateral ID — always reverts in v1.0.
What to expect
Always reverts in v1.0. Use LP-backed collateral via saturnmarket instead.
Lending Protocol · Contract #4

SaturnLoans

saturnloans

Central on-chain ledger for every loan in the Saturn lending protocol. Stores principal, interest rate, total owed, repayment progress, installment schedule, collateral link, and a four-state status machine (active → repaid / defaulted → liquidated). Borrowers call makePayment() directly; anyone can permissionlessly enforce defaults or LTV-based liquidations via triggerDefault() and triggerLiquidation(). A rich set of pure view methods lets integrators reconstruct full loan state in a single pass. Loan creation is driven internally by saturnmarket (P2P) and saturnauto (algorithmic) — not by end-user calls.

Loan Repayment

makePayment()

WRITE
makePayment(from: address, loanId: number, paymentAmount: number)

Borrower repays part or all of their loan. The payment is split between the lender and the protocol fee treasury based on the interest portion. Installment tracking and the next-due timestamp are advanced automatically. If the payment fully clears the outstanding balance (totalRepaid >= totalOwed) the loan status moves to 2 (repaid), collateral is released back to the borrower, the credit score is updated, and TAZ rewards are distributed. Overpayments are silently clamped to the remaining balance — never reverting due to rounding.

Parameters
NameTypeDescription
fromaddressBorrower's address — must be the transaction witness and the loan's registered borrower.
loanIdnumberID of the active loan to pay against.
paymentAmountnumberRaw-unit amount of the loan token to transfer. Clamped to remaining balance if larger.
What to expect
Reverts if: loan status != 1 (active); caller is not the borrower; borrower token balance < paymentAmount; re-entrancy guard active. paymentAmount must be > 0.
Example
// Repay 50 KCAL towards loan #7
const from = "S...myWallet";
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturnloans", "makePayment", [from, 7, 5000000000]) // 50 KCAL (8-decimal scaled)
  .spendGas(from)
  .endScript();

Default & Liquidation Enforcement

triggerDefault()

WRITE
triggerDefault(loanId: number)

Permissionlessly marks a loan as defaulted once the due date plus the grace period has passed. Moves status from 1 (active) → 3 (defaulted) → 4 (liquidated) within the same call, dispatches collateral liquidation by type (token → lender, V4 pool → admin resolution, V3 LP NFT → lender), and updates the borrower's credit score. Anyone — bots, lenders, or keepers — can call this; no witness check is required beyond the loan's time constraints.

Parameters
NameTypeDescription
loanIdnumberID of the active loan to default.
What to expect
Reverts if loan status != 1 (active), or if the current timestamp is not past dueDate + gracePeriod. Idempotent per loan — once status != 1 the check at the top reverts.
Example
// Keeper bot enforcing a stale loan
const tx = sb.begin()
  .allowGas(keeperAddr)
  .callContract("saturnloans", "triggerDefault", [loanId])
  .spendGas(keeperAddr)
  .endScript();

triggerLiquidation()

WRITE
triggerLiquidation(loanId: number)

Permissionlessly liquidates a loan whose current LTV has risen above the liquidation threshold configured in saturnlendcfg. Moves status to 4 (liquidated), dispatches collateral transfer, and impacts the borrower's credit score as a default. Use getCurrentLtv() first to check whether the threshold is breached before submitting. Anyone can call — useful for liquidation bots monitoring under-collateralised positions.

Parameters
NameTypeDescription
loanIdnumberID of the active loan to liquidate.
What to expect
Reverts if loan status != 1 (active), or if getCurrentLtv(loanId) <= liquidationThreshold. Threshold is typically expressed per 10,000 (e.g., 8,000 = 80% LTV).
Example
// Check LTV then liquidate if eligible
const ltv = await readContract("saturnloans", "getCurrentLtv", [loanId]);
const threshold = await readContract("saturnlendcfg", "getLiquidationThreshold", []);
if (ltv > threshold) {
  const tx = sb.begin()
    .allowGas(botAddr)
    .callContract("saturnloans", "triggerLiquidation", [loanId])
    .spendGas(botAddr)
    .endScript();
}

Loan Status Checks

installmentOverdue()

READ
installmentOverdue(loanId: number): number

Returns 1 if the current installment is overdue (past its due timestamp plus the grace period), 0 otherwise. Returns 0 immediately for any non-active loan, so it is safe to call on any loan ID without checking status first. Use this to power overdue-payment warnings in your UI or to decide whether to flag a credit score degradation.

Parameters
NameTypeDescription
loanIdnumberID of the loan to inspect.
Returns
number — 1 if current installment is overdue, 0 if on-time or loan is not active.
What to expect
Never reverts. Returns 0 for non-existent or closed loans.
Example
const overdue = await readContract("saturnloans", "installmentOverdue", [loanId]);
if (overdue === "1") showWarningBanner("Installment overdue — pay now to protect your credit score.");

getCurrentLtv()

READ
getCurrentLtv(loanId: number): number

Returns the live loan-to-value ratio of the position, expressed per 10,000 (e.g., 7,500 = 75% LTV). Computes the remaining balance divided by the current collateral value in loan-token terms, using the RA anchor pool for pricing. If collateral value is zero — e.g., a de-listed token — returns 10,000 (100% LTV) to signal immediate liquidation eligibility.

Parameters
NameTypeDescription
loanIdnumberID of the loan to inspect.
Returns
number — LTV per 10,000. 0 means fully repaid; 10,000 means collateral is worthless.
What to expect
Returns 0 if remaining balance is 0 (fully repaid). Returns 10,000 if collateral price is 0. Calls saturnvault and saturndexadapt — reverts if those contracts revert.
Example
const ltv = await readContract("saturnloans", "getCurrentLtv", [loanId]);
// ltv=7500 → 75% LTV; threshold is typically 8000 → safe

Loan Core Data

getLoanBorrower()

READ
getLoanBorrower(loanId: number): address

Returns the borrower address for the given loan ID.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
address — Borrower's wallet address.

getLoanLender()

READ
getLoanLender(loanId: number): address

Returns the lender address. For auto loans this is the saturnauto contract address; for P2P loans via saturnmarket it is the individual lender's wallet.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
address — Lender address.

getLoanToken()

READ
getLoanToken(loanId: number): string

Returns the symbol of the token that was lent (e.g., "KCAL").

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
string — Token symbol of the loan currency.

getLoanTokenDex()

READ
getLoanTokenDex(loanId: number): number

Returns the DEX version used to price this loan token via the RA anchor pool. 1 = Saturn V3 (SATRN string-keyed pools); 2 = Saturn V4 (saturnpools numeric IDs).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — 1 for V3, 2 for V4.

getLoanPrincipal()

READ
getLoanPrincipal(loanId: number): number

Returns the original scaled principal amount at origination.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Original principal in scaled (8-decimal) units.

getLoanInterestRate()

READ
getLoanInterestRate(loanId: number): number

Returns the annual interest rate for this loan, expressed per 10,000 (e.g., 500 = 5%).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Annual rate per 10,000.

getLoanTotalOwed()

READ
getLoanTotalOwed(loanId: number): number

Returns the total amount owed (principal + interest) fixed at origination in scaled units.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Total owed in scaled units.

getLoanTotalRepaid()

READ
getLoanTotalRepaid(loanId: number): number

Returns the cumulative amount already repaid in scaled units.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Total repaid so far in scaled units.

getLoanRemaining()

READ
getLoanRemaining(loanId: number): number

Convenience view — returns totalOwed minus totalRepaid, floored at 0. Use this for the "amount still owed" figure rather than computing it client-side.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Remaining balance in scaled units; 0 if fully repaid.
Example
const remaining = await readContract("saturnloans", "getLoanRemaining", [loanId]);

getLoanCollateralId()

READ
getLoanCollateralId(loanId: number): number

Returns the collateral ID in saturnvault linked to this loan. Pass this ID to saturnvault view methods to inspect collateral type, value, and status.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Collateral entry ID in saturnvault.

getLoanStatus()

READ
getLoanStatus(loanId: number): number

Returns the current status code: 1 = active, 2 = repaid, 3 = defaulted, 4 = liquidated. Note: triggerDefault() moves through 3 → 4 atomically, so 3 is transient and rarely observed by polling.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — 1 active | 2 repaid | 3 defaulted | 4 liquidated.
Example
const status = await readContract("saturnloans", "getLoanStatus", [loanId]);
const labels = { "1": "Active", "2": "Repaid", "3": "Defaulted", "4": "Liquidated" };

getLoanOrigin()

READ
getLoanOrigin(loanId: number): number

Returns 1 if the loan was originated by saturnauto (algorithmic), 2 if originated by saturnmarket (P2P).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — 1 = auto, 2 = P2P market.

getLoanOriginationFee()

READ
getLoanOriginationFee(loanId: number): number

Returns the one-time origination fee charged at loan creation in scaled units.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Origination fee in scaled units (typically 1% of principal).

getLoanSummary()

READ
getLoanSummary(loanId: number): string

Returns a single packed string with all headline loan fields — status, token, principal, total owed, total repaid, interest rate, origin, and installment progress. Formatted as "status:N_token:SYM_principal:N_owed:N_repaid:N_rate:N_origin:N_installments:paid/total". Ideal for single-call loan cards or log entries.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
string — Packed summary string.
Example
const summary = await readContract("saturnloans", "getLoanSummary", [loanId]);
// "status:1_token:KCAL_principal:500000000000_owed:525000000000_repaid:100000000000_rate:500_origin:2_installments:1/6"

Loan Timing

getLoanCreatedAt()

READ
getLoanCreatedAt(loanId: number): number

Returns the Unix timestamp of loan origination.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Origination timestamp in seconds.

getLoanDuration()

READ
getLoanDuration(loanId: number): number

Returns the total loan term in seconds (e.g., 2,592,000 = 30 days).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Loan duration in seconds.

getLoanDueDate()

READ
getLoanDueDate(loanId: number): number

Returns the Unix timestamp when the full balance is due (createdAt + duration). After this date plus the grace period, triggerDefault() may be called.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Due-date timestamp in seconds.
Example
const due = await readContract("saturnloans", "getLoanDueDate", [loanId]);
const daysLeft = Math.floor((due - Date.now() / 1000) / 86400);

getLoanNextInstallmentDue()

READ
getLoanNextInstallmentDue(loanId: number): number

Returns the Unix timestamp when the next installment payment is due. Updated by makePayment() after each payment. Compare to the current time plus the grace period to determine if installmentOverdue() will return 1.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Next installment due timestamp in seconds.

Installment Details

getLoanInstallmentCount()

READ
getLoanInstallmentCount(loanId: number): number

Returns the total number of installments scheduled for this loan (typically ceil(duration / 30 days)).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Total scheduled installment count.

getLoanInstallmentAmount()

READ
getLoanInstallmentAmount(loanId: number): number

Returns the equal per-installment amount in scaled units. Multiply by installmentCount to verify it equals totalOwed (modulo rounding).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Per-installment amount in scaled units.

getLoanInstallmentsPaid()

READ
getLoanInstallmentsPaid(loanId: number): number

Returns how many full installments have been satisfied so far. Derived from totalRepaid divided by installmentAmount — not a separate counter — so it reflects partial overpayments accurately.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Number of installments fully paid to date.

Borrower Loan Listing

getUserLoanCount()

READ
getUserLoanCount(user: address): number

Returns the total number of loans ever opened by this borrower (including closed and defaulted). Use as the upper bound when iterating getUserLoanAtIndex().

Parameters
NameTypeDescription
useraddressBorrower address to query.
Returns
number — Total loan count for this borrower.

getUserLoanAtIndex()

READ
getUserLoanAtIndex(user: address, index: number): number

Returns the loan ID at a zero-based index in the borrower's personal loan list. Iterate from 0 to getUserLoanCount(user) - 1 to enumerate all loans for a borrower.

Parameters
NameTypeDescription
useraddressBorrower address.
indexnumberZero-based index into the borrower's loan list.
Returns
number — Loan ID at that index.
Example
const count = await readContract("saturnloans", "getUserLoanCount", [borrowerAddr]);
const loanIds = await Promise.all(
  Array.from({ length: Number(count) }, (_, i) =>
    readContract("saturnloans", "getUserLoanAtIndex", [borrowerAddr, i])
  )
);

Lender Loan Listing

getLenderLoanCount()

READ
getLenderLoanCount(lender: address): number

Returns the total number of loans associated with a lender address, including both active and completed positions.

Parameters
NameTypeDescription
lenderaddressLender address to query.
Returns
number — Total loan count for this lender.

getLenderLoanAtIndex()

READ
getLenderLoanAtIndex(lender: address, index: number): number

Returns the loan ID at a zero-based index in the lender's loan list. Iterate from 0 to getLenderLoanCount(lender) - 1 to build a lender portfolio view.

Parameters
NameTypeDescription
lenderaddressLender address.
indexnumberZero-based index into the lender's loan list.
Returns
number — Loan ID at that index.
Example
const count = await readContract("saturnloans", "getLenderLoanCount", [lenderAddr]);
for (let i = 0; i < Number(count); i++) {
  const loanId = await readContract("saturnloans", "getLenderLoanAtIndex", [lenderAddr, i]);
  // then getLoanSummary(loanId)
}

Protocol Stats

getNextLoanId()

READ
getNextLoanId(): number

Returns the ID that will be assigned to the next loan created. Loan IDs are monotonically incremented from 1, so this equals totalLoansEverCreated + 1.

Returns
number — Next loan ID to be issued.

getTotalActiveLoans()

READ
getTotalActiveLoans(): number

Returns the count of currently active (status = 1) loans across the entire protocol.

Returns
number — Number of active loans.

getTotalCompletedLoans()

READ
getTotalCompletedLoans(): number

Returns the count of loans that have been fully repaid (status = 2).

Returns
number — Number of fully-repaid loans.

getTotalDefaultedLoans()

READ
getTotalDefaultedLoans(): number

Returns the count of loans that ended in default or liquidation (status 3 or 4). Note: triggerDefault() moves atomically to 4, so this counter reflects final liquidations as well.

Returns
number — Number of defaulted/liquidated loans.

getTotalPrincipalLent()

READ
getTotalPrincipalLent(): number

Returns the cumulative principal ever lent across all loans, in scaled units. Useful for protocol TVL displays and risk dashboards.

Returns
number — Cumulative principal lent in scaled units.

getTotalInterestEarned()

READ
getTotalInterestEarned(): number

Returns the cumulative interest collected on all fully-repaid loans, in scaled units. Note: defaulted/liquidated loans do not contribute to this counter.

Returns
number — Cumulative interest earned in scaled units.
Lending Protocol · Contract #5

SaturnAuto

saturnauto

Protocol-managed lending reserve with algorithmic loan origination. Lenders deposit tokens into a shared pool; borrowers receive auto-approved loans sized and priced by credit score (LTV 25–80%, rate 3–30%, term 7–365 days). LP collateral earns a 5% rate discount and 10% duration bonus. IMPORTANT: All state-mutating methods in this contract revert in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P". The full ABI and storage layout are preserved on-chain for a future in-place upgrade — integrators should target saturnmarket for all active loan origination. View methods and previewLoanTerms() remain fully functional.

Loan Term Preview

previewLoanTerms()

READ
previewLoanTerms(user: address, loanTokenSymbol: string, loanAmount: number): string

Computes and returns the full set of algorithmic loan terms that would apply to a given borrower and loan request — without creating a loan. Returns a packed string with all key parameters: credit score, max LTV, interest rate, duration, total interest, total owed, installment count and per-installment amount, and origination fee. Use this as a quote widget even while auto-lending is disabled — the calculation logic reads live credit scores and config, so it reflects current parameters.

Parameters
NameTypeDescription
useraddressThe prospective borrower whose credit score drives the terms.
loanTokenSymbolstringSymbol of the token to borrow (e.g., "KCAL").
loanAmountnumberRaw-unit borrow amount before scaling.
Returns
string — Packed string: "score:N_maxLTV:N_rate:N_duration:N_interest:N_totalOwed:N_installments:N_perInstallment:N_origFee:N".
What to expect
Reverts if the token is not approved in the saturnauto registry. Rate per 10,000 (500 = 5%). Duration in seconds. All amounts in scaled (8-decimal) units.
Example
const terms = await readContract("saturnauto", "previewLoanTerms",
  ["S...borrowerAddr", "KCAL", 100]);
// "score:720_maxLTV:6500_rate:450_duration:5184000_interest:10430..._totalOwed:110430..._installments:2_perInstallment:55215..._origFee:1000..."

Reserve Views

getReserveBalance()

READ
getReserveBalance(tokenSymbol: string): number

Returns the current available (unlent) scaled balance of a given token in the protocol reserve. Even though deposits are disabled in v1.0, the reserve may hold tokens from pre-launch testing; this call is safe.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to check (e.g., "KCAL").
Returns
number — Available reserve balance in scaled units.

getReserveTotalDeposited()

READ
getReserveTotalDeposited(tokenSymbol: string): number

Returns the cumulative total deposited into the reserve for a token (lifetime figure, not current balance).

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Cumulative deposits in scaled units.

getReserveTotalLent()

READ
getReserveTotalLent(tokenSymbol: string): number

Returns the cumulative total lent out from the reserve for a token (lifetime figure).

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Cumulative lent amount in scaled units.

getUtilizationRate()

READ
getUtilizationRate(tokenSymbol: string): number

Returns the utilization rate of the reserve for a given token, expressed per 10,000 (e.g., 6,500 = 65% utilised). Computed as totalLent / totalDeposited. Returns 0 if nothing has ever been deposited.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Utilization rate per 10,000.
Example
const util = await readContract("saturnauto", "getUtilizationRate", ["KCAL"]);
// util = "6500" → 65% of reserve is currently lent out

Depositor Views

getDepositorBalance()

READ
getDepositorBalance(depositor: address, tokenSymbol: string): number

Returns the scaled balance a specific depositor has in the reserve for a given token.

Parameters
NameTypeDescription
depositoraddressDepositor address.
tokenSymbolstringToken symbol.
Returns
number — Depositor's reserve balance in scaled units.

getDepositorCount()

READ
getDepositorCount(tokenSymbol: string): number

Returns the number of depositors who have funds in the reserve for a given token.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Number of unique depositors for this token.

Approved Token Registry Views

getTokenApproved()

READ
getTokenApproved(tokenSymbol: string): number

Returns 1 if the token is on the approved lending list, 0 otherwise. Even though lending is disabled in v1.0, the registry may be pre-populated for future use.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to check.
Returns
number — 1 if approved, 0 if not.

getApprovedTokenDex()

READ
getApprovedTokenDex(tokenSymbol: string): number

Returns the DEX version (1 = V3, 2 = V4) that hosts the RA pricing pool for this approved lending token.

Parameters
NameTypeDescription
tokenSymbolstringApproved lending token symbol.
Returns
number — 1 for V3, 2 for V4.

getApprovedTokenCount()

READ
getApprovedTokenCount(): number

Returns the total count of entries in the approved token list (including any that may have been revoked).

Returns
number — Number of entries in the approved token list.

getApprovedTokens()

READ
getApprovedTokens(): string*

Iterates the approved token list and yields each symbol whose approved flag is still set to 1. Returns a stream of strings (Tomb generator return type string*).

Returns
string* — Stream of approved token symbols.
Example
const tokens = await readContract("saturnauto", "getApprovedTokens", []);
// ["KCAL", "SOUL", ...]

getAutoLendingEnabled()

READ
getAutoLendingEnabled(): number

Returns the internal autoLendingEnabled flag (1 = enabled at the contract level, 0 = disabled). Note: in v1.0 all write paths unconditionally revert regardless of this flag — its value reflects the storage state but does not gate the disable. Use this to surface a status indicator in your UI.

Returns
number — 1 if the auto-lending flag is set, 0 if unset.

Reserve Deposits (Disabled in v1.0)

deposit()

WRITE
deposit(from: address, tokenSymbol: string, amount: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, this will allow lenders to deposit tokens into the protocol reserve. Use saturnmarket postLoanOffer() for P2P lending in the current release.

Parameters
NameTypeDescription
fromaddressDepositor's address (witness required).
tokenSymbolstringSymbol of the token to deposit.
amountnumberRaw-unit amount to deposit.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

withdraw()

WRITE
withdraw(from: address, tokenSymbol: string, amount: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, lenders will call this to retrieve their deposited tokens from the reserve. Use saturnmarket for all lender actions in the current release.

Parameters
NameTypeDescription
fromaddressDepositor's address (witness required).
tokenSymbolstringSymbol of the token to withdraw.
amountnumberRaw-unit amount to withdraw.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

Auto Loan Requests (Disabled in v1.0)

requestLoanWithToken()

WRITE
requestLoanWithToken(from: address, loanTokenSymbol: string, loanAmount: number, collateralTokenSymbol: string, collateralAmount: number, collateralDexVersion: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, borrowers will use this to request an auto loan backed by token collateral (single-token collateral is also disabled in v1.0 via saturnvault). For P2P borrowing, use saturnmarket postLoanRequest().

Parameters
NameTypeDescription
fromaddressBorrower's address (witness required).
loanTokenSymbolstringSymbol of the token to borrow.
loanAmountnumberRaw-unit amount to borrow.
collateralTokenSymbolstringSymbol of the token pledged as collateral.
collateralAmountnumberRaw-unit amount of collateral.
collateralDexVersionnumberDEX version (1=V3, 2=V4) for collateral pricing.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

requestLoanWithV4Pool()

WRITE
requestLoanWithV4Pool(from: address, loanTokenSymbol: string, loanAmount: number, poolId: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, borrowers will pledge a V4 LP pool position as collateral. The pool is locked via FinancialLock and provider fees continue accruing to the borrower during the loan term. For V4-pool-collateral P2P loans, use saturnmarket.

Parameters
NameTypeDescription
fromaddressBorrower's address (witness required).
loanTokenSymbolstringSymbol of the token to borrow.
loanAmountnumberRaw-unit amount to borrow.
poolIdnumberV4 pool ID to pledge as collateral.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

requestLoanWithV3Lp()

WRITE
requestLoanWithV3Lp(from: address, loanTokenSymbol: string, loanAmount: number, nftId: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, borrowers will pledge a V3 LP NFT as collateral (held in saturnvault custody; swap-fee growth accumulates in reserves and is realised on release). For V3-LP-collateral P2P loans, use saturnmarket.

Parameters
NameTypeDescription
fromaddressBorrower's address (witness required).
loanTokenSymbolstringSymbol of the token to borrow.
loanAmountnumberRaw-unit amount to borrow.
nftIdnumberV3 LP NFT series/token ID to pledge.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

Admin Token Approval (Disabled in v1.0)

approveTokenForLending()

WRITE
approveTokenForLending(tokenSymbol: string, dexVersion: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, allows the admin to whitelist a token and its RA-pricing DEX version for use in auto loans.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to approve.
dexVersionnumberDEX version (1=V3, 2=V4) for RA pricing.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

revokeTokenForLending()

WRITE
revokeTokenForLending(tokenSymbol: string)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, removes a token from the approved lending list.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to revoke.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".

toggleAutoLending()

WRITE
toggleAutoLending(enabled: number)

DISABLED in v1.0 — reverts with "Auto-lending disabled in v1.0 - use saturnmarket P2P". When re-enabled, allows the admin to flip the autoLendingEnabled flag (1 = on, 0 = off).

Parameters
NameTypeDescription
enablednumber1 to enable auto-lending, 0 to disable.
What to expect
Always reverts in v1.0 with "Auto-lending disabled in v1.0 - use saturnmarket P2P".
Lending Protocol · Contract #6

SaturnMarket

saturnmarket

The primary developer entry-point for Saturn Lending v1.0. Borrowers post loan requests backed by LP collateral (v4 pool or v3 LP NFT — single-token collateral is rejected in v1.0); lenders browse requests, submit binding quotes with escrowed funds, and the borrower atomically accepts a quote to open the loan. Collateral is locked by saturnvault, funds disbursed minus the origination fee, and the live loan is tracked by saturnloans. Build quote browsers, borrower dashboards, and lending bots entirely through this contract's ~52 public methods.

Borrower — Request a Loan

postLoanRequest()

WRITE
postLoanRequest(from: address, loanTokenSymbol: string, loanDexVersion: number, loanAmount: number, collateralType: number, collateralTokenSymbol: string, collateralTokenAmount: number, collateralDexVersion: number, collateralPoolId: number, collateralNftId: number, preferredDuration: number, maxInterestRate: number, message: string, expiresInSeconds: number): none

Publishes a new loan request to the P2P marketplace. The borrower declares what token and how much they want to borrow, which DEX prices that token against RA, and what LP collateral they are offering. collateralType must be 2 (v4 pool) or 3 (v3 LP NFT) — collateralType 1 (single token) is rejected in v1.0. In v1.0 only TAZ loans are accepted. Pass 0 for the fields that don't apply to your collateral type (e.g. collateralTokenSymbol/collateralTokenAmount/collateralDexVersion when using a pool). The borrower must already be registered in saturncredit. The request stays open until cancelled, accepted, or expiresInSeconds elapses (min 1 day, max 30 days).

Parameters
NameTypeDescription
fromaddressBorrower's address. Must be the transaction witness.
loanTokenSymbolstringToken to borrow. Must be TAZ in v1.0 and must have an RA pricing pool on the chosen DEX.
loanDexVersionnumberDEX used to price the loan token against RA. 1 = Saturn V3, 2 = Saturn V4.
loanAmountnumberRaw-unit amount of loanTokenSymbol the borrower wants to receive.
collateralTypenumber2 = v4 LP pool (FinancialLock), 3 = v3 LP NFT. collateralType 1 reverts in v1.0.
collateralTokenSymbolstringToken symbol for type-1 collateral. Pass empty string for types 2 and 3.
collateralTokenAmountnumberRaw-unit token amount for type-1 collateral. Pass 0 for types 2 and 3.
collateralDexVersionnumberDEX that prices the collateral token (type 1 only). Pass 0 for types 2 and 3.
collateralPoolIdnumberv4 pool ID the borrower is pledging (type 2). Must be active, owned, unlocked, and RA-paired. Pass 0 for types 1 and 3.
collateralNftIdnumberv3 LP NFT ID the borrower is pledging (type 3). Must be RA-paired and have non-zero liquidity. Pass 0 for types 1 and 2.
preferredDurationnumberPreferred loan duration in seconds. Advisory only — lenders may quote different durations.
maxInterestRatenumberMaximum per-period interest rate the borrower will accept, in basis points. Quotes above this rate will be visible but the borrower is expected to filter them.
messagestringOptional freeform note shown to lenders (e.g. reason, preferred terms). May be empty.
expiresInSecondsnumberSeconds from now until the request auto-expires. Range: 86400 (1 day) to 2592000 (30 days).
What to expect
Reverts if: market is disabled, loanAmount <= 0, loanDexVersion not 1 or 2, collateralType == 1 (disabled), collateralType outside 1-3, maxInterestRate <= 0, expiresInSeconds < 86400 or > 2592000, loanTokenSymbol != 'TAZ' (v1.0), loan token has no RA pool on chosen DEX, borrower not registered in saturncredit. For type 2: pool not active / not owned by from / already locked / not RA-paired. For type 3: NFT not RA-paired or has no liquidity.
Example
// Offer v4 pool #42 as collateral, borrow 500 TAZ, max 20% rate, 7-day window
const tx = sb.begin()
  .allowGas(borrower)
  .callContract("saturnmarket", "postLoanRequest", [
    borrower,   // from
    "TAZ",      // loanTokenSymbol
    2,          // loanDexVersion  (v4)
    500_000_000,// loanAmount      (500 TAZ @ 6 decimals)
    2,          // collateralType  (v4 LP pool)
    "",         // collateralTokenSymbol (unused)
    0,          // collateralTokenAmount (unused)
    0,          // collateralDexVersion  (unused)
    42,         // collateralPoolId
    0,          // collateralNftId       (unused)
    604800,     // preferredDuration  (7 days)
    2000,       // maxInterestRate    (20%, basis pts)
    "Collateral is TAZ/SOUL pool",
    604800      // expiresInSeconds   (7 days)
  ])
  .spendGas(borrower)
  .endScript();

cancelRequest()

WRITE
cancelRequest(from: address, requestId: number): none

Cancels an open loan request. Only the borrower who posted the request may cancel it, and only while status is 1 (open). Sets status to 3 (cancelled) and decrements totalOpenRequests. Outstanding quotes on the request remain visible but lenders should withdrawQuote to recover their escrowed funds.

Parameters
NameTypeDescription
fromaddressBorrower's address. Must match the request's stored borrower and be the transaction witness.
requestIdnumberID of the loan request to cancel.
What to expect
Reverts if: from is not the witness, from != reqBorrower[requestId], or request status != 1 (open).
Example
const tx = sb.begin()
  .allowGas(borrower)
  .callContract("saturnmarket", "cancelRequest", [borrower, requestId])
  .spendGas(borrower)
  .endScript();

Lender — Quote a Loan

submitQuote()

WRITE
submitQuote(from: address, requestId: number, interestRate: number, duration: number, offeredLoanAmount: number, colType: number, colTokenSymbol: string, colTokenAmount: number, colDexVersion: number, colPoolId: number, colNftId: number, message: string, expiresInSeconds: number): none

Submits a lending quote against an open loan request. The lender specifies their rate, duration, exact loan amount, and the collateral they require from the borrower. The offered loan amount is immediately escrowed from the lender's wallet into the contract so that acceptQuote does not require a second lender signature. colType must be 2 or 3 (LP-backed) — colType 1 (single token) is rejected in v1.0. The lender must hold sufficient funds to cover offeredLoanAmount at time of submission.

Parameters
NameTypeDescription
fromaddressLender's address. Must be the transaction witness. Cannot be the borrower of the target request.
requestIdnumberThe open loan request this quote is for.
interestRatenumberPer-period interest rate in basis points offered by the lender. Must be > 0.
durationnumberLoan term in seconds. Must be within saturnlendcfg's [minLoanDuration, maxLoanDuration] range.
offeredLoanAmountnumberRaw-unit amount of the request's loan token the lender is willing to disburse. Immediately escrowed.
colTypenumberCollateral type the lender demands. 2 = v4 LP pool, 3 = v3 LP NFT. colType 1 reverts in v1.0.
colTokenSymbolstringRequired token symbol for type-1 collateral. Pass empty string for types 2 and 3.
colTokenAmountnumberRequired token amount for type-1 collateral. Pass 0 for types 2 and 3.
colDexVersionnumberDEX version used to price type-1 collateral. Pass 0 for types 2 and 3.
colPoolIdnumberSpecific v4 pool ID required as collateral (type 2). Must be active and RA-paired. Pass 0 for types 1 and 3.
colNftIdnumberSpecific v3 LP NFT ID required as collateral (type 3). Must be RA-paired. Pass 0 for types 1 and 2.
messagestringOptional note to the borrower (terms, conditions). May be empty.
expiresInSecondsnumberSeconds until this quote auto-expires. Should be <= remaining lifetime of the target request.
What to expect
Reverts if: market disabled, request not open (status != 1), request expired, from == borrower, interestRate/duration/offeredLoanAmount <= 0, colType == 1 (disabled), duration outside protocol min/max, lender's balance < offeredLoanAmount. For colType 2: pool not active or not RA-paired. For colType 3: NFT not RA-paired.
Example
// Lender quotes 500 TAZ, demands v4 pool #99 as collateral, 15% rate, 14-day term
const tx = sb.begin()
  .allowGas(lender)
  .callContract("saturnmarket", "submitQuote", [
    lender,
    requestId,
    1500,        // interestRate  (15%)
    1209600,     // duration      (14 days in seconds)
    500_000_000, // offeredLoanAmount (escrowed immediately)
    2,           // colType  (v4 LP pool)
    "",          // colTokenSymbol  (unused)
    0,           // colTokenAmount  (unused)
    0,           // colDexVersion   (unused)
    99,          // colPoolId
    0,           // colNftId        (unused)
    "Standard 14-day TAZ loan",
    1209600      // expiresInSeconds
  ])
  .spendGas(lender)
  .endScript();

withdrawQuote()

WRITE
withdrawQuote(from: address, quoteId: number): none

Cancels a pending quote and refunds the escrowed loan amount to the lender. Only the lender who submitted the quote may withdraw it, and only while it is still pending (status 1). Sets status to 4 (withdrawn) and transfers the escrowed funds back to from. Call this if the underlying request was cancelled, your terms changed, or you need your funds back.

Parameters
NameTypeDescription
fromaddressLender's address. Must match quoteLender[quoteId] and be the transaction witness.
quoteIdnumberID of the quote to withdraw.
What to expect
Reverts if: from is not the witness, from != quoteLender[quoteId], or quote status != 1 (pending).
Example
const tx = sb.begin()
  .allowGas(lender)
  .callContract("saturnmarket", "withdrawQuote", [lender, quoteId])
  .spendGas(lender)
  .endScript();

Accept & Open

acceptQuote()

WRITE
acceptQuote(from: address, quoteId: number): none

Atomically accepts a lender's quote and opens the loan. The borrower's collateral is locked in saturnvault (v4 pool via FinancialLock or v3 LP NFT transferred to vault custody), the loan record is created in saturnloans, and the disbursement (escrowed amount minus origination fee) is transferred to the borrower. The origination fee goes to the protocol admin. Any rounding dust is returned to the lender. Both the quote and the parent request are marked as accepted (status 2) atomically. The resulting loanId is stored in reqLoanId[requestId] for later retrieval. Single-token collateral quotes (colType 1) are rejected as a defense-in-depth guard even if one somehow existed.

Parameters
NameTypeDescription
fromaddressBorrower's address. Must own the request that the quote is for, and be the transaction witness.
quoteIdnumberID of the lender's quote to accept.
What to expect
Reverts if: from is not the witness, quote status != 1 (pending), quote expired, from != borrower on the associated request, request status != 1 (open), escrow balance insufficient, borrower already at max concurrent loans (saturnlendcfg.getMaxLoansPerUser), colType == 1 (disabled). For colType 2: borrower must still own the pool. Emits a loan creation event via saturnloans.
Example
// Borrower accepts quote #7 — collateral locked, TAZ disbursed in the same tx
const tx = sb.begin()
  .allowGas(borrower)
  .callContract("saturnmarket", "acceptQuote", [borrower, quoteId])
  .spendGas(borrower)
  .endScript();

// After the tx confirms, fetch the new loan ID:
const requestId = await readContract("saturnmarket", "getQuoteRequestId", [quoteId]);
const loanId    = await readContract("saturnmarket", "getRequestLoanId",  [requestId]);

Market Views

getBorrowerProfile()

READ
getBorrowerProfile(borrower: address): string

Returns a packed credit summary for a borrower address, useful for lender UIs that need to show creditworthiness at a glance. The string is prefixed with the number of days the borrower has been in the system, then appended with the full credit report from saturncredit. Format: "daysInSystem:<N>_<creditReport>" (e.g. "daysInSystem:42_score:780_..."). Reverts if the address is not registered.

Parameters
NameTypeDescription
borroweraddressAddress of the borrower whose profile to fetch.
Returns
string — Packed string: "daysInSystem:<N>_<creditReport>".
What to expect
Reverts if borrower is not registered in saturncredit.
Example
const profile = await readContract("saturnmarket", "getBorrowerProfile", [borrowerAddr]);
// "daysInSystem:42_score:780_repaid:5_defaulted:0_streak:5"

getMarketEnabled()

READ
getMarketEnabled(): number

Returns 1 if the P2P market is open for new requests and quotes, 0 if an admin has paused it. Check this before rendering the post-request UI.

Returns
number — 1 = enabled, 0 = disabled.
Example
const enabled = await readContract("saturnmarket", "getMarketEnabled", []);

getTotalOpenRequests()

READ
getTotalOpenRequests(): number

Live count of requests currently in status 1 (open). Use for marketplace summary stats.

Returns
number — Number of open loan requests.
Example
const open = await readContract("saturnmarket", "getTotalOpenRequests", []);

getTotalMatchedLoans()

READ
getTotalMatchedLoans(): number

Cumulative count of loan requests that have been accepted (i.e. loans opened) through the marketplace since deployment.

Returns
number — Total number of matched / accepted loans.
Example
const matched = await readContract("saturnmarket", "getTotalMatchedLoans", []);

getNextRequestId()

READ
getNextRequestId(): number

Returns the ID that will be assigned to the next loan request. Subtract 1 to get the most recently created request ID. Use to paginate all-time listings.

Returns
number — Next request ID (starts at 1; increments by 1 for each postLoanRequest).
Example
const nextId = await readContract("saturnmarket", "getNextRequestId", []);
// Iterate requestIds 1 .. nextId-1 to enumerate all requests

getNextQuoteId()

READ
getNextQuoteId(): number

Returns the ID that will be assigned to the next submitted quote. Use alongside getNextRequestId to enumerate all market activity.

Returns
number — Next quote ID (starts at 1; increments by 1 for each submitQuote).
Example
const nextQId = await readContract("saturnmarket", "getNextQuoteId", []);

getRequestSummary()

READ
getRequestSummary(reqId: number): string

One-call summary of a request's most-needed fields. Format: "token:<sym>_loanDex:<n>_amount:<n>_colType:<n>_status:<n>_quotes:<n>_maxRate:<n>". Use for marketplace listing cards where a single round-trip is preferable to seven.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
string — Packed summary string.
Example
const summary = await readContract("saturnmarket", "getRequestSummary", [reqId]);
// "token:TAZ_loanDex:2_amount:500000000_colType:2_status:1_quotes:3_maxRate:2000"

getRequestBorrower()

READ
getRequestBorrower(reqId: number): address

Returns the borrower address that posted this loan request.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
address — Borrower's wallet address.

getRequestLoanToken()

READ
getRequestLoanToken(reqId: number): string

Returns the token symbol the borrower wants to borrow (TAZ in v1.0).

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
string — Token symbol, e.g. "TAZ".

getRequestLoanDexVersion()

READ
getRequestLoanDexVersion(reqId: number): number

Returns which DEX (1 = V3, 2 = V4) is used to price the loan token against RA.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — 1 = V3, 2 = V4.

getRequestLoanAmount()

READ
getRequestLoanAmount(reqId: number): number

Returns the raw-unit amount of the loan token the borrower is requesting.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Requested loan amount in raw token units.

getRequestCollateralType()

READ
getRequestCollateralType(reqId: number): number

Returns the borrower's offered collateral type. In v1.0 this will be 2 (v4 pool) or 3 (v3 LP NFT) — type 1 is never stored since it is rejected at post time.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — 2 = v4 LP pool, 3 = v3 LP NFT.

getRequestCollateralToken()

READ
getRequestCollateralToken(reqId: number): string

Returns the collateral token symbol (type 1 only). Empty string for types 2 and 3.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
string — Token symbol for type-1 collateral; empty otherwise.

getRequestCollateralAmount()

READ
getRequestCollateralAmount(reqId: number): number

Returns the collateral token amount (type 1 only). 0 for types 2 and 3.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Raw-unit collateral token amount; 0 for LP collateral types.

getRequestCollateralDexVersion()

READ
getRequestCollateralDexVersion(reqId: number): number

Returns the DEX version used to price the type-1 collateral token. 0 for types 2 and 3.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — 1 = V3, 2 = V4, or 0 for LP collateral types.

getRequestCollateralPoolId()

READ
getRequestCollateralPoolId(reqId: number): number

Returns the v4 pool ID offered as collateral (type 2). 0 for types 1 and 3.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — v4 pool ID, or 0 if not a pool-backed request.

getRequestCollateralNftId()

READ
getRequestCollateralNftId(reqId: number): number

Returns the v3 LP NFT ID offered as collateral (type 3). 0 for types 1 and 2.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — v3 LP NFT ID, or 0 if not an NFT-backed request.

getRequestPreferredDuration()

READ
getRequestPreferredDuration(reqId: number): number

Returns the borrower's preferred loan duration in seconds. Advisory — lenders may quote different durations.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Preferred duration in seconds.

getRequestMaxInterestRate()

READ
getRequestMaxInterestRate(reqId: number): number

Returns the maximum interest rate (basis points) the borrower is willing to accept. Use to filter out quotes above this threshold in your lender UI.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Max interest rate in basis points.

getRequestMessage()

READ
getRequestMessage(reqId: number): string

Returns the borrower's optional freeform message attached to the request.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
string — Freeform note from the borrower. May be empty.

getRequestStatus()

READ
getRequestStatus(reqId: number): number

Returns the current status of the request. 1 = open, 2 = accepted, 3 = cancelled, 4 = expired.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — 1 open | 2 accepted | 3 cancelled | 4 expired.
Example
const status = await readContract("saturnmarket", "getRequestStatus", [reqId]);
if (status === 1) { /* show quote button */ }

getRequestCreatedAt()

READ
getRequestCreatedAt(reqId: number): number

Returns the Unix timestamp (seconds) when the request was posted.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Unix timestamp of creation.

getRequestExpiresAt()

READ
getRequestExpiresAt(reqId: number): number

Returns the Unix timestamp (seconds) when the request expires. Use for countdown timers in the marketplace UI.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Unix expiry timestamp.
Example
const expiresAt = await readContract("saturnmarket", "getRequestExpiresAt", [reqId]);
const secsLeft = expiresAt - Math.floor(Date.now() / 1000);

getRequestQuoteCount()

READ
getRequestQuoteCount(reqId: number): number

Returns how many quotes have been submitted for this request. Combine with getRequestQuoteAtIndex to iterate them.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Total quote count for this request (includes withdrawn quotes).
Example
const count = await readContract("saturnmarket", "getRequestQuoteCount", [reqId]);
for (let i = 0; i < count; i++) {
  const qId = await readContract("saturnmarket", "getRequestQuoteAtIndex", [reqId, i]);
}

getRequestAcceptedQuoteId()

READ
getRequestAcceptedQuoteId(reqId: number): number

Returns the ID of the quote that was accepted to open the loan. Only meaningful when request status is 2 (accepted).

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Accepted quote ID, or 0 if no quote has been accepted yet.

getRequestLoanId()

READ
getRequestLoanId(reqId: number): number

Returns the saturnloans loanId created when this request was accepted. Use to link from the marketplace entry to the live loan in saturnloans. Only set after status becomes 2.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — Loan ID in saturnloans, or 0 if not yet accepted.

getRequestQuoteAtIndex()

READ
getRequestQuoteAtIndex(requestId: number, index: number): number

Returns the quoteId at a given index within the quote list for a specific request. Iterate from 0 to getRequestQuoteCount(requestId)-1 to enumerate all quotes on a request.

Parameters
NameTypeDescription
requestIdnumberLoan request ID.
indexnumberZero-based index into the request's quote list.
Returns
number — Quote ID at the given index.
Example
const count = await readContract("saturnmarket", "getRequestQuoteCount", [reqId]);
const quoteIds = await Promise.all(
  Array.from({ length: count }, (_, i) =>
    readContract("saturnmarket", "getRequestQuoteAtIndex", [reqId, i])
  )
);

getQuoteSummary()

READ
getQuoteSummary(qId: number): string

One-call summary of a quote's most-needed fields. Format: "rate:<n>_duration:<n>_amount:<n>_colType:<n>_status:<n>". Use for the quote card in the borrower's decision UI.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
string — Packed summary string.
Example
const summary = await readContract("saturnmarket", "getQuoteSummary", [quoteId]);
// "rate:1500_duration:1209600_amount:500000000_colType:2_status:1"

getQuoteLender()

READ
getQuoteLender(qId: number): address

Returns the lender address that submitted this quote.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
address — Lender's wallet address.

getQuoteRequestId()

READ
getQuoteRequestId(qId: number): number

Returns the loan request ID that this quote is responding to.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Parent request ID.

getQuoteInterestRate()

READ
getQuoteInterestRate(qId: number): number

Returns the per-period interest rate offered by the lender, in basis points.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Interest rate in basis points.

getQuoteDuration()

READ
getQuoteDuration(qId: number): number

Returns the loan term in seconds proposed by the lender.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Loan duration in seconds.

getQuoteLoanAmount()

READ
getQuoteLoanAmount(qId: number): number

Returns the raw-unit loan amount the lender is offering (and has escrowed). This amount minus the origination fee is what the borrower actually receives.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Escrowed loan amount in raw token units.

getQuoteColType()

READ
getQuoteColType(qId: number): number

Returns the collateral type the lender is demanding. In v1.0 this will be 2 (v4 pool) or 3 (v3 LP NFT).

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — 2 = v4 LP pool, 3 = v3 LP NFT.

getQuoteColTokenSymbol()

READ
getQuoteColTokenSymbol(qId: number): string

Returns the token symbol the lender requires as collateral (type 1 only). Empty for types 2 and 3.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
string — Collateral token symbol for type-1 quotes; empty otherwise.

getQuoteColTokenAmount()

READ
getQuoteColTokenAmount(qId: number): number

Returns the collateral token amount required (type 1 only). 0 for types 2 and 3.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Required collateral amount in raw token units; 0 for LP types.

getQuoteColDexVersion()

READ
getQuoteColDexVersion(qId: number): number

Returns the DEX version used to price the type-1 collateral. 0 for types 2 and 3.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — 1 = V3, 2 = V4, or 0 for LP collateral types.

getQuoteColPoolId()

READ
getQuoteColPoolId(qId: number): number

Returns the specific v4 pool ID the lender requires as collateral (type 2). 0 for types 1 and 3.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Required v4 pool ID; 0 if not a pool-collateral quote.

getQuoteColNftId()

READ
getQuoteColNftId(qId: number): number

Returns the specific v3 LP NFT ID the lender requires as collateral (type 3). 0 for types 1 and 2.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Required v3 LP NFT ID; 0 if not an NFT-collateral quote.

getQuoteMessage()

READ
getQuoteMessage(qId: number): string

Returns the lender's optional message attached to the quote.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
string — Freeform note from the lender. May be empty.

getQuoteStatus()

READ
getQuoteStatus(qId: number): number

Returns the current status of the quote. 1 = pending, 2 = accepted, 4 = withdrawn.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — 1 pending | 2 accepted | 4 withdrawn.
Example
const status = await readContract("saturnmarket", "getQuoteStatus", [quoteId]);
if (status === 1) { /* quote is still live, borrower can accept */ }

getQuoteCreatedAt()

READ
getQuoteCreatedAt(qId: number): number

Returns the Unix timestamp (seconds) when the quote was submitted.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Unix timestamp of quote creation.

getQuoteExpiresAt()

READ
getQuoteExpiresAt(qId: number): number

Returns the Unix timestamp (seconds) when the quote expires. acceptQuote reverts after this time.

Parameters
NameTypeDescription
qIdnumberQuote ID.
Returns
number — Unix expiry timestamp.
Example
const expiresAt = await readContract("saturnmarket", "getQuoteExpiresAt", [quoteId]);
const expired = Date.now() / 1000 > expiresAt;

getUserRequestCount()

READ
getUserRequestCount(user: address): number

Returns the total number of loan requests ever posted by this address (includes cancelled and accepted ones). Use as the upper bound when iterating getUserRequestAtIndex.

Parameters
NameTypeDescription
useraddressBorrower address to look up.
Returns
number — Total request count for this user.
Example
const count = await readContract("saturnmarket", "getUserRequestCount", [userAddr]);

getUserRequestAtIndex()

READ
getUserRequestAtIndex(user: address, index: number): number

Returns the requestId at the given zero-based index in the user's personal request list. Iterate from 0 to getUserRequestCount(user)-1 for a full user history.

Parameters
NameTypeDescription
useraddressBorrower address.
indexnumberZero-based index.
Returns
number — Loan request ID at the given index.
Example
const count = await readContract("saturnmarket", "getUserRequestCount", [userAddr]);
const ids = await Promise.all(
  Array.from({ length: count }, (_, i) =>
    readContract("saturnmarket", "getUserRequestAtIndex", [userAddr, i])
  )
);

getUserQuoteCount()

READ
getUserQuoteCount(user: address): number

Returns the total number of quotes ever submitted by this lender address (includes withdrawn ones). Use as the upper bound when iterating getUserQuoteAtIndex.

Parameters
NameTypeDescription
useraddressLender address to look up.
Returns
number — Total quote count for this lender.

getUserQuoteAtIndex()

READ
getUserQuoteAtIndex(user: address, index: number): number

Returns the quoteId at the given zero-based index in the lender's personal quote list. Iterate from 0 to getUserQuoteCount(user)-1 for a full lender quote history.

Parameters
NameTypeDescription
useraddressLender address.
indexnumberZero-based index.
Returns
number — Quote ID at the given index.
Example
const count = await readContract("saturnmarket", "getUserQuoteCount", [lenderAddr]);
const qIds = await Promise.all(
  Array.from({ length: count }, (_, i) =>
    readContract("saturnmarket", "getUserQuoteAtIndex", [lenderAddr, i])
  )
);
Lending Protocol · Contract #7

SaturnDexAdapt

saturndexadapt

Unified pricing and pool-validation bridge between the Saturn Lending protocol and both DEX generations: Saturn DEX v3 (SATRN string-keyed pools, LP NFTs) and Saturn DEX v4 (saturnpools numeric pool IDs, FinancialLock). Every loan and collateral token is valued via its RA-paired pool — the "RA-anchor" model — so pricing is always grounded in the protocol's reference asset. Developers building integrations can use the public views to price tokens, value LP collateral positions, resolve pool keys, and convert amounts between token denominations without touching the DEX contracts directly.

Anchor Configuration

getAnchorToken()

READ
getAnchorToken(): string

Returns the current anchor token symbol (default: "RA"). Every pricing lookup routes through a pool that includes this token on one side. Call this before any pricing query to confirm the anchor hasn't been changed by governance.

Returns
string — Symbol of the anchor token, e.g. "RA".
What to expect
Always a non-empty string. Never reverts.
Example
const anchor = await readContract("saturndexadapt", "getAnchorToken", []);
// "RA"

anchoredPair()

READ
anchoredPair(tokenA: string, tokenB: string): number

Returns 1 if at least one of the two token symbols equals the anchor token (i.e., the pair is a valid RA-paired pool from the protocol's perspective), 0 otherwise. Use as a fast pre-check before calling pricing methods.

Parameters
NameTypeDescription
tokenAstringFirst token symbol.
tokenBstringSecond token symbol.
Returns
number — 1 if the pair involves the anchor token, 0 if not.
What to expect
Pure computation, no on-chain reads. Never reverts.
Example
const ok = await readContract("saturndexadapt", "anchoredPair", ["MKST", "RA"]);
// 1

hasAnchorPool()

READ
hasAnchorPool(tokenSymbol: string, dexVersion: number): number

Returns 1 if a live RA-paired pool exists for the given token on the specified DEX version (1 = v3/SATRN, 2 = v4/saturnpools), 0 if not. A token whose value is 1 can be priced via priceInAnchor. Always call this before depositing collateral to confirm the token is priceable.

Parameters
NameTypeDescription
tokenSymbolstringToken to check for an RA-paired pool.
dexVersionnumber1 for Saturn DEX v3, 2 for Saturn DEX v4.
Returns
number — 1 if an RA pool exists, 0 if the token is not priceable.
What to expect
Returns 1 unconditionally if tokenSymbol == anchorToken. No revert.
Example
const priceable = await readContract("saturndexadapt", "hasAnchorPool", ["MKST", 2]);
// 1 — safe to use as v4 collateral

expectAnchorPool()

READ
expectAnchorPool(tokenSymbol: string, dexVersion: number)

Reverts with a descriptive message if no RA-paired pool exists for the token on the given DEX version. Use in scripts or simulation to gate-check a token before building a transaction.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to validate.
dexVersionnumber1 for v3, 2 for v4.
What to expect
Reverts: "No RA pricing pool for <symbol> on requested DEX" if hasAnchorPool returns 0.
Example
// Will throw if NEWTOKEN has no RA pool on v4
await readContract("saturndexadapt", "expectAnchorPool", ["NEWTOKEN", 2]);

Amount Scaling

warmScale()

WRITE
warmScale(tokenSymbol: string)

Precomputes and stores the scale factor for a token in the v4 pool registry (saturnpools.computeAndStoreScaleFactor). Call this once for any new token before the first scaleUp call in a transaction batch to avoid extra compute cost at pricing time.

Parameters
NameTypeDescription
tokenSymbolstringToken whose scale factor should be cached.
What to expect
Delegates entirely to saturnpools. Reverts if the token doesn't exist on-chain.
Example
const tx = sb.begin()
  .allowGas(from)
  .callContract("saturndexadapt", "warmScale", ["NEWTOKEN"])
  .spendGas(from)
  .endScript();

scaleUp()

READ
scaleUp(rawAmount: number, tokenSymbol: string): number

Converts a raw token amount (native decimals) to the protocol's internal 8-decimal scaled representation. All pricing views expect scaled inputs; call scaleUp on any user-supplied raw amount before passing it to priceInAnchor or priceInToken.

Parameters
NameTypeDescription
rawAmountnumberAmount in the token's native on-chain decimals.
tokenSymbolstringToken symbol to determine the scale factor.
Returns
number — Equivalent amount scaled to 8 decimal places.
What to expect
Internally calls computeAndStoreScaleFactor to ensure the factor is warm. Reverts if token unknown.
Example
// Convert 10 RA (9-dec native) to scaled
const scaled = await readContract("saturndexadapt", "scaleUp", [10_000_000_000, "RA"]);
// e.g. 1_000_000_000 (8-dec)

scaleDown()

READ
scaleDown(scaledAmount: number, tokenSymbol: string): number

Converts a scaled (8-decimal) amount back to the token's native raw decimals. Use to convert a pricing result into a value you can display to users or pass to a transfer call.

Parameters
NameTypeDescription
scaledAmountnumberAmount in 8-decimal scaled units.
tokenSymbolstringToken symbol to determine the scale factor.
Returns
number — Equivalent amount in the token's native decimals.
What to expect
Truncates on division. Never reverts on valid input.
Example
const raw = await readContract("saturndexadapt", "scaleDown", [1_000_000_000, "RA"]);
// 10_000_000_000 (9-dec RA)

V4 Pool Reads

v4PoolActive()

READ
v4PoolActive(poolId: number): number

Returns 1 if the v4 pool is active (live and accepting swaps), 0 otherwise. Check before submitting any collateral or pricing call against a specific pool ID.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID in the v4 PoolRegistry.
Returns
number — 1 if active, 0 if inactive or non-existent.
Example
const active = await readContract("saturndexadapt", "v4PoolActive", [42]);

v4PoolProvider()

READ
v4PoolProvider(poolId: number): address

Returns the address of the liquidity provider for a v4 pool. Useful to verify ownership before pledging or locking a pool as collateral.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID in the v4 PoolRegistry.
Returns
address — Wallet address of the pool's single provider.
Example
const provider = await readContract("saturndexadapt", "v4PoolProvider", [42]);

v4PoolLocked()

READ
v4PoolLocked(poolId: number): number

Returns 1 if the v4 pool is currently locked by any financial product (loan, bond, rental, etc.), 0 if free. A locked pool cannot have liquidity added or removed, but swaps and provider fee accrual continue normally.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID.
Returns
number — 1 if locked, 0 if free.
What to expect
Based on saturnpools.getPoolFinancialLockCount — non-zero lock count means locked.
Example
const locked = await readContract("saturndexadapt", "v4PoolLocked", [42]);
if (locked) { /* pool already used as collateral */ }

v4PoolTokenA()

READ
v4PoolTokenA(poolId: number): string

Returns the symbol of token A for the given v4 pool.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID.
Returns
string — Symbol of the pool's token A.
Example
const tokenA = await readContract("saturndexadapt", "v4PoolTokenA", [42]);

v4PoolTokenB()

READ
v4PoolTokenB(poolId: number): string

Returns the symbol of token B for the given v4 pool.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID.
Returns
string — Symbol of the pool's token B.
Example
const tokenB = await readContract("saturndexadapt", "v4PoolTokenB", [42]);

v4PoolReserveA()

READ
v4PoolReserveA(poolId: number): number

Returns the current reserve of token A in the specified v4 pool, in scaled (8-decimal) units.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID.
Returns
number — Scaled token A reserve amount.
Example
const resA = await readContract("saturndexadapt", "v4PoolReserveA", [42]);

v4PoolReserveB()

READ
v4PoolReserveB(poolId: number): number

Returns the current reserve of token B in the specified v4 pool, in scaled (8-decimal) units.

Parameters
NameTypeDescription
poolIdnumberNumeric pool ID.
Returns
number — Scaled token B reserve amount.
Example
const resB = await readContract("saturndexadapt", "v4PoolReserveB", [42]);

V4 Pool Lock Proxies

v4LockPool()

WRITE
v4LockPool(from: address, poolId: number)

Increments the FinancialLock count on a v4 pool, preventing the provider from adding or removing liquidity for the duration of the lock. This is the mechanism behind LP-collateral loans: the pool stays in place while the loan is open. Can only be called by the saturnvault contract — not directly by end users. Documented here as a developer integration reference.

Parameters
NameTypeDescription
fromaddressPool provider address; must be the pool's registered provider.
poolIdnumberID of the v4 pool to lock.
What to expect
Reverts: "Only vault" if not called from saturnvault. "Pool not active" if the pool is inactive. "Only pool provider" if from != pool provider. "Pool has active fee redirect" if a fee redirect is set. "Pool already under another financial product" if lockCount > 0.
Example
// Not called directly — invoked by saturnvault during collateral deposit.
// Shown for integration reference only.

v4UnlockPool()

WRITE
v4UnlockPool(poolId: number)

Decrements the FinancialLock count on a v4 pool, re-enabling liquidity management once the loan is repaid or liquidated. Can only be called by saturnvault — not directly by end users. Documented as a developer integration reference.

Parameters
NameTypeDescription
poolIdnumberID of the v4 pool to unlock.
What to expect
Reverts: "Only vault" if not called from saturnvault.
Example
// Not called directly — invoked by saturnvault during collateral release.
// Shown for integration reference only.

V3 Pool Reads

v3BuildPairKey()

READ
v3BuildPairKey(tokenA: string, tokenB: string): string

Constructs the ordered pair key string "TOKENA_TOKENB" used as the storage key in Saturn DEX v3 pools. Utility for building the keys needed by v3PoolLiquidity and v3PoolReserve.

Parameters
NameTypeDescription
tokenAstringFirst token symbol.
tokenBstringSecond token symbol.
Returns
string — Pair key string, e.g. "MKST_RA".
Example
const key = await readContract("saturndexadapt", "v3BuildPairKey", ["MKST", "RA"]);
// "MKST_RA"

v3PoolLiquidity()

READ
v3PoolLiquidity(pairKey: string): number

Returns the total liquidity units for a v3 pool identified by its ordered pair key. A non-zero result confirms the pool exists and has liquidity.

Parameters
NameTypeDescription
pairKeystringOrdered pair key, e.g. "MKST_RA".
Returns
number — Total liquidity units in the v3 pool; 0 if pool does not exist.
Example
const liq = await readContract("saturndexadapt", "v3PoolLiquidity", ["MKST_RA"]);

v3PoolReserve()

READ
v3PoolReserve(pairKey: string, tokenSymbol: string): number

Returns the reserve of a specific token inside a v3 pool. Both pairKey and tokenSymbol must be consistent — tokenSymbol must be one of the two tokens in the pair.

Parameters
NameTypeDescription
pairKeystringOrdered pair key, e.g. "MKST_RA".
tokenSymbolstringThe token whose reserve to read; must be in the pair.
Returns
number — Token reserve in the v3 pool (scaled).
Example
const raReserve = await readContract("saturndexadapt", "v3PoolReserve", ["MKST_RA", "RA"]);

v3ResolvePairKey()

READ
v3ResolvePairKey(tokenA: string, tokenB: string): string

Discovers the canonical direction of a v3 pool for a token pair by checking both orderings (TOKENA_TOKENB and TOKENB_TOKENA) and returning whichever has positive liquidity. Returns an empty string if no v3 pool exists for the pair. Use this when you have a token pair but don't know the canonical key ordering.

Parameters
NameTypeDescription
tokenAstringFirst token symbol.
tokenBstringSecond token symbol.
Returns
string — Canonical pair key with liquidity, or "" if no v3 pool exists.
What to expect
Returns "" if neither ordering has positive liquidity.
Example
const key = await readContract("saturndexadapt", "v3ResolvePairKey", ["RA", "MKST"]);
// "MKST_RA" or "RA_MKST" depending on the canonical direction

V3 LP NFT Metadata

v3NftPairKey()

READ
v3NftPairKey(nftId: number): string

Returns the v3 pool pair key associated with the given LP NFT ID. Use to identify which pool the NFT represents before valuing it as collateral.

Parameters
NameTypeDescription
nftIdnumberOn-chain series ID of the v3 LP NFT.
Returns
string — Pair key string, e.g. "MKST_RA".
Example
const pairKey = await readContract("saturndexadapt", "v3NftPairKey", [7]);

v3NftTokenA()

READ
v3NftTokenA(nftId: number): string

Returns the symbol of token A recorded in the v3 LP NFT's metadata.

Parameters
NameTypeDescription
nftIdnumberOn-chain series ID of the v3 LP NFT.
Returns
string — Symbol of token A for this LP position.
Example
const tokenA = await readContract("saturndexadapt", "v3NftTokenA", [7]);

v3NftTokenB()

READ
v3NftTokenB(nftId: number): string

Returns the symbol of token B recorded in the v3 LP NFT's metadata.

Parameters
NameTypeDescription
nftIdnumberOn-chain series ID of the v3 LP NFT.
Returns
string — Symbol of token B for this LP position.
Example
const tokenB = await readContract("saturndexadapt", "v3NftTokenB", [7]);

v3NftLiquidity()

READ
v3NftLiquidity(nftId: number): number

Returns the liquidity units recorded for the given v3 LP NFT. Divide by the pool's total liquidity (v3PoolLiquidity) to get the NFT's pro-rata share of pool reserves.

Parameters
NameTypeDescription
nftIdnumberOn-chain series ID of the v3 LP NFT.
Returns
number — Liquidity units attributed to this NFT position.
Example
const nftLiq = await readContract("saturndexadapt", "v3NftLiquidity", [7]);
const totalLiq = await readContract("saturndexadapt", "v3PoolLiquidity", ["MKST_RA"]);
const share = nftLiq / totalLiq;

RA-Anchor Pricing

priceInAnchor()

READ
priceInAnchor(tokenSymbol: string, scaledAmount: number, dexVersion: number): number

Returns the scaled anchor-token (RA) equivalent of a given scaled amount of tokenSymbol, using the DEX reserves of the RA-paired pool on the specified DEX version. This is the central pricing primitive: the lending protocol uses it for every loan and collateral valuation. Pass scaled amounts (use scaleUp first). Returns the anchor amount in 8-decimal scaled units.

Parameters
NameTypeDescription
tokenSymbolstringToken to price in anchor units.
scaledAmountnumberAmount of the token in 8-decimal scaled units.
dexVersionnumber1 for v3/SATRN, 2 for v4/saturnpools.
Returns
number — Scaled RA-equivalent value.
What to expect
Reverts: "Amount must be > 0" if scaledAmount == 0. "No v3/v4 RA pool for <token>" if no pricing pool exists. Returns scaledAmount unchanged if tokenSymbol == anchorToken.
Example
// How much RA is 50 MKST worth on v4?
const scaledMkst = await readContract("saturndexadapt", "scaleUp", [50_000_000_000, "MKST"]);
const raValue = await readContract("saturndexadapt", "priceInAnchor", [scaledMkst, "MKST", 2]);
// raValue is in 8-dec scaled RA units

priceInToken()

READ
priceInToken(fromToken: string, scaledAmount: number, fromDex: number, toToken: string, toDex: number): number

Cross-token pricing: converts a scaled amount of fromToken (priced on fromDex) into the equivalent scaled value denominated in toToken (priced on toDex), routing through RA as an intermediate anchor. Use this for cross-DEX or cross-token collateral comparisons (e.g., compare v3 LP collateral to a v4 loan denomination).

Parameters
NameTypeDescription
fromTokenstringSource token symbol.
scaledAmountnumberAmount of the source token in 8-decimal scaled units.
fromDexnumberDEX version for the source token's RA pool (1 = v3, 2 = v4).
toTokenstringDestination token symbol.
toDexnumberDEX version for the destination token's RA pool (1 = v3, 2 = v4).
Returns
number — Scaled equivalent in toToken units.
What to expect
Reverts if either token lacks an RA-paired pool on its specified DEX. Reverts if scaledAmount == 0.
Example
// How many USDC (v4) is 100 MKST (v4) worth?
const scaledMkst = await readContract("saturndexadapt", "scaleUp", [100_000_000_000, "MKST"]);
const usdcEquiv = await readContract("saturndexadapt", "priceInToken",
  ["MKST", scaledMkst, 2, "USDC", 2]);

LP Collateral Valuation

v4PoolValueInBase()

READ
v4PoolValueInBase(poolId: number, baseToken: string, baseDex: number): number

Values the entire reserve of a v4 LP pool in terms of a base token, routed through RA. Sums priceInToken(tokenA, resA, 2, baseToken, baseDex) + priceInToken(tokenB, resB, 2, baseToken, baseDex). This is the headline collateral value used by saturnvault when evaluating a v4 LP deposit.

Parameters
NameTypeDescription
poolIdnumberNumeric v4 pool ID.
baseTokenstringToken to denominate the result in (e.g. "RA" or the loan token).
baseDexnumberDEX version for the base token's RA pool (1 = v3, 2 = v4).
Returns
number — Total pool value in scaled baseToken units.
What to expect
Reverts: "v4 pool not RA-paired" if neither token in the pool is the anchor token.
Example
// Full collateral value of v4 pool #42 denominated in RA
const value = await readContract("saturndexadapt", "v4PoolValueInBase", [42, "RA", 2]);
// Scaled 8-dec RA units

v3LpNftValueInBase()

READ
v3LpNftValueInBase(nftId: number, baseToken: string, baseDex: number): number

Values a v3 LP NFT position in terms of a base token. Computes the NFT's pro-rata share of pool reserves (nftLiquidity / totalLiquidity) then prices both token portions through RA into baseToken. This is the collateral value used by saturnvault for v3 LP NFT deposits.

Parameters
NameTypeDescription
nftIdnumberOn-chain series ID of the v3 LP NFT.
baseTokenstringToken to denominate the result in (e.g. "RA" or the loan token).
baseDexnumberDEX version for the base token's RA pool (1 = v3, 2 = v4).
Returns
number — NFT position value in scaled baseToken units.
What to expect
Reverts: "v3 LP NFT not RA-paired" if the NFT's pair doesn't include the anchor. "v3 pool has no liquidity" or "v3 NFT has no liquidity" if either is zero.
Example
// Value of v3 LP NFT #7 denominated in RA (v4 RA pool)
const nftVal = await readContract("saturndexadapt", "v3LpNftValueInBase", [7, "RA", 2]);
Lending Protocol · Contract #8

SaturnTaz

saturntaz

Awards TAZ governance tokens on every fully-repaid loan, split 30% to the borrower, 30% to the lender, and 40% to the lender's active RA pledgers (v1.1 anti-abuse formula). RA owners pledge to specific lenders to curate the lender set and earn a share of their loan rewards; pledges are fixed 30-day windows and can be custodial (v3, RA transferred in) or non-custodial (v4, RA locked in saturnholders). The reward formula is fully bounded — every input has a cap and a floor — so emission is predictable. An annual deposit cap gates TAZ inflows into the treasury regardless of how many authorized depositors exist.

Treasury — Deposits & Balance

depositTaz()

WRITE
depositTaz(from: address, amount: number)

Deposits TAZ from an authorized treasury wallet into the contract's reward pool. The caller must be on the authorized-depositor allowlist (added by the owner). Enforces a rolling 365-day emission cap: all authorized depositors combined cannot exceed yearlyDepositCap TAZ in a single annual window. If the current window has expired it is silently rolled forward at deposit time.

Parameters
NameTypeDescription
fromaddressAuthorized depositor address (must be witness and on the allowlist).
amountnumberRaw TAZ amount to deposit (9-decimal).
What to expect
Reverts: "Wallet not registered as treasury depositor" if from is not on the allowlist. "Insufficient TAZ balance" if from lacks enough TAZ. "Yearly deposit cap exceeded" if amount would breach the annual cap.
Example
const tx = sb.begin()
  .allowGas(treasury)
  .callContract("saturntaz", "depositTaz", [treasury, 10_000_000_000_000n])
  .spendGas(treasury)
  .endScript();

getTazBalance()

READ
getTazBalance(): number

Returns the current TAZ balance held by this contract — the reward treasury available for distribution. Monitor this to alert when the treasury needs topping up.

Returns
number — Raw TAZ balance (9-decimal) held in the contract.
Example
const bal = await readContract("saturntaz", "getTazBalance", []);

getTazSymbol()

READ
getTazSymbol(): string

Returns the TAZ token symbol used by this contract (default: "TAZ"). Check this if you need to query TAZ token metadata or display the token symbol dynamically.

Returns
string — Current TAZ token symbol.
Example
const sym = await readContract("saturntaz", "getTazSymbol", []);

getAuthorizedDepositor()

READ
getAuthorizedDepositor(wallet: address): number

Returns 1 if the wallet is authorized to deposit TAZ into the treasury, 0 if not. Use to check allowlist status before attempting a depositTaz call.

Parameters
NameTypeDescription
walletaddressAddress to check.
Returns
number — 1 if authorized, 0 if not.
Example
const ok = await readContract("saturntaz", "getAuthorizedDepositor", [walletAddr]);

Annual Deposit Cap Views

getYearlyDepositCap()

READ
getYearlyDepositCap(): number

Returns the maximum raw TAZ that all authorized depositors may collectively deposit in a single 365-day window. Default is 40,000 TAZ (40,000 × 10^9 raw).

Returns
number — Yearly TAZ deposit cap in raw 9-decimal units.
Example
const cap = await readContract("saturntaz", "getYearlyDepositCap", []);

getYearlyDurationSeconds()

READ
getYearlyDurationSeconds(): number

Returns the length of one deposit-cap window in seconds (default: 31,536,000 = 365 days).

Returns
number — Window duration in seconds.
Example
const dur = await readContract("saturntaz", "getYearlyDurationSeconds", []);

getYearStartTime()

READ
getYearStartTime(): number

Returns the unix timestamp at which the current annual deposit window started. Pair with getYearlyDurationSeconds to compute when the window resets.

Returns
number — Unix timestamp (seconds) of the current window start.
Example
const start = await readContract("saturntaz", "getYearStartTime", []);
const end = start + await readContract("saturntaz", "getYearlyDurationSeconds", []);

getCurrentYearDeposited()

READ
getCurrentYearDeposited(): number

Returns how much TAZ has been deposited by all authorized depositors in the current annual window so far.

Returns
number — Raw TAZ deposited in the current window.
Example
const used = await readContract("saturntaz", "getCurrentYearDeposited", []);

getYearlyRemaining()

READ
getYearlyRemaining(): number

Convenience view: returns the remaining TAZ deposit budget in the current annual window. If the window has expired, returns the full cap (a fresh window starts on the next deposit). Use this to show treasury operators how much headroom is left.

Returns
number — Remaining raw TAZ that may still be deposited in this window.
What to expect
Returns yearlyDepositCap if the window has expired. Returns 0 if the cap is already exhausted. Never reverts.
Example
const remaining = await readContract("saturntaz", "getYearlyRemaining", []);
// UI: "You may deposit up to X TAZ this year"

Pledge Configuration Views

getPledgeDurationSeconds()

READ
getPledgeDurationSeconds(): number

Returns the fixed pledge window length in seconds (default: 2,592,000 = 30 days). A pledge cannot be withdrawn until this many seconds have elapsed since it was created.

Returns
number — Pledge lock duration in seconds.
Example
const secs = await readContract("saturntaz", "getPledgeDurationSeconds", []);
const days = secs / 86400; // 30

getMaxPledgersPerLender()

READ
getMaxPledgersPerLender(): number

Returns the maximum number of active pledgers a single lender may have at once. Once reached, new pledgers are blocked until an existing pledge expires and its slot is freed.

Returns
number — Hard cap on simultaneous pledgers per lender (default: 30).
Example
const cap = await readContract("saturntaz", "getMaxPledgersPerLender", []);

Reward Configuration Views

getRewardParam()

READ
getRewardParam(key: string): number

Returns a single reward formula parameter by key. Valid keys: "baseReward" (max TAZ raw per loan when all factors at cap), "loanCapRA" (SCALED RA saturation cap), "durationCapDays" (days saturation cap), "pledgeCapRA" (SCALED RA pledge saturation cap), "minLoanRA" (floor — loans below this earn zero), "minDurationDays" (floor — loans shorter than this earn zero), "minPledgeRA" (floor — lenders with less eligible pledge earn zero), "maxRewardPerLoan" (hard TAZ ceiling per loan), "borrowerBps" (basis points for borrower), "lenderBps" (basis points for lender), "pledgerBps" (basis points for pledger pool).

Parameters
NameTypeDescription
keystringParameter key (see description for valid keys).
Returns
number — Current value of the requested reward parameter.
Example
const base = await readContract("saturntaz", "getRewardParam", ["baseReward"]);
// e.g. 30_000_000_000 (30 TAZ raw)

const borrowerBps = await readContract("saturntaz", "getRewardParam", ["borrowerBps"]);
// 3000  (30%)

RA Pledging — V3 (Custodial)

pledgeV3()

WRITE
pledgeV3(from: address, lender: address, amount: number)

Custodial RA pledge: transfers amount of RA from the caller into this contract's custody and registers the pledge against lender for a fixed 30-day window. During the window the pledger is eligible to receive a share of TAZ from every loan the lender fully repays. Cannot pledge to yourself. Any previous pledge to the same lender must be withdrawn (unpledgeV3) before re-pledging.

Parameters
NameTypeDescription
fromaddressPledger address (must be witness).
lenderaddressTarget lender address to pledge RA to.
amountnumberRaw RA amount to pledge (native RA decimals, typically 9-dec).
What to expect
Reverts: "Cannot pledge to yourself". "Existing pledge to this lender is still active - wait until expiry" if expiry > now. "Existing v3/v4 pledge must be withdrawn before re-pledging" if prior pledge balance remains. "Pledge amount rounds to zero" if amount is too small to survive scaleUp. "Lender pledger cap reached" if lender already has maxPledgersPerLender active pledgers.
Example
const tx = sb.begin()
  .allowGas(pledger)
  .callContract("saturntaz", "pledgeV3", [pledger, lenderAddr, 5_000_000_000n])
  .spendGas(pledger)
  .endScript();

unpledgeV3()

WRITE
unpledgeV3(from: address, lender: address)

Withdraws a custodial v3 RA pledge after the 30-day window expires. Returns the originally-pledged RA to the caller (amount is the stored scaled amount converted back to raw). Will revert if the pledge has not yet expired — pledges are time-locked.

Parameters
NameTypeDescription
fromaddressPledger address (must be witness).
lenderaddressLender address the pledge was registered against.
What to expect
Reverts: "No v3 pledge to this lender" if no pledge exists. "Pledge still active - wait until expiry" if now < pledgeExpiry.
Example
const tx = sb.begin()
  .allowGas(pledger)
  .callContract("saturntaz", "unpledgeV3", [pledger, lenderAddr])
  .spendGas(pledger)
  .endScript();

RA Pledging — V4 (Non-Custodial)

pledgeV4()

WRITE
pledgeV4(from: address, lender: address, amount: number)

Non-custodial RA pledge: locks amount of RA in saturnholders via lockForPledge (the RA stays in the staking contract but cannot be unstaked while the pledge is active). Registers the pledge against lender for a fixed 30-day window. Requires that saturntaz is on saturnholders' lockForPledge allowlist. Cannot pledge to yourself; prior pledge must be cleared first.

Parameters
NameTypeDescription
fromaddressPledger address (must be witness and have sufficient unlocked RA stake in saturnholders).
lenderaddressTarget lender address to pledge to.
amountnumberRaw RA amount to lock (native RA decimals).
What to expect
Reverts: "Cannot pledge to yourself". "Existing pledge to this lender is still active" if expiry > now. "Existing v3/v4 pledge must be withdrawn before re-pledging" if prior balance remains. saturnholders.lockForPledge reverts if from lacks sufficient unlocked stake. "Lender pledger cap reached" if the lender's pledger list is full.
Example
const tx = sb.begin()
  .allowGas(pledger)
  .callContract("saturntaz", "pledgeV4", [pledger, lenderAddr, 5_000_000_000n])
  .spendGas(pledger)
  .endScript();

unpledgeV4()

WRITE
unpledgeV4(from: address, lender: address)

Releases a non-custodial v4 RA pledge after the 30-day window expires. Calls saturnholders.unlockPledge to restore the RA to its normal unlocked-stake state. Will revert if the pledge has not yet expired.

Parameters
NameTypeDescription
fromaddressPledger address (must be witness).
lenderaddressLender address the pledge was registered against.
What to expect
Reverts: "No v4 pledge to this lender" if no pledge exists. "Pledge still active - wait until expiry" if now < pledgeExpiry.
Example
const tx = sb.begin()
  .allowGas(pledger)
  .callContract("saturntaz", "unpledgeV4", [pledger, lenderAddr])
  .spendGas(pledger)
  .endScript();

TAZ Reward Claims

claimPledgeRewards()

WRITE
claimPledgeRewards(from: address)

Transfers all accumulated TAZ rewards to the pledger. Pledger rewards accrue in pledgerClaimable[from] each time a lender's loan is fully repaid; call this to collect them. Borrower and lender rewards are paid out automatically at repayment time and do not require a separate claim.

Parameters
NameTypeDescription
fromaddressPledger address claiming accumulated TAZ (must be witness).
What to expect
Reverts: "Nothing to claim" if pledgerClaimable[from] == 0. "Treasury short - try again later or contact admin" if the contract's TAZ balance is insufficient (should be rare if the treasury is properly funded).
Example
const tx = sb.begin()
  .allowGas(pledger)
  .callContract("saturntaz", "claimPledgeRewards", [pledger])
  .spendGas(pledger)
  .endScript();

getPledgerClaimable()

READ
getPledgerClaimable(pledger: address): number

Returns the accumulated unclaimed TAZ balance owed to a pledger. Use this to show a "Claimable rewards" figure in your UI before the pledger submits a claimPledgeRewards transaction.

Parameters
NameTypeDescription
pledgeraddressPledger address to check.
Returns
number — Raw TAZ (9-decimal) ready to claim.
Example
const pending = await readContract("saturntaz", "getPledgerClaimable", [pledgerAddr]);
// Show "Claimable: X TAZ" in the UI

Reward Preview

previewLoanReward()

READ
previewLoanReward(loanValueInRA: number, durationDays: number, lender: address): number

Simulates the total TAZ reward a loan would generate (before the 30/30/40 split) given the loan parameters and the lender's current active pledge. All anti-abuse formula caps and floors are applied. Returns 0 if any floor is not met. Use this in a loan quoting UI to show borrowers and lenders how much TAZ they stand to earn.

Parameters
NameTypeDescription
loanValueInRAnumberLoan value in scaled (8-dec) RA units (as returned by saturndexadapt.priceInAnchor).
durationDaysnumberLoan duration in days.
lenderaddressLender address whose current active pledge will be used in the formula.
Returns
number — Total raw TAZ reward (9-decimal) that would be distributed on full repayment.
What to expect
Returns 0 if loanValueInRA < minLoanRA, durationDays < minDurationDays, or eligible pledge < minPledgeRA. Never reverts.
Example
// Preview reward for a 30-day loan worth 50 RA
const loanRA = await readContract("saturndexadapt", "scaleUp", [50_000_000_000, "RA"]);
const reward = await readContract("saturntaz", "previewLoanReward",
  [loanRA, 30, lenderAddr]);
const borrowerShare = reward * 0.30;
const lenderShare   = reward * 0.30;
const pledgerShare  = reward * 0.40;

Pledge State Views

getPledgeScaledV3()

READ
getPledgeScaledV3(pledger: address, lender: address): number

Returns the scaled (8-decimal) RA amount of a pledger's active v3 custodial pledge to a specific lender. Returns 0 if no pledge exists.

Parameters
NameTypeDescription
pledgeraddressPledger address.
lenderaddressLender address.
Returns
number — Scaled RA amount (8-dec) of the v3 pledge; 0 if none.
Example
const scaled = await readContract("saturntaz", "getPledgeScaledV3", [pledgerAddr, lenderAddr]);

getPledgeScaledV4()

READ
getPledgeScaledV4(pledger: address, lender: address): number

Returns the scaled (8-decimal) RA amount of a pledger's active v4 non-custodial pledge to a specific lender. Returns 0 if no pledge exists.

Parameters
NameTypeDescription
pledgeraddressPledger address.
lenderaddressLender address.
Returns
number — Scaled RA amount (8-dec) of the v4 pledge; 0 if none.
Example
const scaled = await readContract("saturntaz", "getPledgeScaledV4", [pledgerAddr, lenderAddr]);

getPledgeExpiry()

READ
getPledgeExpiry(pledger: address, lender: address): number

Returns the unix timestamp at which the pledge (v3 or v4) expires and becomes withdrawable. Returns 0 if no pledge has been made.

Parameters
NameTypeDescription
pledgeraddressPledger address.
lenderaddressLender address.
Returns
number — Unix expiry timestamp (seconds); 0 if no pledge.
Example
const expiry = await readContract("saturntaz", "getPledgeExpiry", [pledgerAddr, lenderAddr]);
const canWithdraw = Date.now() / 1000 >= expiry;

getPledgeRawV3()

READ
getPledgeRawV3(pledger: address, lender: address): number

Returns the raw (native-decimal) RA amount of a v3 custodial pledge by converting the stored scaled value back via scaleDownRA. Useful for displaying the pledge amount to users in familiar token units.

Parameters
NameTypeDescription
pledgeraddressPledger address.
lenderaddressLender address.
Returns
number — Raw RA amount (native decimals) of the v3 pledge; 0 if none.
Example
const rawRA = await readContract("saturntaz", "getPledgeRawV3", [pledgerAddr, lenderAddr]);

getPledgeRawV4()

READ
getPledgeRawV4(pledger: address, lender: address): number

Returns the raw (native-decimal) RA amount of a v4 non-custodial pledge by converting the stored scaled value back via scaleDownRA.

Parameters
NameTypeDescription
pledgeraddressPledger address.
lenderaddressLender address.
Returns
number — Raw RA amount (native decimals) of the v4 pledge; 0 if none.
Example
const rawRA = await readContract("saturntaz", "getPledgeRawV4", [pledgerAddr, lenderAddr]);

getLenderPledgerCount()

READ
getLenderPledgerCount(lender: address): number

Returns the current pledger list high-water mark for a lender (the highest slot index ever used, minus reclaimed gaps). Use alongside getLenderPledgerAt to enumerate all pledgers.

Parameters
NameTypeDescription
lenderaddressLender address.
Returns
number — High-water index; iterate 1..N to enumerate pledger slots.
Example
const n = await readContract("saturntaz", "getLenderPledgerCount", [lenderAddr]);
for (let i = 1; i <= n; i++) {
  const p = await readContract("saturntaz", "getLenderPledgerAt", [lenderAddr, i]);
  if (p) console.log("pledger:", p);
}

getLenderPledgerAt()

READ
getLenderPledgerAt(lender: address, index: number): address

Returns the pledger address at a specific 1-indexed slot in a lender's pledger list. Freed slots return null/zero address. Use in combination with getLenderPledgerCount to enumerate the full pledger set.

Parameters
NameTypeDescription
lenderaddressLender address.
indexnumber1-indexed slot number (1 to getLenderPledgerCount).
Returns
address — Pledger address at this slot, or null address if the slot was freed.
Example
const pledger = await readContract("saturntaz", "getLenderPledgerAt", [lenderAddr, 1]);

getLenderEligiblePledge()

READ
getLenderEligiblePledge(lender: address): number

Returns the total scaled RA amount of all currently-eligible (non-expired) pledges backing a lender. This is exactly the value used by the reward formula at distribution time. Useful for showing lenders how much RA backing they have and for computing previewLoanReward inputs.

Parameters
NameTypeDescription
lenderaddressLender address to evaluate.
Returns
number — Total scaled (8-dec) RA across all active, non-expired pledgers.
What to expect
Returns 0 if lender has no pledgers or all pledges have expired.
Example
const eligRA = await readContract("saturntaz", "getLenderEligiblePledge", [lenderAddr]);
// Compare against getRewardParam("minPledgeRA") to confirm lender qualifies

Loan Snapshot Views

getLoanRegistered()

READ
getLoanRegistered(loanId: number): number

Returns 1 if the loan was registered with this contract at creation (via onLoanCreated hook from saturnloans), 0 if not. A loan must be registered for rewards to be distributed on repayment.

Parameters
NameTypeDescription
loanIdnumberLoan ID as assigned by saturnloans.
Returns
number — 1 if registered, 0 if not.
Example
const reg = await readContract("saturntaz", "getLoanRegistered", [loanId]);

getLoanRewardPaid()

READ
getLoanRewardPaid(loanId: number): number

Returns 1 if a TAZ reward has already been distributed for this loan, 0 if not yet paid. Rewards are idempotent — a loan can only be rewarded once.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — 1 if reward distributed, 0 if pending or ineligible.
Example
const paid = await readContract("saturntaz", "getLoanRewardPaid", [loanId]);

getLoanValueRA()

READ
getLoanValueRA(loanId: number): number

Returns the scaled (8-decimal) RA value of the loan, snapshotted at creation time by saturnloans. This is the loanValueInRA input used in the reward formula, not the live DEX price.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Scaled RA value of the loan at creation (8-dec).
Example
const raVal = await readContract("saturntaz", "getLoanValueRA", [loanId]);

getLoanDurationDays()

READ
getLoanDurationDays(loanId: number): number

Returns the duration (in days) of the loan, snapshotted at creation time. This is the durationDays input used in the reward formula.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Loan duration in days as recorded at creation.
Example
const days = await readContract("saturntaz", "getLoanDurationDays", [loanId]);

Protocol Statistics

getTotalTazPaid()

READ
getTotalTazPaid(): number

Returns the cumulative raw TAZ paid out as rewards since contract deployment (borrower + lender + pledger shares combined).

Returns
number — Total raw TAZ (9-decimal) distributed as rewards.
Example
const paid = await readContract("saturntaz", "getTotalTazPaid", []);

getTotalTazDeposited()

READ
getTotalTazDeposited(): number

Returns the cumulative raw TAZ deposited into the treasury via depositTaz since contract deployment.

Returns
number — Total raw TAZ (9-decimal) ever deposited.
Example
const deposited = await readContract("saturntaz", "getTotalTazDeposited", []);

getTotalLoansRewarded()

READ
getTotalLoansRewarded(): number

Returns the number of loans for which a TAZ reward distribution was triggered (i.e., fully-repaid loans that passed all formula floors).

Returns
number — Count of loans that have received a TAZ reward.
Example
const rewarded = await readContract("saturntaz", "getTotalLoansRewarded", []);

getTotalLoansResolved()

READ
getTotalLoansResolved(): number

Returns the total number of loans resolved (both full repayments and defaults/liquidations) that were registered with this contract.

Returns
number — Count of all resolved registered loans.
Example
const resolved = await readContract("saturntaz", "getTotalLoansResolved", []);

Saturn DEX v3

Saturn DEX v3 · Single Contract

Saturn DEX v3 (SATRN)

SATRN

The legacy Saturn DEX, still live on Phantasma mainnet as a single self-contained contract (SATRN). All DEX state — pools, reserves, LP positions — lives here. Pools are identified by string keys of the form "TOKEN0_TOKEN1". Liquidity positions are represented as non-fungible SATRN NFTs; each NFT carries the pool key and the exact liquidity-units it represents, making LP shares portable and composable — the saturnloans lending protocol accepts these SATRN LP NFTs as collateral. Swap fees total 0.4%: 0.21% to LPs, 0.09% to the admin, and 0.1% to the output-token owner (or a configurable wallet for SOUL/KCAL). Series-5 FACTORY NFT holders receive an 80% discount on all three fee slices.

Pool Info & Reserves

getTokensInDEXList()

READ
getTokensInDEXList(): string*

Yields every token symbol that has been deposited into at least one pool. Use this to enumerate all tokens traded on v3 without knowing symbol names in advance.

Returns
string* — Iterator of token symbol strings (e.g. "SOUL", "KCAL", "CROWN").
What to expect
Returns an empty iterator when no pools exist. Each symbol appears at most once.
Example
const script = sb.begin()
  .callContract("SATRN", "getTokensInDEXList", [])
  .endScript();
const res = await api.invokeRawScript("main", script);
const tokens = res.decoded; // string[]

getPairsInDEXList()

READ
getPairsInDEXList(): string*

Yields all pool keys in canonical "TOKEN0_TOKEN1" format. A pool key is the string used everywhere else in the SATRN API to identify a specific pair.

Returns
string* — Iterator of pool key strings, e.g. "SOUL_KCAL".
What to expect
Returns an empty iterator if no pools have been created. The key orientation matches the order the pool was initialised with.
Example
const script = sb.begin()
  .callContract("SATRN", "getPairsInDEXList", [])
  .endScript();
const pairs = (await api.invokeRawScript("main", script)).decoded;

getPoolsAndReservesInDEXList()

READ
getPoolsAndReservesInDEXList(): string*

Yields all reserve-lookup keys in the form "POOL_KEY_TOKEN". Each pool produces two keys: one per token side. Pass a key to getTokenPairAndReserveKeysOnListVALUE() to retrieve the scaled reserve amount.

Returns
string* — Iterator of reserve keys, e.g. "SOUL_KCAL_SOUL" and "SOUL_KCAL_KCAL".
Example
const keys = (await api.invokeRawScript("main",
  sb.begin().callContract("SATRN","getPoolsAndReservesInDEXList",[]).endScript()
)).decoded;

getAllReserves()

READ
getAllReserves(): number*

Yields the global scaled reserve for every token in the DEX, in the same order as getTokensInDEXList(). Values are scaled to 8 decimal places internally; divide by the token's scale factor to get real units.

Returns
number* — Iterator of scaled reserve totals, one per token.
Example
const reserves = (await api.invokeRawScript("main",
  sb.begin().callContract("SATRN","getAllReserves",[]).endScript()
)).decoded; // number[]

getReserveValue()

READ
getReserveValue(tokenSymbol: string): number

Returns the global scaled reserve total for a single token across all pools. Useful for quickly checking whether a token has any liquidity in the DEX.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol, e.g. "SOUL".
Returns
number — Scaled reserve (8 dp internally). Returns 0 when the token is not in any pool.
Example
const r = await readContract("SATRN", "getReserveValue", ["SOUL"]);

getTokenOnList()

READ
getTokenOnList(select: number): string

Returns the symbol at index `select` in the tokens-in-DEX list. Combine with getCountOfTokensOnList() for paginated enumeration.

Parameters
NameTypeDescription
selectnumberZero-based index.
Returns
string — Token symbol at that index.
What to expect
Reverts if select >= count.
Example
const first = await readContract("SATRN", "getTokenOnList", [0]);

getCountOfTokensOnList()

READ
getCountOfTokensOnList(): number

Returns the number of distinct token symbols that have ever been deposited into any pool. Use this as the loop bound for getTokenOnList().

Returns
number — Count of tokens tracked by the DEX.
Example
const n = await readContract("SATRN", "getCountOfTokensOnList", []);

getCountOfTokenPairsOnList()

READ
getCountOfTokenPairsOnList(): number

Returns the total number of pools that have been created. Use as the loop bound for getPairsInDEXList() pagination.

Returns
number — Number of pools in the DEX.
Example
const poolCount = await readContract("SATRN", "getCountOfTokenPairsOnList", []);

getCountOfTokenPairsAndReserveKeysOnList()

READ
getCountOfTokenPairsAndReserveKeysOnList(): number

Returns the total number of reserve keys (two per pool). Use as the loop bound for getTokenPairAndReserveKeysOnList().

Returns
number — Total reserve-key entries (2 × pool count).
Example
const n = await readContract("SATRN", "getCountOfTokenPairsAndReserveKeysOnList", []);

getTokenPairAndReserveKeysOnList()

READ
getTokenPairAndReserveKeysOnList(select: number): string

Returns the reserve key at index `select`, e.g. "SOUL_KCAL_SOUL". Feed this string directly to getTokenPairAndReserveKeysOnListVALUE() to retrieve the scaled reserve.

Parameters
NameTypeDescription
selectnumberZero-based index into the reserve-keys list.
Returns
string — Reserve key string.
What to expect
Reverts if select >= count.
Example
const key = await readContract("SATRN", "getTokenPairAndReserveKeysOnList", [0]);

getTokenPairAndReserveKeysOnListVALUE()

READ
getTokenPairAndReserveKeysOnListVALUE(pairKeyPlusTokenToViewBalance: string): number

Given a reserve key of the form "POOL_KEY_TOKEN" (e.g. "SOUL_KCAL_SOUL"), returns the scaled reserve amount for that token side. Divide by the token's scale factor (getScaleFactor) to recover real units.

Parameters
NameTypeDescription
pairKeyPlusTokenToViewBalancestringComposite key "poolKey_tokenSymbol", e.g. "SOUL_KCAL_SOUL".
Returns
number — Scaled reserve amount. Returns 0 if the key is unknown.
Example
const scaledReserve = await readContract(
  "SATRN", "getTokenPairAndReserveKeysOnListVALUE", ["SOUL_KCAL_SOUL"]
);

getAllContractBalances()

READ
getAllContractBalances(): number*

Yields the raw on-chain balance held by the SATRN contract for each tracked token, in the same order as getTokensInDEXList(). The balance may exceed scaled reserves due to accrued storage fees or donated amounts.

Returns
number* — Iterator of raw token balances held by the contract.
Example
const balances = (await api.invokeRawScript("main",
  sb.begin().callContract("SATRN","getAllContractBalances",[]).endScript()
)).decoded;

getContractTokenBalanceEach()

READ
getContractTokenBalanceEach(tokenSymbol: string): number

Returns the raw balance of a specific token held by the SATRN contract. Subtract the unscaled reserve to find skimmable surplus.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Raw token balance (native decimals) held by the contract.
Example
const bal = await readContract("SATRN", "getContractTokenBalanceEach", ["KCAL"]);

getAllTotalLiquidityPerPool()

READ
getAllTotalLiquidityPerPool(): string*

Yields one string per pool, formatted as "POOL_KEY_totalLiquidity". Convenient for a dashboard that needs to show pool depth without iterating reserves separately.

Returns
string* — Strings like "SOUL_KCAL_4831920". Total liquidity is in LP units.
Example
const poolDepths = (await api.invokeRawScript("main",
  sb.begin().callContract("SATRN","getAllTotalLiquidityPerPool",[]).endScript()
)).decoded;

getLiquidityPerPoolSaturn()

READ
getLiquidityPerPoolSaturn(pairKey: string): number

Returns the total LP liquidity units for a given pool key. This is the denominator used when computing a position's share of reserves on remove.

Parameters
NameTypeDescription
pairKeystringPool key, e.g. "SOUL_KCAL".
Returns
number — Total LP units in the pool. Returns 0 if the pool does not exist.
Example
const lp = await readContract("SATRN", "getLiquidityPerPoolSaturn", ["SOUL_KCAL"]);

getUserLiquidityInPool()

READ
getUserLiquidityInPool(from: address, poolKey: string): number

Sums the LP units across every SATRN NFT owned by `from` that belongs to `poolKey`. Use this to show a user their total share in a specific pool before they decide to remove or consolidate.

Parameters
NameTypeDescription
fromaddressWallet address to query.
poolKeystringPool key, e.g. "SOUL_KCAL".
Returns
number — Aggregate LP units held by the user in that pool. Returns 0 if they have none.
Example
const myLP = await readContract(
  "SATRN", "getUserLiquidityInPool",
  ["P2K..myWallet..", "SOUL_KCAL"]
);

Swaps

swap()

WRITE
swap(from: address, amountIn: number, tokenIn: string, tokenOut: string, minAmountOut: number): number

Executes a constant-product AMM swap of `amountIn` of `tokenIn` for `tokenOut`. The total fee is 0.4%: 0.21% stays in the pool, 0.09% goes to the admin, and 0.1% goes to the output-token owner (for SOUL/KCAL, where there is no on-chain token owner, this slice is redirected to the configured chain-token fee wallet). Holders of a Series-5 FACTORY NFT pay only 0.08% total (5× discount applied to all three slices). Pass `minAmountOut > 0` to enforce a slippage guard; the transaction reverts if the actual output is below that floor. Returns the real raw amount of `tokenOut` delivered to `from`.

Parameters
NameTypeDescription
fromaddressCaller and signing witness; both payer and recipient.
amountInnumberRaw input amount in `tokenIn` native units.
tokenInstringSymbol of the token being sold.
tokenOutstringSymbol of the token being bought.
minAmountOutnumberMinimum acceptable output. Pass 0 to disable the slippage guard (not recommended).
Returns
number — Actual raw amount of `tokenOut` transferred to `from`.
What to expect
Reverts if: pool does not exist; pool has no liquidity; amountIn is below token-scaled minimum (see getMinRawForSwap); output would round to zero; admin fee rounds to zero; token-owner fee rounds to zero; slippage guard fails (realAmountOut < minAmountOut); reentrancy guard is set for `from`.
Example
// Swap 1 SOUL for KCAL, accept at least 50_000 KCAL raw
const from = "P2K..myWallet..";
const tx = sb.begin()
  .allowGas(from, sb.nullAddress, gasPrice, gasLimit)
  .callContract("SATRN", "swap", [from, 100000000, "SOUL", "KCAL", 50000])
  .spendGas(from)
  .endScript();
const hash = await wallet.sendTransaction(tx, "main");

Liquidity (v2)

initializePool()

WRITE
initializePool(from: address, amountToken0: number, amountToken1: number, token0Symbol: string, token1Symbol: string): bool

Creates a brand-new liquidity pool for the `token0Symbol`/`token1Symbol` pair. Both tokens must not already share a pool (in either orientation). The initial price is set by the ratio `amountToken0 / amountToken1`. A SATRN NFT representing the initial LP position is minted to `from`. The pool key will be `token0Symbol_token1Symbol`. Returns `true` on success.

Parameters
NameTypeDescription
fromaddressCreator and signing witness; must hold sufficient balances of both tokens.
amountToken0numberRaw amount of token0 to seed the pool.
amountToken1numberRaw amount of token1 to seed the pool.
token0SymbolstringSymbol of the first token.
token1SymbolstringSymbol of the second token.
Returns
bool — Always true on success (reverts on any failure).
What to expect
Reverts if: pool already exists; either token does not exist on-chain; amounts are below their scaled minimums (see getMinRawForPoolCreation); caller has insufficient balance; token0Symbol == token1Symbol; reentrancy guard is set for `from`.
Example
const from = "P2K..myWallet..";
const tx = sb.begin()
  .allowGas(from, sb.nullAddress, gasPrice, gasLimit)
  .callContract("SATRN", "initializePool",
    [from, 100000000, 500000000, "SOUL", "KCAL"])
  .spendGas(from)
  .endScript();
await wallet.sendTransaction(tx, "main");

addLiquidity_v2()

WRITE
addLiquidity_v2(from: address, amountFirstToken: number, tokenFirst: string, tokenSecond: string, maxSecondTokenAmount: number): bool

Adds liquidity to an existing pool. You specify how much of `tokenFirst` to deposit; the contract calculates the proportional amount of `tokenSecond` required to maintain the current price. If that computed second-token amount exceeds `maxSecondTokenAmount`, the transaction reverts — use this as your slippage cap on the second token. A new SATRN NFT is minted representing the LP units added. Returns `true` on success.

Parameters
NameTypeDescription
fromaddressCaller and signing witness; must hold the required balances.
amountFirstTokennumberRaw amount of `tokenFirst` to deposit.
tokenFirststringSymbol of the token you are specifying the deposit amount for.
tokenSecondstringSymbol of the paired token (computed amount).
maxSecondTokenAmountnumberMaximum raw units of `tokenSecond` you are willing to pay. Acts as a slippage guard.
Returns
bool — Always true on success.
What to expect
Reverts if: pool does not exist or has no liquidity; amountFirstToken is below scaled minimum (see getMinRawForAddLiquidity); computed second-token amount exceeds maxSecondTokenAmount; caller lacks balance; liquidityTokens minted would be 0; reentrancy guard is set for `from`.
Example
const from = "P2K..myWallet..";
// Pre-check: const required = await readContract("SATRN","getMinRawForAddLiquidity",["SOUL"]);
const tx = sb.begin()
  .allowGas(from, sb.nullAddress, gasPrice, gasLimit)
  .callContract("SATRN", "addLiquidity_v2",
    [from, 50000000, "SOUL", "KCAL", 300000000])
  .spendGas(from)
  .endScript();
await wallet.sendTransaction(tx, "main");

removeLiquidity()

WRITE
removeLiquidity(from: address, NFTuniqueID: number): bool

Burns a SATRN LP NFT and returns the proportional share of pool reserves to `from`. The returned amounts are computed as `(nftLiquidity / poolTotalLiquidity) × reserveForToken`, then scaled back to raw units. If the scale-down rounding would lose more than `maxTruncationPercent` of either side, the transaction reverts with a "consolidate first" message — use consolidatePositions() to merge small NFTs before removing. TACAL reward claims are no longer performed automatically here; claim TACAL separately if needed. Returns `true` on success.

Parameters
NameTypeDescription
fromaddressCaller and signing witness; must own the NFT.
NFTuniqueIDnumberThe unique numeric ID of the SATRN LP NFT to burn.
Returns
bool — Always true on success.
What to expect
Reverts if: `from` does not own the NFT; pool has no liquidity; scaled share rounds to zero for either token; truncation exceeds maxTruncationPercent (10% default); reentrancy guard is set for `from`.
Example
const nftId = 42; // from getUserLiquidityPools_v2
const tx = sb.begin()
  .allowGas(from, sb.nullAddress, gasPrice, gasLimit)
  .callContract("SATRN", "removeLiquidity", [from, nftId])
  .spendGas(from)
  .endScript();
await wallet.sendTransaction(tx, "main");

consolidatePositions()

WRITE
consolidatePositions(from: address, poolKey: string): bool

Merges all SATRN LP NFTs owned by `from` in `poolKey` into a single new NFT. Requires at least two matching NFTs. This is the standard fix for "truncation too high" errors on removeLiquidity — a single large NFT always has enough LP units to survive the scale-down check. TACAL reward claims are no longer performed automatically; claim before consolidating if needed. Returns `true` on success.

Parameters
NameTypeDescription
fromaddressCaller and signing witness; must own at least 2 NFTs in the pool.
poolKeystringPool key of the positions to merge, e.g. "SOUL_KCAL".
Returns
bool — Always true on success.
What to expect
Reverts if: fewer than 2 matching NFTs are found; total liquidity is 0; pool does not exist; reentrancy guard is set for `from`.
Example
const tx = sb.begin()
  .allowGas(from, sb.nullAddress, gasPrice, gasLimit)
  .callContract("SATRN", "consolidatePositions", [from, "SOUL_KCAL"])
  .spendGas(from)
  .endScript();
await wallet.sendTransaction(tx, "main");

LP Position NFTs

getUserLiquidityPools_v2()

READ
getUserLiquidityPools_v2(from: address): string*

Yields the pool key associated with each SATRN LP NFT owned by `from`, in ownership-list order. Parallel-iterate with NFT.getOwnerships to map NFT IDs to their pool keys. This is the recommended way to enumerate a user's open positions.

Parameters
NameTypeDescription
fromaddressWallet address to enumerate positions for.
Returns
string* — Iterator of pool key strings, one per owned SATRN NFT.
Example
const script = sb.begin()
  .callContract("SATRN", "getUserLiquidityPools_v2", ["P2K..myWallet.."])
  .endScript();
const poolKeys = (await api.invokeRawScript("main", script)).decoded;

getNFTliquidity()

READ
getNFTliquidity(nftidnumber: number): number

Returns the LP units stored in a specific SATRN NFT. This is the numerator used by removeLiquidity when computing the user's reserve share.

Parameters
NameTypeDescription
nftidnumbernumberSATRN NFT unique ID.
Returns
number — LP units represented by this NFT. Returns 0 if the ID is unknown or already burned.
Example
const lp = await readContract("SATRN", "getNFTliquidity", [42]);

getNFTticketReserveA()

READ
getNFTticketReserveA(nftidnumber: number): string

Returns the symbol of the first token (reserve A) encoded in the LP NFT. This is the `token0` orientation the pool was created with.

Parameters
NameTypeDescription
nftidnumbernumberSATRN NFT unique ID.
Returns
string — Token symbol for reserve A, e.g. "SOUL".
Example
const tokenA = await readContract("SATRN", "getNFTticketReserveA", [42]);

getNFTticketReserveB()

READ
getNFTticketReserveB(nftidnumber: number): string

Returns the symbol of the second token (reserve B) encoded in the LP NFT.

Parameters
NameTypeDescription
nftidnumbernumberSATRN NFT unique ID.
Returns
string — Token symbol for reserve B, e.g. "KCAL".
Example
const tokenB = await readContract("SATRN", "getNFTticketReserveB", [42]);

getNFTticketKey()

READ
getNFTticketKey(nftidnumber: number): string

Returns the pool key encoded in the LP NFT, derived by concatenating reserveA + "_" + reserveB. Equivalent to calling getNFTticketReserveA + "_" + getNFTticketReserveB but in a single call.

Parameters
NameTypeDescription
nftidnumbernumberSATRN NFT unique ID.
Returns
string — Pool key string, e.g. "SOUL_KCAL".
Example
const key = await readContract("SATRN", "getNFTticketKey", [42]);

getTotalNftMinted()

READ
getTotalNftMinted(): number

Returns the running count of live SATRN LP NFTs (incremented on mint, decremented on burn). Useful for analytics and for verifying that consolidation or removal decreased the count.

Returns
number — Current number of live SATRN LP NFTs.
Example
const count = await readContract("SATRN", "getTotalNftMinted", []);

Fees & Discounts

getChainTokenFeeWallet()

READ
getChainTokenFeeWallet(): address

Returns the wallet that receives the 0.1% token-owner fee slice on swaps where the output token is SOUL or KCAL (chain-native tokens that have no on-chain token owner). Returns `@null` if admin has never called setChainTokenFeeWallet, in which case the swap path uses the hardcoded fallback address P2KAZhQ34TrLE7diBUqQNiTG1CyZjrntegfwcP9UcY5xC87.

Returns
address — Configured fee-redirect wallet, or @null if not yet set.
Example
const wallet = await readContract("SATRN", "getChainTokenFeeWallet", []);

getTotalStorageFeesPaid()

READ
getTotalStorageFeesPaid(): number

Returns the cumulative SOUL storage-fee amount accrued in the contract's accounting ledger (historic, from before v3.9.8.13 when fees were removed). The actual contract balance minus scaled reserves represents skimmable surplus today.

Returns
number — Accumulated SOUL storage fee total in raw SOUL units (historic accounting only).
Example
const fees = await readContract("SATRN", "getTotalStorageFeesPaid", []);

Protocol Views

getScaleFactor()

READ
getScaleFactor(tokenSymbol: string): number

Returns the cached scale factor for a token — the multiplier applied to raw amounts before AMM math. For an 8-decimal token the factor is 1; for a 0-decimal token it is 10^8. Returns 0 if the scale factor has not been computed yet (it is computed and cached on the first pool or swap interaction with that token).

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Scale factor. 1 for 8-decimal tokens; 10^8 for 0-decimal tokens; 0 if uncached.
Example
const sf = await readContract("SATRN", "getScaleFactor", ["CROWN"]);

getTargetDecimals()

READ
getTargetDecimals(): number

Returns the internal decimal-place target all tokens are scaled to before AMM arithmetic. Currently fixed at 8.

Returns
number — Target decimals (8).
Example
const dec = await readContract("SATRN", "getTargetDecimals", []);

getMaxTruncationPercent()

READ
getMaxTruncationPercent(): number

Returns the current maximum truncation-loss threshold (in percent) enforced by removeLiquidity. If scale-down rounding discards more than this percentage on either token side, the remove reverts and the user must consolidate positions first.

Returns
number — Max truncation percent (default 10, range 1–50).
Example
const maxTrunc = await readContract("SATRN", "getMaxTruncationPercent", []);

getMinRawForPoolCreation()

READ
getMinRawForPoolCreation(tokenSymbol: string): number

Returns the minimum raw amount of `tokenSymbol` required to create a pool. Factors in the token's scale factor and the global minScaledPoolUnits parameter. Call this before building the initializePool transaction to validate user input.

Parameters
NameTypeDescription
tokenSymbolstringToken you intend to use in the pool.
Returns
number — Minimum raw amount in `tokenSymbol` native units for pool creation.
Example
const minSoul = await readContract("SATRN", "getMinRawForPoolCreation", ["SOUL"]);

getMinRawForSwap()

READ
getMinRawForSwap(tokenSymbol: string): number

Returns the minimum raw input amount for swapping `tokenSymbol`. The swap reverts below this floor because the admin fee would round to zero at smaller amounts. Always call this before submitting a swap to avoid unnecessary revert costs.

Parameters
NameTypeDescription
tokenSymbolstringInput token symbol.
Returns
number — Minimum raw input amount for `tokenSymbol` swaps.
Example
const minIn = await readContract("SATRN", "getMinRawForSwap", ["SOUL"]);

getMinRawForAddLiquidity()

READ
getMinRawForAddLiquidity(tokenSymbol: string): number

Returns the minimum raw amount of `tokenSymbol` when calling addLiquidity_v2. The second token's amount is computed proportionally, so only the first token needs this explicit check.

Parameters
NameTypeDescription
tokenSymbolstringFirst-token symbol for the addLiquidity_v2 call.
Returns
number — Minimum raw amount of the first token for add-liquidity.
Example
const minAdd = await readContract("SATRN", "getMinRawForAddLiquidity", ["SOUL"]);

getReentrancyGuardState()

READ
getReentrancyGuardState(user: address): number

Returns the reentrancy guard value for `user`. 0 = unlocked (normal state), 1 = locked (a state-mutating call from this address is in progress). Useful for diagnostics; if a call failed mid-execution and left the guard stuck at 1, the admin can call clearReentrancy. Under normal operation this always returns 0.

Parameters
NameTypeDescription
useraddressAddress to check.
Returns
number — 0 = unlocked, 1 = locked.
Example
const guard = await readContract("SATRN", "getReentrancyGuardState", ["P2K..myWallet.."]);

getVersion()

READ
getVersion(): string

Returns the on-chain version string of the deployed SATRN contract. Use this to confirm which build is live before relying on version-specific behaviour.

Returns
string — Version string, e.g. "3.9.9.2".
Example
const ver = await readContract("SATRN", "getVersion", []);

Disabled Methods (Disabled in v1.0)

addLiquidity()

WRITE
addLiquidity(from: address, amountFirstToken: number, tokenFirst: string, tokenSecond: string)

DISABLED. This method was the original add-liquidity entrypoint but lacked a `maxSecondTokenAmount` slippage guard. It is permanently disabled in v3.9.9.1 — calling it always reverts with "Deprecated: use addLiquidity_v2 with maxSecondTokenAmount parameter". Use addLiquidity_v2() instead.

Parameters
NameTypeDescription
fromaddressCaller address (unused — always reverts).
amountFirstTokennumberDeposit amount (unused).
tokenFirststringFirst token symbol (unused).
tokenSecondstringSecond token symbol (unused).
What to expect
Always reverts. Do not call.

getUserLiquidityPools()

READ
getUserLiquidityPools(from: address, nftID: number): string*

DISABLED. The original per-NFT pool-lookup method. It is permanently disabled in v3.9.9.1 — calling it always reverts with "Deprecated: use getUserLiquidityPools_v2". Use getUserLiquidityPools_v2() instead, which iterates all owned NFTs without requiring an individual NFT ID.

Parameters
NameTypeDescription
fromaddressWallet address (unused — always reverts).
nftIDnumberNFT ID (unused).
What to expect
Always reverts. Do not call.