SATURN Reference Interactive docs · Bots & AI agents · 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 (31 modular contracts across core DEX, advanced pools & capital, financial products, agent automation, and lending — live on Phantasma mainnet at https://pharpc1.phantasma.info/rpc, nexus mainnet, and on devnet at https://devnet.phantasma.info/rpc, nexus testnet) and v3 (the legacy monolithic SATRN contract). Read methods are free to call; a write method that takes a caller address must be signed by that wallet (the witness), and an admin-only method by the admin. Address each contract by its lowercase symbol (e.g. saturnrouter, saturnswap, saturnmarket) or SATRN for v3. Gas is KCAL (most calls 0.02–0.15 KCAL; createPool about 2,500 KCAL for the NFT mint); no Saturn contract charges SOUL, but new storage keys escrow about 0.002 SOUL each from the sender.

How to build: Use phantasma-sdk-ts (npm install phantasma-sdk-ts) or the Poltergeist / Ecto wallets through PhantasmaLink. To READ: script = new ScriptBuilder().beginScript().callContract(contractId, method, [args]).endScript(); res = await new PhantasmaAPI(rpcUrl, null, nexus).invokeRawScript('main', script). A revert comes back as text in res.error. Each yielded value is a hex string in res.results (a generator yields one per item); decode it with VMObject.FromBytes(Base16.decodeUint8Array(hex)). The examples call this readContract(contractId, method, [args]), write new ScriptBuilder().beginScript() as ScriptBuilder.begin() or sb.begin(), and write allowGas(from, null, gasPrice, gasLimit) for the full call (for example gasPrice 100000 and gasLimit 200000, a 2 KCAL cap). To WRITE: allowGas(from, Address.Null, 100000, gasLimit), callContract(contractId, method, [args]), spendGas(from), endScript(). A dApp has the user sign it in the wallet; a bot signs new Transaction(nexus, 'main', script, expiration, '') with its own key and sends it with api.sendRawTransaction. The signer is the on-chain witness. Gas is paid in KCAL: the fee is gasPrice x gasUsed in raw KCAL (10 decimals), capped at gasPrice x gasLimit.

Launch the app: Operations (Saturn v4) · v3 swap, mainnet and devnet from its network menu. Machine-readable: llms.txt · llms-full.txt. Examples: GitHub.

32 contracts · 1010 public methods documented.

Saturn DEX v4

Core DEX · Contract #1

SaturnAdmin

saturnadmin saturnadmin-4.4.0

Central configuration hub. Exposes global protocol parameters — the four-way fee split (reinvest / provider / admin / holder), pool fee range, scaling limits and minimum amounts. All other Saturn contracts read from here, and so does your frontend. Every method outside the Admin & Internal section is a free view call and the source of truth for showing limits to users before they submit transactions; the writes in that section are reserved for the protocol admin or other Saturn contracts, or are deprecated and always revert. Mainnet and devnet run the fee split 60 / 10 / 20 / 10 (reinvest / provider / admin / holder). The SOUL entry fees of earlier releases are gone: createPool and addLiquidity no longer charge SOUL, and the getters that used to expose those fees are kept only for compatibility and always return 0.

Ownership & Access

getAdmin()

READ
getAdmin(): address

Returns the current protocol admin address. The admin slice of every swap fee (getAdminPct()) is paid to it, and the admin-only methods of saturnadmin and most other v4 contracts check its signature (SATURN keeps its own owner, SATURN.getOwner()). Use it to show protocol ownership or to check whether a wallet has admin rights.

Returns
address — The admin address for the Saturn protocol.
What to expect
Always returns a valid, non-null address. No reverts.
Example
const admin = await readContract("saturnadmin", "getAdmin", []);
// "P2KBPHBKq1xuoSajuKxQCd7RfCfGFyoczoHQdVxacEUc9As" on mainnet and devnet today

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnadmin-4.4.0". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnadmin-4.4.0".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnadmin", "getContractVersion", []);
// "saturnadmin-4.4.0"

Fee Split Configuration

getFeeSplitRatios()

READ
getFeeSplitRatios(): string

Returns all four fee split percentages packed into a single string of the form "reinvest:X_provider:Y_admin:Z_holder:H". Convenient for single-call rendering of the complete fee split. The holder slice is credited to the saturnholders stakers of the swap's input token; since saturnswap-4.4.3 it stays in the pool as reinvest when nobody stakes that token.

Returns
string — Mainnet and devnet today: "reinvest:60_provider:10_admin:20_holder:10" — whole percentages that sum to 100.
What to expect
The four numbers always sum to 100 and admin is always >= 1. A fresh deployment starts at 60 / 10 / 30 / 0. Use this if you want one round-trip instead of four.
Example
const split = await readContract("saturnadmin", "getFeeSplitRatios", []);
// "reinvest:60_provider:10_admin:20_holder:10" on mainnet and devnet
const parts = Object.fromEntries(split.split("_").map((kv) => kv.split(":")));

getReinvestPct()

READ
getReinvestPct(): number

Percentage of each swap fee that stays in the pool's reserves, growing its depth. saturnswap does not read this value: the reinvest part is whatever is left of the fee after the provider, admin and holder slices, so when nobody stakes the input token the holder slice is added to it (70% instead of 60% on mainnet today).

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 sent straight to the protocol admin wallet (saturnadmin.getAdmin()) at swap time.

Returns
number — Whole percent of each swap fee (constructor default 30; 20 on mainnet and devnet today).
What to expect
Always ≥ 1: both fee-split setters revert with "Admin split must be >= 1%" below it. A swap whose admin slice rounds to 0 raw units reverts with "Admin fee rounds to zero".
Example
const admin = await readContract("saturnadmin", "getAdminPct", []);

getHolderPct()

READ
getHolderPct(): number

Percentage of each swap fee credited to the stakers of the swap's input token in saturnholders (saturnholders.accrueHolderFee), pro-rata by stake; 10 on mainnet and devnet today. Since saturnswap-4.4.3 the slice is taken only when saturnholders.getTotalStaked(tokenIn) > 0; when nobody stakes the input token it stays in the pool as reinvest. Together with getReinvestPct(), getProviderPct() and getAdminPct() it sums to 100. Zero means no holder rewards are being paid from swap fees (the value on a fresh deployment).

Returns
number — Whole-number percent (0..100).
What to expect
Never reverts. Changes only when the admin updates the split.
Example
const holderPct = await readContract("saturnadmin", "getHolderPct", []);

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: every token amount is converted to this many decimals inside the protocol (multiplied up for tokens with fewer decimals, divided down for tokens with more, such as KCAL's 10), so pool reserves and fee balances are 8-decimal numbers.

Returns
number — Target decimals: 8.
What to expect
Fixed at 8: saturnadmin has no setter for it.
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
10,000,000,000 on mainnet and devnet (the constructor default) = 100 whole tokens per side: 10,000,000,000 raw SOUL (8 decimals), 1,000,000,000,000 raw KCAL (10 decimals). createPool reverts with "token0 needs at least N raw units" / "token1 needs at least N raw units" below it.
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 swap input in 8-decimal scaled units.
What to expect
1,000,000 on mainnet and devnet (the constructor default) = 0.01 token: 1,000,000 raw SOUL (8 decimals), 100,000,000 raw KCAL (10 decimals); the raw minimum never drops below getAbsoluteMinRaw() (100 raw units). saturnswap.swap and swapFromContract revert with "Below minimum swap" under saturnrouter.getMinRawForSwap(tokenIn). The admin cannot set it below 1,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
100,000,000 on mainnet and devnet (the constructor default) = 1 whole token of the pool's tokenA: 100,000,000 raw SOUL, 10,000,000,000 raw KCAL (never below 100 raw units). addLiquidity reverts with "Min N raw units for TOKEN" below it; token B has no minimum of its own.
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
Fixed at 100 raw units: saturnadmin has no setter for it. 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", []);

Removed SOUL Fees (always 0)

getSOULfeeCreatePool()

READ
getSOULfeeCreatePool(): number

Always returns 0. The SOUL storage fee that createPool() used to charge was removed; the method is kept so older integrations keep working. Do not budget SOUL for pool creation because of this value — the one notable extra cost of createPool() is the gas burned to mint the SATURN certificate NFT (about 2,500 KCAL).

Returns
number — Always 0.
What to expect
Never reverts. Frozen at 0.
Example
const fee = await readContract("saturnadmin", "getSOULfeeCreatePool", []);
// fee === 0

getSOULfeeAddLiquidity()

READ
getSOULfeeAddLiquidity(): number

Always returns 0. addLiquidity() no longer charges a SOUL fee; the getter remains for ABI compatibility only.

Returns
number — Always 0.
What to expect
Never reverts. Frozen at 0.
Example
const fee = await readContract("saturnadmin", "getSOULfeeAddLiquidity", []);

getStorageFee()

READ
getStorageFee(): number

Always returns 0. The protocol no longer collects SOUL for storage. On Phantasma the transaction itself escrows a small amount of SOUL per new storage key it creates (about 0.002 SOUL, refunded when the key is later deleted), which is why wallets that create pools, orders, bonds or loans should hold a little SOUL besides KCAL for gas.

Returns
number — Always 0.
What to expect
Never reverts. Frozen at 0.
Example
const accrued = 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);

getGuardOwner()

READ
getGuardOwner(user: address): string

Returns the name of the Saturn contract that currently holds the per-user reentrancy lock for `user` (for example "saturnswap" or "saturnbonds"), or an empty string when the user is not locked. Pair it with getGuardLocked() when diagnosing a transaction that was refused with a reentrancy error.

Parameters
NameTypeDescription
useraddressWallet whose lock is inspected.
Returns
string — Contract name holding the lock, or "" when unlocked.
What to expect
Never reverts. A lock that survives a transaction can only be released by the admin (clearReentrancy); a normal call never leaves one behind, because a failed transaction is rolled back as a whole, lock included.
Example
const owner = await readContract("saturnadmin", "getGuardOwner", [userAddress]);

Admin & Internal

updateAdmin()

WRITE
updateAdmin(newAdmin: address)

Admin only. Only the saturnadmin owner (the wallet getAdmin() returns) can call this. Hands the protocol admin role to newAdmin at once. Everything that reads saturnadmin.getAdmin() follows: the admin slice of each swap fee goes to the new wallet, and most admin checks and upgrades in the other v4 contracts accept its signature. SATURN keeps its own owner (SATURN.getOwner()), which this does not change.

Parameters
NameTypeDescription
newAdminaddressNew admin wallet; must not be null.
What to expect
Reverts: "Only admin" (caller is not the current admin), "Invalid admin" (null address). Emits AdminUpdated for newAdmin with note "admin-updated". There is no two-step accept: a wrong address locks the admin out.

updateFeeSplitRatios()

WRITE
updateFeeSplitRatios(newReinvest: number, newProvider: number, newAdmin: number)

Admin only. Only the saturnadmin owner can call this. Legacy 3-way setter: sets the reinvest / provider / admin percentages of every swap fee and forces the holder slice (getHolderPct) to 0, which switches holder rewards off. Mainnet and devnet run 60 / 10 / 20 / 10 today; use updateFeeSplitRatiosV2 to keep a holder slice.

Parameters
NameTypeDescription
newReinvestnumberWhole percent of the fee left in the pool.
newProvidernumberWhole percent credited to the pool provider in saturnfees.
newAdminnumberWhole percent sent to the admin wallet; at least 1.
What to expect
Reverts: "Only admin", "Must sum to 100%", "Admin split must be >= 1%", "Reinvest cannot be negative", "Provider cannot be negative". Emits FeeSplitUpdated(reinvestPct, providerPct, adminPct). Applies from the next swap.

updateFeeSplitRatiosV2()

WRITE
updateFeeSplitRatiosV2(newReinvest: number, newProvider: number, newAdmin: number, newHolder: number)

Admin only. Only the saturnadmin owner can call this. Sets the 4-way swap-fee split in whole percent: reinvest (left in the pool), provider (credited in saturnfees), admin (sent to getAdmin()) and holder (credited to saturnholders stakers of the swap's input token; since saturnswap-4.4.3 it stays in the pool when nobody stakes that token). Mainnet and devnet run 60 / 10 / 20 / 10.

Parameters
NameTypeDescription
newReinvestnumberWhole percent of the fee left in the pool.
newProvidernumberWhole percent credited to the pool provider.
newAdminnumberWhole percent sent to the admin wallet; at least 1.
newHoldernumberWhole percent credited to stakers of the input token.
What to expect
Reverts: "Only admin", "Must sum to 100%", "Admin split must be >= 1%", "Reinvest cannot be negative", "Provider cannot be negative", "Holder cannot be negative". Emits FeeSplitUpdatedV2(reinvestPct, providerPct, adminPct, holderPct). Applies from the next swap.

updatePoolFeeRange()

WRITE
updatePoolFeeRange(newMin: number, newMax: number)

Admin only. Only the saturnadmin owner can call this. Sets the allowed range of per-swap pool fees (per 10,000). New pools (createPool, syndicate, launchpad, saturnclpools) and fee changes (saturnpools.updatePoolFee, rentals, fee options) must stay inside it. Existing pools keep their fee. Mainnet and devnet: 30 to 3000.

Parameters
NameTypeDescription
newMinnumberLowest pool fee per 10,000; at least 10 (0.1%).
newMaxnumberHighest pool fee per 10,000; at most 5000 (50%), and above newMin.
What to expect
Reverts: "Only admin", "Min fee too low (10 = 0.1%)", "Max fee too high (5000 = 50%)", "Min must be < max". Emits PoolFeeRangeUpdated(minFeePer10k, maxFeePer10k).

updateMinScaledAmounts()

WRITE
updateMinScaledAmounts(newPoolMin: number, newSwapMin: number, newAddLiqMin: number)

Admin only. Only the saturnadmin owner can call this. Sets the three minimums, in 8-decimal scaled units: per side of createPool, per swap input, and per addLiquidity token A. saturnpools.getMinRawForToken turns each into a raw minimum per token (never below getAbsoluteMinRaw(), 100).

Parameters
NameTypeDescription
newPoolMinnumberScaled minimum per side for createPool; at least 1,000,000 (0.01 token). Today 10,000,000,000 (100 tokens).
newSwapMinnumberScaled minimum swap input; at least 1,000,000. Today 1,000,000 (0.01 token).
newAddLiqMinnumberScaled minimum token A for addLiquidity; at least 100,000 (0.001 token). Today 100,000,000 (1 token).
What to expect
Reverts: "Only admin", "Pool min too low", "Swap min too low", "AddLiq min too low". Emits MinScaledUpdated(poolMin, swapMin, addLiqMin).

updateMaxTruncationPercent()

WRITE
updateMaxTruncationPercent(newPercent: number)

Admin only. Only the saturnadmin owner can call this. Sets how much of a reserve (whole percent) scale-down rounding may lose when a pool is removed; removePool and saturnpools.clearPoolReserves revert with "Truncation N% on TOKEN" above it.

Parameters
NameTypeDescription
newPercentnumberWhole percent, 1 to 50. Today 10.
What to expect
Reverts: "Only admin", "Must be 1-50". Emits TruncationUpdated(maxPercent).

updateSOULfees()

WRITE
updateSOULfees(newCreatePool: number, newAddLiquidity: number)

Admin only. DEPRECATED — the saturnadmin owner's call always reverts with "SOUL entry fees removed: createPool/addLiquidity no longer charge SOUL"; any other caller gets "Only admin". Kept in the ABI for upgrade compatibility; getSOULfeeCreatePool() and getSOULfeeAddLiquidity() always return 0.

Parameters
NameTypeDescription
newCreatePoolnumberIgnored.
newAddLiquiditynumberIgnored.
What to expect
Always reverts.

accrueStorageFee()

WRITE
accrueStorageFee(feesFromSoul: number)

Internal: only saturnliquidity can call this. DEPRECATED no-op: it records nothing and moves no tokens, and getStorageFee() always returns 0. Kept in the ABI for upgrade compatibility; saturnliquidity 4.3.3 no longer calls it.

Parameters
NameTypeDescription
feesFromSoulnumberIgnored.
What to expect
Any other caller: "Only liquidity manager". From saturnliquidity it does nothing.

increaseStorage()

WRITE
increaseStorage(from: address, stakeAmount: number, soultoken: string)

DEPRECATED — always reverts with "Storage staking removed: Gen3 storage is funded by tx data escrow, not by staking or SOUL charged here", for every caller. Kept in the ABI for upgrade compatibility. Storage is paid by each transaction's SOUL data escrow instead (about 0.002 SOUL per new storage key).

Parameters
NameTypeDescription
fromaddressIgnored.
stakeAmountnumberIgnored.
soultokenstringIgnored.
What to expect
Always reverts.

acquireGuard()

WRITE
acquireGuard(user: address)

Internal: only saturnswap, saturnliquidity, saturnfees, saturnrewards, saturnclpools, saturnflash, saturnholders, saturnbonds, saturnrental, saturnfeeopts, saturnsyndicate, saturnlaunchpad, saturnarb, saturnlimit, saturnpredict, saturnvaults, saturntwamm and saturnstakearb can call this. Takes the per-user reentrancy lock at the start of a user action: marks user as locked and records the calling contract as the lock owner (see getGuardLocked / getGuardOwner).

Parameters
NameTypeDescription
useraddressWallet the lock is keyed on (the user of the outer call).
What to expect
Reverts: "Only DEX contracts" (any other caller), "Reentrancy detected" (user already locked, e.g. a nested Saturn call for the same wallet in one transaction). The lock stays until the same contract calls releaseGuard or the admin calls clearReentrancy.

releaseGuard()

WRITE
releaseGuard(user: address)

Internal: only the Saturn DEX contracts listed under acquireGuard can call this, and only the one that took the lock. Clears the per-user reentrancy lock at the end of a user action.

Parameters
NameTypeDescription
useraddressWallet whose lock is released.
What to expect
Reverts: "Only DEX contracts", "Only the acquiring contract can release" (the caller is not getGuardOwner(user), including when no lock is held).

clearReentrancy()

WRITE
clearReentrancy(user: address)

Admin only. Only the saturnadmin owner can call this. Force-clears a user's reentrancy lock (getGuardLocked becomes 0, getGuardOwner becomes ""). Meant for a lock left behind by a bug; a failed transaction rolls its own lock back.

Parameters
NameTypeDescription
useraddressWallet whose lock is cleared.
What to expect
Reverts: "Only admin". Emits GuardCleared(note "guard-cleared"). Works whether or not a lock is held.
Core DEX · Contract #2

SaturnPools

saturnpools saturnpools-4.1.10

Canonical source of every pool in the protocol. Stores per-pool state (provider, token pair, reserves, fee, active flag, certificate NFT id, campaign and financial lock counts, and the burn flag and time-lock end that only saturnlplock can write), 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. The provider can update the per-pool fee while the pool is free of campaign and financial-product locks (on a burned pool only downwards). The certificate holder can take the provider role with claimPoolProvider, and anyone can warm a token's scale with computeAndStoreScaleFactor. The pawn / unpawn methods are deprecated stubs that always revert.

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
Never reverts. A pool id that was never created returns "_".
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 × getScaleFactor / getScaleDivisor, rounded down (a divisor of 0 counts as 1).
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: amount × getScaleDivisor / getScaleFactor, rounded down.
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: 10^(8 − decimals) for a token with 8 decimals or fewer, 1 for a token with more (the division is in getScaleDivisor). scaled = raw × factor / divisor. Live values: SOUL factor 1 / divisor 1, RA and TAZ 1 / 10, KCAL 1 / 100. 0 means the scale was never computed.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Scale multiplier (1 for tokens above 8 decimals); 0 when never computed.
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: <symbol>" 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");
}

getScaleDivisor()

READ
getScaleDivisor(tokenSymbol: string): number

Returns the cached divisor that scaleDown() applies to a token whose decimals exceed the protocol target (8). It is 1 for tokens at or below the target and 0 when the token's scale has never been computed (cold cache). With a cold cache scaleUp() and scaleDown() return the amount unchanged and getMinRawForToken() returns saturnadmin.getAbsoluteMinRaw(), while saturnrouter.getBestPoolForSwapV2() and the router's getMinRawFor*() fail as reads because they try to write the cache. A factor cached with divisor 0 (before the divisor existed) is treated as divisor 1.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Power of ten (1, 10, 100 …), or 0 when the cache is cold.
What to expect
Never reverts. Any pool creation, swap route or lending price that touches the token warms the cache as a side effect.
Example
const divisor = await readContract("saturnpools", "getScaleDivisor", ["KCAL"]);

computeAndStoreScaleFactor()

WRITE
computeAndStoreScaleFactor(symbol: string)

Warms the scale cache for a token: reads Token.getDecimals(symbol) and stores the factor / divisor pair that converts raw amounts to the protocol's 8-decimal internal units. Idempotent — it does nothing when the factor is already stored. No witness is required, so any wallet (or your indexer) can call it, and every path that needs a scale (createPool, router scoring, the lending adapter's warmScale) calls it internally. Use it before the first read against a brand-new token, because getScaleFactor() / getScaleDivisor() return 0 while the cache is cold.

Parameters
NameTypeDescription
symbolstringToken symbol to warm.
What to expect
Never reverts for a token that exists on-chain (a missing symbol fails inside Token.getDecimals). The first call for a token writes two storage keys, which the sender's transaction funds through Phantasma's SOUL storage escrow.
Example
// Warm the scale cache once for a newly listed token
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpools", "computeAndStoreScaleFactor", ["NEWTOKEN"])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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
Since 4.1.10 the provider follows the SATURN certificate: every certificate transfer that does not involve the lending vault makes the new holder the provider (the transfer reverts while the pool carries a financial product or a campaign). A certificate moved before 4.1.10 needs claimPoolProvider(). A loan liquidation or default makes the lender the provider. Reverts for a pool id that was never created ("Invalid cast"), like saturnrouter.getPoolProvider; the other per-pool getters return 0 or "".
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 and pair index forever. Only getBestPoolForSwapV2 skips them: getPoolCountForPair, getPoolIdForPairAtIndex, getPoolFullInfo, getAllPoolsData and getAllPoolIds still return them.
Example
const isActive = (await readContract("saturnpools", "getPoolActive", [poolId])) === 1;

getPoolPawned()

READ
getPoolPawned(poolId: number): number

1 while the pool is pledged as collateral for a saturnmarket loan (since 4.1.8), 0 otherwise. A pledged pool holds a financial lock of exactly 1 and its SATURN certificate sits in saturnvault; swaps, provider fees and addLiquidity continue, but it cannot be removed, re-priced by the provider, burned, time-locked, enrolled in a campaign or put under another product until the loan is repaid (pledge released, certificate back to the borrower) or liquidated / defaulted (the lender becomes the provider). pawnPool / unpawnPool are unrelated deprecated stubs.

Parameters
NameTypeDescription
poolIdnumberPool to inspect.
Returns
number — 1 = pledged to a loan, 0 = not pledged.
What to expect
Never reverts.
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 financial products hold this pool: bonds, rentals, fee options, syndicate and launchpad pools, or a loan pledge (exactly 1 while getPoolPawned() = 1). While > 0 the pool cannot be removed and the provider cannot change its fee; swaps and addLiquidity continue.

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

Length of the token list behind getTokensInDEXList(). A symbol is appended whenever a pool is created while the DEX holds none of that token (its global reserve is 0), so a token whose pools were all removed and later re-created is listed again: this counts entries, not unique tokens (mainnet lists ANGEL three times).

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
Insertion order. The same symbol can appear more than once (see getCountOfTokensOnList): de-duplicate on the client, or take the tokens that trade now from getActivePoolsData().
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 token balance at the saturnpools contract address. saturnpools holds no tokens: reserves, unclaimed provider fees and holder rewards all sit at saturnliquidity.getLiquidityAddress(), so this normally returns 0 (mainnet SOUL reads 0).

Parameters
NameTypeDescription
tokenSymbolstringToken symbol.
Returns
number — Raw balance (not scaled).
What to expect
Debug only. It is not the reserve backing: use getReserveValue(symbol) for the scaled total in pools, and the token balance of saturnliquidity.getLiquidityAddress() for custody (which also holds fees, holder rewards and concentrated-pool liquidity).
Example
const balance = await readContract("saturnpools", "getContractTokenBalanceEach", ["SOUL"]);

getAllPoolsData()

READ
getAllPoolsData(): string*

Yields one pipe-delimited row per registered pool (active or removed), in registration order: poolId|tokenA|tokenB|reserveA|reserveB|feePer10k|active. Reserves are in the protocol's 8-decimal scaled units — use scaleDown() to display them. One call replaces seven per-pool reads when building a pool table.

Returns
string* — Stream of "poolId|tokenA|tokenB|reserveA|reserveB|feePer10k|active" rows.
What to expect
Empty when no pool has ever been created. Removed pools appear with active = 0 and zero reserves. It walks every pool id in one read, and a read is capped at 100,000 VM work units: on devnet (about 240 pool ids) it already faults with "VM work budget exceeded". Mainnet (36 ids) is far from it. For large registries page through getAllPoolIds with the per-pool getters or saturnrouter.getPoolFullInfo.
Example
const rows = await readContract("saturnpools", "getAllPoolsData", []);
// rows[0] => "1|KCAL|SOUL|100000000000|250000000000|30|1"
const pools = rows.map((r) => { const [id, a, b, ra, rb, fee, active] = r.split("|"); return { id: +id, a, b, ra, rb, fee: +fee, active: active === "1" }; });

getActivePoolsData()

READ
getActivePoolsData(): string*

Same rows as getAllPoolsData() but only for pools whose active flag is 1. The natural feed for a swap UI's pool list.

Returns
string* — Stream of "poolId|tokenA|tokenB|reserveA|reserveB|feePer10k|active" rows (active = 1 on every row).
What to expect
Empty until the first pool is created. It still walks every pool id, active or not, in one read: on devnet (about 240 ids) it uses about 93% of the 100,000-unit read budget and will fault at around 250 ids. Mainnet (36 ids) is far from it. Page through getAllPoolIds with the per-pool getters when the registry grows.
Example
const rows = await readContract("saturnpools", "getActivePoolsData", []);

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", []);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnpools-4.1.10". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnpools-4.1.10".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnpools", "getContractVersion", []);
// "saturnpools-4.1.10"

Time-Weighted Prices (TWAP)

getTwapTracked()

READ
getTwapTracked(poolId: number): number

1 when the admin keeps a time-weighted average price for this pool (setTwapTracked), 0 otherwise. The RA/TAZ reference pool the lending protocol prices with is tracked (pool 33 on mainnet, saturndexadapt.getReferencePool()).

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = tracked, 0 = not tracked.
What to expect
Never reverts. The cumulative getters return 0 growth for an untracked pool.
Example
const tracked = await readContract("saturnpools", "getTwapTracked", [33]); // 1 on mainnet

getTwapCumulativeA()

READ
getTwapCumulativeA(poolId: number): number

Accumulated price of token A in token B, Uniswap-v2 style: the sum of (reserveB × getTwapScale() / reserveA) × the seconds each price held, up to now (the interval since the last reserve write is added at the current reserves, so no write is needed to read it). Two readings c1 at t1 and c2 at t2 give the average price (c2 − c1) / (t2 − t1), scaled by 10^18. Several reserve writes in one block add zero weight, so a swap in and back inside one transaction does not move the average.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Price of A in B × 10^18 × seconds, cumulative.
What to expect
Never reverts. Compare two readings only while getTwapTracked() = 1 and getTwapSince() is not later than the first reading. The price is a ratio of 8-decimal scaled reserves, so it is the human price whatever the decimals. Exception, devnet only: pools created before the scale cache fix keep reserves in the older unit (the six KCAL pools #15, #19, #20, #78, #84 and #85, and AMIPOLAKAO, whose getScaleDivisor is 0), so their price is off by 10^(decimals − 8). Mainnet has no such pool.
Example
const c1 = BigInt(await readContract("saturnpools", "getTwapCumulativeA", [poolId]));
const t1 = Math.floor(Date.now() / 1000);
// ... at least a few minutes later
const c2 = BigInt(await readContract("saturnpools", "getTwapCumulativeA", [poolId]));
const t2 = Math.floor(Date.now() / 1000);
const avgBperA = Number((c2 - c1) / BigInt(t2 - t1)) / 1e18; // wall-clock seconds approximate chain time

getTwapCumulativeB()

READ
getTwapCumulativeB(poolId: number): number

The same accumulator for the price of token B in token A (reserveA × 10^18 / reserveB, times seconds).

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Price of B in A × 10^18 × seconds, cumulative.
What to expect
Never reverts. Same rules as getTwapCumulativeA.
Example
const cB = await readContract("saturnpools", "getTwapCumulativeB", [poolId]);

getTwapSince()

READ
getTwapSince(poolId: number): number

Unix time the current unbroken price series started: when tracking started, or the last reserve write that found a side empty (a hole). While a side is empty it reports the current time. 0 while the pool is not tracked. If it is later than your first reading, discard that reading: the average would span a stop or a hole.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Unix seconds, or 0 when untracked.
What to expect
Never reverts.
Example
const since = await readContract("saturnpools", "getTwapSince", [poolId]);

getTwapLastUpdate()

READ
getTwapLastUpdate(poolId: number): number

Unix time the pool's accumulators were last written (its last reserve write while tracked, or when tracking started).

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Unix seconds.
What to expect
Never reverts. The cumulative getters already add the time since this moment.
Example
const last = await readContract("saturnpools", "getTwapLastUpdate", [poolId]);

getTwapScale()

READ
getTwapScale(): number

The scale of every TWAP price: 10^18.

Returns
number — 1000000000000000000.
What to expect
Never reverts. Constant.
Example
const scale = await readContract("saturnpools", "getTwapScale", []);

Burn, Time Lock & Certificate

getPoolWithdrawable()

READ
getPoolWithdrawable(poolId: number): number

1 when the provider could withdraw the pool today (liquidity not burned and no live time lock: now ≥ getPoolLockUntil), 0 otherwise. Burns and time locks are made through saturnlplock (burnPool, lockPool). A pool reading 0 cannot be pledged as loan collateral; the lending reference pool must read 0.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = withdrawable, 0 = burned or time-locked.
What to expect
Never reverts. Financial and campaign locks are separate (getPoolFinancialLockCount, getPoolCampaignLockCount).
Example
const canPull = (await readContract("saturnpools", "getPoolWithdrawable", [poolId])) === 1;

getPoolBurned()

READ
getPoolBurned(poolId: number): number

1 when the pool's liquidity was burned through saturnlplock.burnPool: removePool refuses it forever ("Pool liquidity is burned - it can never be withdrawn"), the provider can only lower its fee, and the SATURN certificate becomes the pool's fee key (saturnfees pays the provider fees to its holder, and it cannot be destroyed). Swaps continue. 0 otherwise. A burn cannot be undone.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = burned, 0 = not burned.
What to expect
Never reverts.
Example
const burned = await readContract("saturnpools", "getPoolBurned", [poolId]);

getPoolLockUntil()

READ
getPoolLockUntil(poolId: number): number

Unix time until which the pool's liquidity is time-locked through saturnlplock.lockPool (0 = never locked). removePool refuses the pool until then with "Pool liquidity is time-locked until <unix>". A live lock can only be extended, never shortened.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Unix seconds, 0 when not time-locked.
What to expect
Never reverts. A past time means the lock has run out. The value is kept after the lock ends and after a burn, so compare it with the current time (or read getPoolWithdrawable).
Example
const until = await readContract("saturnpools", "getPoolLockUntil", [poolId]);

getNftPoolId()

READ
getNftPoolId(nftId: number): number

Reverse of getPoolNftId: the pool a SATURN certificate id belongs to. 0 when unknown (certificates minted before the reverse map existed are filled in by claimPoolProvider or a burn).

Parameters
NameTypeDescription
nftIdnumberSATURN certificate token id.
Returns
number — Pool ID, or 0.
What to expect
Never reverts.
Example
const poolId = await readContract("saturnpools", "getNftPoolId", [nftId]);

claimPoolProvider()

WRITE
claimPoolProvider(from: address, poolId: number)

The holder of a pool's SATURN certificate takes the provider role. Needed for pools whose certificate moved before 4.1.10 (when the provider did not follow the certificate); since 4.1.10 every certificate transfer does this automatically. Also records the certificate id to pool id map. Refused while the pool is pledged to a loan, under a bond, rental or fee option, or enrolled in a reward campaign.

Parameters
NameTypeDescription
fromaddressCertificate holder (must be the transaction witness).
poolIdnumberPool whose certificate from holds.
What to expect
Reverts with "Not authorized", "Pool not active", "Pool has no certificate", "Only the certificate holder", "Pool is pledged to a loan", "Pool is under a bond, rental or fee option: move the certificate after it ends" or "Pool is enrolled in a reward campaign: move the certificate after it ends". Emits PoolLockChanged with lockType "provider" when the provider changes.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpools", "claimPoolProvider", [from, poolId])
  .spendGas(from)
  .endScript();
// sign with the certificate holder's wallet and send

skim()

WRITE
skim(to: address, tokentoskim: string)

DISABLED — always reverts with "skim disabled (F-08): shared-custody reconciliation removed - cannot distinguish stranded tokens from module-owned funds". Kept in the ABI for compatibility.

Parameters
NameTypeDescription
toaddressIgnored.
tokentoskimstringIgnored.
What to expect
Always reverts.

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 (getMinPoolFeePer10k .. getMaxPoolFeePer10k, 30–3000 per 10k by default) and the pool must be free of locks: no active reward-campaign enrollment and no financial product (bond, rental, option, syndicate, launchpad or loan collateral). While a rental or fee option is live the fee is driven by saturnrental / saturnfeeopts, which call this method on the operator's behalf — a direct call from the provider, even one who owns the rental listing, is refused until the product ends.

Parameters
NameTypeDescription
fromaddressPool provider (must be the transaction witness).
poolIdnumberPool to update.
newFeePer10knumberNew fee in units of 1/10,000 (30 = 0.3%).
What to expect
Reverts with: "Not authorized to change fee" (caller is not the provider), "Pool not active", "Pool locked in campaign - cannot change fee", "Pool under financial product - fee locked to owning contract", "Fee too low, min: N" or "Fee too high, max: N". A provider who raises the fee of a burned pool gets "Burned pool: the fee can only go down". Emits PoolFeeChanged with reason "provider", "rental" or "feeopts".
Example
// Move the pool fee to 0.5%
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpools", "updatePoolFee", [from, poolId, 50])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

pawnPool()

WRITE
pawnPool(from: address, poolId: number)

DEPRECATED — always reverts with "Deprecated in 4.1.0: pawn functionality moved to a dedicated contract". Kept in the ABI for compatibility; there is nothing to call here. A pool is pledged to a loan through saturnmarket.acceptQuote, after which getPoolPawned() reads 1.

Parameters
NameTypeDescription
fromaddressIgnored.
poolIdnumberIgnored.
What to expect
Always reverts.

unpawnPool()

WRITE
unpawnPool(from: address, poolId: number)

DEPRECATED — always reverts with "Deprecated in 4.1.0: pawn functionality moved to a dedicated contract". Kept in the ABI for compatibility; there is nothing to call here. A loan pledge is released by the full repayment (saturnloans.makePayment) or handed to the lender on liquidation / default.

Parameters
NameTypeDescription
fromaddressIgnored.
poolIdnumberIgnored.
What to expect
Always reverts.

Admin & Internal

clearScaleFactor()

WRITE
clearScaleFactor(symbol: string)

Admin only. The saturnpools _owner (the owner address set when the contract was deployed, not the saturnadmin getAdmin() check) must sign. Sets the cached scale factor and divisor of symbol back to 0. Until computeAndStoreScaleFactor runs again (every swap, pool creation and liquidity add calls it), scaleUp and scaleDown return amounts unchanged and getMinRawForToken returns saturnadmin.getAbsoluteMinRaw. Token decimals never change, so a recompute normally stores the same pair. The exception is a legacy entry (factor set, divisor 0) for a token with more than 8 decimals: it is recomputed with a divisor of 10^(decimals - 8) instead of the identity it used before. Its stored reserves are not rewritten, so they, and any open saturnloans loan in that token, are then read off by that factor. Use it only when no loan is open in the symbol and the reserves are migrated in the same transaction.

Parameters
NameTypeDescription
symbolstringToken symbol whose cached scale factor and divisor to clear.
What to expect
Reverts with "Only admin" unless the saturnpools _owner signed. It does not check that the token exists and emits no event.

setTwapTracked()

WRITE
setTwapTracked(poolId: number, tracked: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must sign. tracked = 1 starts the time-weighted price of an active pool: getTwapLastUpdate and getTwapSince are set to now and the accumulators keep their values. Starting a pool that is already tracked changes nothing. tracked = 0 first brings the accumulators up to now at the current reserves, then freezes them and sets getTwapSince to 0. Stopping a pool that is not tracked changes nothing. Each change emits PoolLockChanged(poolId, tracked, "twap").

Parameters
NameTypeDescription
poolIdnumberPool to start or stop tracking.
trackednumber1 = start tracking, 0 = stop.
What to expect
Reverts on: "Only admin" (the saturnadmin owner did not sign), "tracked must be 0 or 1", "Pool not active" (only when starting).

registerPool()

WRITE
registerPool(from: address, token0Symbol: string, token1Symbol: string, scaledAmount0: number, scaledAmount1: number, customFeePer10k: number): number

Internal: only saturnliquidity (createPool), saturnsyndicate or saturnlaunchpad can call this. Creates the pool record: takes the next pool id, records from as provider, stores the pair and the opening reserves (8-decimal scaled), marks the pool active and sets its fee per 10,000. It also adds the pool to the pair index (getPairPoolCount / getPairPoolAtIndex) and to getAllPoolIds, adds the reserves to the global per-token totals (getReserveValue), appends a token to getTokensInDEXList when its global total was 0 and a pair to getPairsInDEXList when it had no pool yet, and emits PoolRegistered. The caller moves the tokens; this method moves none.

Parameters
NameTypeDescription
fromaddressProvider recorded for the pool (for syndicate and launchpad pools, that contract's own address).
token0SymbolstringFirst token, stored as tokenA.
token1SymbolstringSecond token, stored as tokenB.
scaledAmount0numberOpening reserve of token0, 8-decimal scaled.
scaledAmount1numberOpening reserve of token1, 8-decimal scaled.
customFeePer10knumberPool fee per 10,000 (30 = 0.30%), within saturnadmin.getMinPoolFeePer10k / getMaxPoolFeePer10k.
Returns
number — The new pool id.
What to expect
Reverts on: "Only liquidity, syndicate, or launchpad", "Fee too low, min: <min>", "Fee too high, max: <max>".

addReserves()

WRITE
addReserves(poolId: number, scaledAddA: number, scaledAddB: number)

Internal: only saturnliquidity (addLiquidity) can call this. Adds scaledAddA and scaledAddB (8-decimal scaled) to the pool's reserves and to the global per-token totals, brings the TWAP up to date first if the pool is tracked, and emits PoolReservesChanged with reason "addLiquidity". The caller checks the pool and the provider and moves the tokens.

Parameters
NameTypeDescription
poolIdnumberPool receiving liquidity.
scaledAddAnumberAmount added to reserve A, 8-decimal scaled.
scaledAddBnumberAmount added to reserve B, 8-decimal scaled.
What to expect
Reverts with "Only liquidity manager" for any other caller.

setReservesAfterSwap()

WRITE
setReservesAfterSwap(poolId: number, newResA: number, newResB: number, tokenIn: string, tokenOut: string, scaledInDelta: number, scaledOutDelta: number)

Internal: only saturnswap can call this. Writes the pool's new reserves after a swap (8-decimal scaled), adds scaledInDelta to the global total of tokenIn and takes scaledOutDelta from the total of tokenOut, brings the TWAP up to date first if the pool is tracked, and emits PoolReservesChanged with reason "swap". tokenIn and tokenOut must be the pool's own pair, in either order.

Parameters
NameTypeDescription
poolIdnumberPool that was swapped against.
newResAnumberNew reserve A, 8-decimal scaled.
newResBnumberNew reserve B, 8-decimal scaled.
tokenInstringToken sold into the pool.
tokenOutstringToken bought from the pool.
scaledInDeltanumberAmount of tokenIn added to the reserve (the reinvested input), 8-decimal scaled.
scaledOutDeltanumberAmount of tokenOut taken from the reserve, 8-decimal scaled.
What to expect
Reverts on: "Only swap engine", "Token pair mismatch", "Drainage: <tokenOut>" (the global total of tokenOut would go below 0).

clearPoolReserves()

WRITE
clearPoolReserves(poolId: number)

Internal: only saturnliquidity (removePool), saturnsyndicate or saturnlaunchpad can call this. It is the one gate every pool withdrawal passes. It refuses a pledged, burned or still time-locked pool, and a pool with any financial or campaign lock. It checks that converting each reserve back to raw units loses no more than saturnadmin.getMaxTruncationPercent, subtracts the reserves from the global per-token totals, sets both reserves to 0 and the pool inactive, and emits PoolCleared. The caller sends the tokens out.

Parameters
NameTypeDescription
poolIdnumberPool being removed.
What to expect
Reverts on: "Only liquidity manager, syndicate, or launchpad", "Pool is pledged to a loan", "Pool liquidity is burned - it can never be withdrawn", "Pool liquidity is time-locked until <unix time>", "Pool has financial locks", "Pool has campaign locks", "Truncation <n>% on <token>", "Drainage: <token>".

incrementCampaignLock()

WRITE
incrementCampaignLock(poolId: number)

Internal: only saturnrewards can call this, from enrollInCampaign. Adds 1 to the pool's campaign lock count (getPoolCampaignLockCount) and emits PoolLockChanged(poolId, newCount, "campaign"). While the count is above 0 the provider cannot change the fee, remove the pool or move its certificate.

Parameters
NameTypeDescription
poolIdnumberPool joining a reward campaign.
What to expect
Reverts on: "Only rewards contract", "Pool is pledged to a loan".

decrementCampaignLock()

WRITE
decrementCampaignLock(poolId: number)

Internal: only saturnrewards can call this, from claimCampaignReward and withdrawFromCampaign. Takes 1 off the pool's campaign lock count and emits PoolLockChanged(poolId, newCount, "campaign"). At 0 it does nothing and does not revert.

Parameters
NameTypeDescription
poolIdnumberPool leaving a reward campaign.
What to expect
Reverts with "Only rewards contract" for any other caller.

incrementFinancialLock()

WRITE
incrementFinancialLock(poolId: number)

Internal: only saturnbonds, saturnrental, saturnfeeopts, saturnsyndicate or saturnlaunchpad can call this, when a financial product takes the pool. Adds 1 to the pool's financial lock count (getPoolFinancialLockCount) and emits PoolLockChanged(poolId, newCount, "financial"). While the count is above 0 the provider cannot change the fee, remove the pool or move its certificate. saturndexadapt is not on this list since 4.1.8: lending locks a pool only through pledgePool.

Parameters
NameTypeDescription
poolIdnumberPool taken by a financial product.
What to expect
Reverts on: "Only financial contracts", "Pool is pledged to a loan".

decrementFinancialLock()

WRITE
decrementFinancialLock(poolId: number)

Internal: only saturnbonds, saturnrental, saturnfeeopts, saturnsyndicate or saturnlaunchpad can call this, when their product ends. Takes 1 off the pool's financial lock count and emits PoolLockChanged(poolId, newCount, "financial"). At 0 it does nothing. On a pledged pool it will not take the count below 1: that lock belongs to the pledge, and only releasePledge or handOverPledgedPool clears it.

Parameters
NameTypeDescription
poolIdnumberPool released by a financial product.
What to expect
Reverts on: "Only financial contracts", "Pool is pledged to a loan: its lock is released only by releasePledge or handOverPledgedPool" (a pledged pool with a count under 2).

setPoolNftId()

WRITE
setPoolNftId(poolId: number, nftId: number)

Internal: only the SATURN certificate contract can call this, from mintPoolCertificate when a pool is created. Records the certificate id on the pool (getPoolNftId) and the reverse map from certificate to pool (getNftPoolId). No event.

Parameters
NameTypeDescription
poolIdnumberPool the certificate belongs to.
nftIdnumberSATURN certificate token id.
What to expect
Reverts with "Only NFT contract" for any other caller.

adminReduceReservesForMigration()

WRITE
adminReduceReservesForMigration(poolId: number, scaledSubA: number, scaledSubB: number)

Internal: only saturnfees can call this, from its admin-only adminZeroAccumulatorForMigration (the saturnadmin owner signs there). That method zeroes a pool's legacy provider-fee claimable balances and passes the same amounts here, which takes scaledSubA and scaledSubB (8-decimal scaled) off the pool's reserves and the global per-token totals. A side passed as 0 is left alone. Brings the TWAP up to date first if the pool is tracked and emits PoolReservesChanged with reason "migration".

Parameters
NameTypeDescription
poolIdnumberPool being migrated.
scaledSubAnumberAmount to take off reserve A, 8-decimal scaled.
scaledSubBnumberAmount to take off reserve B, 8-decimal scaled.
What to expect
Reverts on: "Only saturnfees", "Migration would underflow reserveA", "Migration would underflow reserveB", "Migration underflow: <token>" (the global total would go below 0).

setPoolBurned()

WRITE
setPoolBurned(poolId: number)

Internal: only saturnlplock can call this, from burnPool. Marks the pool's liquidity burned for good (getPoolBurned = 1): clearPoolReserves refuses it forever, the provider can only lower the fee, and SATURN refuses to burn its certificate, which stays the fee key. Also records the certificate-to-pool map (getNftPoolId) and emits PoolLockChanged(poolId, 1, "burned"). The financial lock count is untouched, so bonds, rentals and fee options still work on a burned pool.

Parameters
NameTypeDescription
poolIdnumberPool whose liquidity is burned.
What to expect
Reverts on: "Only lock contract", "Pool not active", "Pool is pledged to a loan".

setPoolLockUntil()

WRITE
setPoolLockUntil(poolId: number, lockUntil: number)

Internal: only saturnlplock can call this, from lockPool. Sets the unix time (seconds) until which the pool's liquidity cannot be withdrawn (getPoolLockUntil); clearPoolReserves refuses the pool before then. saturnlplock only ever moves it later. Emits PoolLockChanged(poolId, lockUntil, "timelock").

Parameters
NameTypeDescription
poolIdnumberPool being time-locked.
lockUntilnumberUnix time in seconds when withdrawal opens again.
What to expect
Reverts on: "Only lock contract", "Pool not active", "Pool is pledged to a loan".

pledgePool()

WRITE
pledgePool(poolId: number, owner: address)

Internal: only saturndexadapt can call this, from v4LockPool when saturnvault takes a borrower's v4 pool as loan collateral. owner must sign and be the pool's provider. The pool must be active, have a certificate and carry nothing: no pledge, no financial or campaign lock, not burned, no live time lock. Sets the financial lock count to exactly 1 and getPoolPawned to 1, and emits PoolLockChanged with lockType "financial", then "pledge". While pledged, the pool takes no other lock, burn, time lock or campaign and cannot be removed.

Parameters
NameTypeDescription
poolIdnumberPool being pledged.
owneraddressPool provider (the borrower); must sign.
What to expect
Reverts on: "Only the lending adapter (saturndexadapt)", "The pool owner must sign the pledge", "Pool not active", "Only the pool provider can pledge it", "Pool has no certificate", "Pool already pledged", "Pool has financial locks", "Pool has campaign locks", "Pool liquidity is burned", "Pool liquidity is time-locked".

releasePledge()

WRITE
releasePledge(poolId: number)

Internal: only saturndexadapt can call this, from v4UnlockPool when saturnvault releases the collateral after full repayment. Clears the pledge (getPoolPawned = 0) and takes the financial lock count down by 1. It is lenient on the count, so a repayment is never blocked here. Emits PoolLockChanged with lockType "financial", then "unpledge".

Parameters
NameTypeDescription
poolIdnumberPledged pool to release.
What to expect
Reverts on: "Only the lending adapter (saturndexadapt)", "Pool not pledged", "Pledged pool has no financial lock".

handOverPledgedPool()

WRITE
handOverPledgedPool(poolId: number, newProvider: address)

Internal: only saturndexadapt can call this, from v4HandOverPool on a liquidation or default. Strict: the pool must be pledged, active and hold a financial lock count of exactly 1, so the lender never receives a pool another product still holds. Clears the pledge and the lock and makes newProvider (the lender) the provider; saturnvault sends the certificate in the same call. Emits PoolLockChanged with lockType "financial", then "handover".

Parameters
NameTypeDescription
poolIdnumberPledged pool to hand over.
newProvideraddressThe lender, who becomes the provider.
What to expect
Reverts on: "Only the lending adapter (saturndexadapt)", "Pool not pledged", "Pledged pool must hold exactly one financial lock", "Pool not active", "Invalid new provider", "New provider is already the provider".

followCertificate()

WRITE
followCertificate(poolId: number, holder: address)

Internal: only the SATURN certificate contract can call this. SATURN's onSend trigger calls it on every certificate transfer that neither starts nor ends at the lending vault, when getNftPoolId knows the certificate's pool (for an older certificate it does not, and the new holder runs claimPoolProvider instead). The new holder becomes the pool's provider (fee claims, fee changes, liquidity, removal). It refuses while the pool is pledged, under a bond, rental or fee option, or enrolled in a reward campaign, so the whole certificate transfer reverts with the strings below and an obligation never changes hands half way. A removed (inactive) pool is skipped, so its certificate moves freely. A change of provider emits PoolLockChanged(poolId, 0, "provider").

Parameters
NameTypeDescription
poolIdnumberPool of the certificate being sent.
holderaddressReceiver of the certificate.
What to expect
Reverts on: "Only the certificate contract", "Invalid holder", "Pool is pledged to a loan", "Pool is under a bond, rental or fee option: move the certificate after it ends", "Pool is enrolled in a reward campaign: move the certificate after it ends".
Core DEX · Contract #3

SaturnLiquidity

saturnliquidity saturnliquidity-4.3.3

The wallet entry point for creating a pool, adding liquidity to your own pool, and removing a pool entirely. It is also the custodian of every v4 pool's tokens: other Saturn contracts pay out of it through sendTokensOut(). The three user methods (createPool, addLiquidity, removePool) take the protocol reentrancy guard, need the caller's wallet signature, and coordinate with SaturnPools (createPool and removePool also with the SATURN NFT contract). A pool's rights follow its SATURN certificate (saturnpools 4.1.10, SATURN 4.1.5). No SOUL entry fee is charged any more; the one cost to plan for is createPool(), whose SATURN certificate mint burns about 2,500 KCAL of gas on top of the normal transaction fee.

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 units of 1/10,000 — it 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 are transferred into protocol custody. Several pools can exist for the same pair, each with its own fee, which is why the router scores pools instead of assuming one per pair.

Parameters
NameTypeDescription
fromaddressCreator wallet (must be witness).
amountToken0numberRaw amount of the first token to seed; at least saturnrouter.getMinRawForPoolCreation(token0Symbol), 100 whole tokens today (10,000,000,000 raw SOUL).
amountToken1numberRaw amount of the second token to seed; at least getMinRawForPoolCreation(token1Symbol), 100 whole tokens today (1,000,000,000,000 raw KCAL).
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
Reverts: "Reentrancy detected", "Only wallet owner can call." (from did not sign), "Token does not exist: SYMBOL", "amountToken0 must be > 0" / "amountToken1 must be > 0", "Same token", "token0 needs at least N raw units" / "token1 needs at least N raw units" (below getMinRawForPoolCreation), "Fee too low, min: N" / "Fee too high, max: N" (outside saturnadmin.getPoolFeeRange()). Budget gas generously: minting the SATURN certificate burns about 2,500 KCAL (a mainnet createPool on 2026-09-27 paid 2,500.05 KCAL), far more than any other Saturn call (well under 1 KCAL). Pre-check minimums with saturnrouter.getMinRawForPoolCreation() so the UI can clamp inputs. Emits PoolCreated(poolId, tokenA, tokenB, amountA, amountB, feePer10k) with raw amounts; the new poolId is saturnpools.getNextPoolId() read just before.
Example
// Create a 0.3% fee SOUL/KCAL pool seeded with 100 SOUL and 100 KCAL
const minSoul = await readContract("saturnrouter", "getMinRawForPoolCreation", ["SOUL"]); // 10000000000
const minKcal = await readContract("saturnrouter", "getMinRawForPoolCreation", ["KCAL"]); // 1000000000000
const poolId = await readContract("saturnpools", "getNextPoolId", []); // expected id; confirm it in the PoolCreated event

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit) // gasPrice × gasLimit must cover ~2,500 KCAL (certificate mint)
  .callContract("saturnliquidity", "createPool", [from,
    10000000000,     // amountToken0: 100 SOUL (8 decimals)
    1000000000000,   // amountToken1: 100 KCAL (10 decimals)
    "SOUL", "KCAL",
    30])             // customFeePer10k: 30 = 0.3%
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send; the wallet needs the two amounts plus ~2,500 KCAL gas

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. Token A is the pool's tokenA (saturnpools.getPoolTokenA), not whichever token you pick. Only the pool provider can add liquidity; since saturnpools 4.1.10 the provider follows the SATURN certificate. A burned or time-locked pool (saturnlplock) still accepts liquidity, and the added tokens fall under the same terms: on a burned pool they can never be withdrawn, and on a time-locked pool not before the lock ends.

Parameters
NameTypeDescription
fromaddressPool provider wallet (must be witness).
poolIdnumberThe pool to deposit into.
amountTokenAnumberRaw amount of the pool's tokenA to add; at least saturnrouter.getMinRawForAddLiquidity(tokenA), 1 whole token today (100,000,000 raw SOUL).
maxAmountTokenBnumberMaximum raw amount of token B you're willing to add.
Returns
void — Success = amountTokenA and the required token B moved into custody and the pool's reserves increased. Emits LiquidityAdded(poolId, amountTokenA, token B taken) in raw units.
What to expect
Reverts: "Reentrancy detected", "Not authorized" (from did not sign), "Pool not active", "Only pool provider can add liquidity", "Amount must be > 0", "maxAmountTokenB must be > 0", "Min N raw units for TOKEN" (below getMinRawForAddLiquidity), "Reserve A is zero" / "Reserve B is zero", "Rounds to zero" (the required B rounds to 0 raw units), "Exceeds max: N" (required B is above maxAmountTokenB). Required B = scaleDown(scaleUp(amountTokenA, tokenA) × reserveB ÷ reserveA, tokenB), rounded down; compute it from reserves read just before sending.
Example
// Project the token B amount from the live reserves (8-decimal scaled; use BigInt)
const tokenA = await readContract("saturnpools", "getPoolTokenA", [poolId]);
const tokenB = await readContract("saturnpools", "getPoolTokenB", [poolId]);
const resA = BigInt(await readContract("saturnpools", "getPoolReserveA", [poolId]));
const resB = BigInt(await readContract("saturnpools", "getPoolReserveB", [poolId]));
const amountA = 1000000000n; // raw tokenA, e.g. 10 SOUL (8 decimals)
const scaledA = BigInt(await readContract("saturnpools", "scaleUp", [amountA, tokenA]));
const needB = BigInt(await readContract("saturnpools", "scaleDown", [scaledA * resB / resA, tokenB]));
const maxB = needB * 101n / 100n; // accept 1% price movement

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnliquidity", "addLiquidity", [from, poolId,
    amountA,   // amountTokenA: raw units of the pool's tokenA
    maxB])     // maxAmountTokenB: raw tokenB cap, reverts "Exceeds max: N" above it
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

removePool()

WRITE
removePool(from: address, poolId: number)

Removes a pool entirely. Both reserves are paid out in raw units together with the pool's pending provider fees, the pool is deactivated, and its SATURN certificate is burned if the caller holds it (otherwise the certificate is left alone). The caller must hold the pool's SATURN certificate or be its provider (saturnpools.getPoolProvider). Since saturnpools 4.1.10 a certificate transfer also makes the new holder the provider, so a seller loses the right to remove the pool; the provider route remains so a pool whose certificate was destroyed can still be withdrawn. The pool cannot be removed while it is enrolled in a reward campaign, held by any financial product (bond, rental, fee option, syndicate, launchpad or loan pledge), or burned or time-locked through saturnlplock.

Parameters
NameTypeDescription
fromaddressCertificate holder or pool provider (must be witness); receives the payout.
poolIdnumberPool to remove.
Returns
void — Success = pool marked inactive and tokens returned. Emits PoolRemoved(poolId, returnedA, returnedB, returnedPendingA, returnedPendingB) in raw units.
What to expect
Reverts: "Reentrancy detected", "Not authorized" (from did not sign), "Pool not active", "Only the pool certificate holder or the original provider", "Pool locked in reward campaign - wait for campaign to end", "Pool has active financial products (bond/rental/option) - cannot remove" (also a loan pledge, syndicate or launchpad lock), "Truncation N% on TOKEN" (scale-down rounding would lose more than getMaxTruncationPercent of a reserve), and from saturnpools.clearPoolReserves "Pool liquidity is burned - it can never be withdrawn" or "Pool liquidity is time-locked until T" (T in unix seconds). Check saturnpools.getPoolWithdrawable and the lock counts first to give users a clear message.
Example
// Pre-flight: removePool reverts while any of these blocks it
const withdrawable = await readContract("saturnpools", "getPoolWithdrawable", [poolId]); // 0 = burned or time-locked
const campLocks = await readContract("saturnpools", "getPoolCampaignLockCount", [poolId]);
const finLocks = await readContract("saturnpools", "getPoolFinancialLockCount", [poolId]); // bond, rental, option, syndicate, launchpad, loan pledge
if (Number(withdrawable) !== 1 || Number(campLocks) > 0 || Number(finLocks) > 0) throw new Error("Pool not removable yet");

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnliquidity", "removePool", [from, poolId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send; reserves plus pending provider fees come back in raw units

Introspection

getLiquidityAddress()

READ
getLiquidityAddress(): address

Returns the custody address that holds every pool's reserves, the unclaimed provider fees and the holder-reward slices; saturnflash borrows from this same balance (saturnflash.getMaxBorrowable reads it). Other contracts transfer tokens here before calling swapFromContract(); a frontend can use it to show total value locked per token.

Returns
address — The SaturnLiquidity contract address: S3dCxj4CLFc5WsFwDMuzP85dygnRzsWo7NVaecHurU2VmMA on mainnet and devnet.
What to expect
Never reverts. Fixed for the life of the deployment.
Example
const custody = await readContract("saturnliquidity", "getLiquidityAddress", []);

getContractTokenBalanceEach()

READ
getContractTokenBalanceEach(tokenSymbol: string): number

Raw on-chain balance of one token held in custody by this contract: every v4 pool's reserve in that token (including syndicate, launchpad and saturnclpools positions), plus provider fees and holder rewards not yet claimed. Compare it with the sum of getPoolReserveA/B (scaled down) plus pending fees to reconcile an indexer.

Parameters
NameTypeDescription
tokenSymbolstringToken to look up.
Returns
number — Raw units (token decimals) held by the contract.
What to expect
Never reverts. 0 for tokens the protocol has never held.
Example
const tvlSoul = await readContract("saturnliquidity", "getContractTokenBalanceEach", ["SOUL"]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnliquidity-4.3.3". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnliquidity-4.3.3".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnliquidity", "getContractVersion", []);
// "saturnliquidity-4.3.3"

Admin & Internal

sendTokensOut()

WRITE
sendTokensOut(to: address, tokenSymbol: string, amount: number)

Internal: only saturnswap, saturnfees, saturnpools, saturnsyndicate, saturnlaunchpad, saturnclpools, saturnflash and saturnholders can call this. Pays amount raw units of tokenSymbol out of custody (the shared balance behind every pool) to `to`. The calling contract does the accounting: swap output and admin fees, fee claims, holder rewards, flash loans, concentrated-pool, syndicate and launchpad withdrawals.

Parameters
NameTypeDescription
toaddressRecipient.
tokenSymbolstringToken to send.
amountnumberRaw units (token decimals).
What to expect
Reverts: "Not authorized" (any other caller). amount 0 is a no-op; a shortfall fails inside the token transfer.

receiveSwapInputV3()

WRITE
receiveSwapInputV3(from: address, tokenIn: string, reinvestAmount: number, providerFeeAmount: number, holderFeeAmount: number, adminFeeAmount: number)

Internal: only saturnswap can call this. Collects the input of a user-signed swap: moves reinvestAmount + providerFeeAmount + holderFeeAmount of tokenIn (raw) from `from` into custody, and adminFeeAmount straight to the saturnadmin.getAdmin() wallet.

Parameters
NameTypeDescription
fromaddressThe swapper (signed the swap transaction).
tokenInstringSwap input token.
reinvestAmountnumberRaw input that goes to the pool (after-fee amount plus the reinvest slice).
providerFeeAmountnumberRaw provider slice, kept in custody for saturnfees claims.
holderFeeAmountnumberRaw holder slice, kept in custody for saturnholders claims (0 when nobody stakes tokenIn).
adminFeeAmountnumberRaw admin slice, sent to the admin wallet.
What to expect
Reverts: "Only swap engine". Zero amounts are skipped. saturnswap.swap calls it once per swap; swapFromContract skips it because contract callers have already moved their tokens into custody.

receiveSwapInputV2()

WRITE
receiveSwapInputV2(from: address, tokenIn: string, reinvestAmount: number, providerFeeAmount: number, adminFeeAmount: number)

Internal: only saturnswap can call this. Legacy 3-amount form of receiveSwapInputV3 (no holder slice): moves reinvestAmount + providerFeeAmount of tokenIn (raw) from `from` into custody and adminFeeAmount to the saturnadmin.getAdmin() wallet. saturnswap-4.4.3 always calls receiveSwapInputV3.

Parameters
NameTypeDescription
fromaddressThe swapper.
tokenInstringSwap input token.
reinvestAmountnumberRaw input that goes to the pool.
providerFeeAmountnumberRaw provider slice.
adminFeeAmountnumberRaw admin slice.
What to expect
Reverts: "Only swap engine". Zero amounts are skipped.

receiveSwapInput()

WRITE
receiveSwapInput(from: address, tokenIn: string, poolAmount: number, adminFeeAmount: number)

DEPRECATED — always reverts with "Deprecated in 4.1.0: swap engine must call receiveSwapInputV2 or V3", for every caller. Kept in the ABI for upgrade compatibility.

Parameters
NameTypeDescription
fromaddressIgnored.
tokenInstringIgnored.
poolAmountnumberIgnored.
adminFeeAmountnumberIgnored.
What to expect
Always reverts.
Core DEX · Contract #4

SaturnSwap

saturnswap saturnswap-4.4.3

The hot path of the DEX. A constant-product AMM that reads pool reserves and the pool's custom fee rate, splits the fee four ways — reinvestment (stays in the pool), the provider (accrued in the FeeVault), holders staking the input token in saturnholders (only when that token has stakers; otherwise the slice stays in the pool), 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 contract-only swapFromContract() accepts calls only from saturnlimit, saturnarb, saturnflash, saturnstakearb, saturnvaults and saturntwamm (any other caller gets "Only authorized contracts"); those contracts first transfer the input to saturnliquidity (saturnswap itself never holds tokens) and must pass minAmountOut > 0. It applies the same pair checks and fees as swap().

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 output is the constant-product amount for amountIn minus the whole pool fee, computed in 8-decimal scaled units with integer division, and tokenOut is sent to the caller. The fee is then split: provider 10% (claimable in saturnfees), admin 20% (sent to the admin wallet), holders 10% (only when tokenIn has stakers in saturnholders) and the rest added to the pool's reserve (saturnadmin.getFeeSplitRatios). 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), "Zero reserve in" / "Zero reserve out", "Output rounds to zero", "Slippage exceeded", "Admin fee rounds to zero" (amount too small to pay a 1-unit admin fee), "Reinvest rounds to zero", "Cannot drain pool", "Not authorized" (from did not sign), "Amount must be > 0", "Same token" or "Reentrancy detected".
Example
// 1) Find the best pool for SOUL→KCAL (scan up to 20 pools of the pair)
const poolId = await readContract("saturnrouter", "getBestPoolForSwapV2", ["SOUL", "KCAL", amountIn, 20]);
if (poolId === 0) throw new Error("No liquidity available");

// 2) Quote exactly like the engine: 8-decimal scaled units, integer division
const info = await readContract("saturnrouter", "getPoolFullInfo", [poolId]);
const f = Object.fromEntries(info.split("_").map((p) => p.split(":")));
const inIsA = f.tokenA === "SOUL";
const rIn = BigInt(inIsA ? f.resA : f.resB), rOut = BigInt(inIsA ? f.resB : f.resA);
const sIn = BigInt(await readContract("saturnpools", "scaleUp", [amountIn, "SOUL"]));
const afterFee = sIn - (sIn * BigInt(f.fee)) / 10000n;
const sOut = (afterFee * rOut) / (rIn + afterFee);
const expectedOut = BigInt(await readContract("saturnpools", "scaleDown", [Number(sOut), "KCAL"]));
const minOut = Number((expectedOut * 99n) / 100n); // 1% slippage, raw KCAL

// 3) Submit the swap
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit) // up to 0.2 KCAL; one swap burns about 0.05 KCAL and the rest is refunded
  .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. The swap engine never holds tokens: swap inputs, reserves and unclaimed fees all sit at saturnliquidity.getLiquidityAddress(), and the contracts allowed to call swapFromContract() transfer their input there, not here. Useful mainly to identify saturnswap in transaction traces.

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", []);
// token balances live at saturnliquidity.getLiquidityAddress(), not here

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnswap-4.4.3". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnswap-4.4.3".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnswap", "getContractVersion", []);
// "saturnswap-4.4.3"

Admin & Internal

swapFromContract()

WRITE
swapFromContract(poolId: number, amountIn: number, tokenIn: string, tokenOut: string, minAmountOut: number, recipient: address): number

Internal: only saturnlimit, saturnarb, saturnflash, saturnstakearb, saturnvaults or saturntwamm can call this. The contract path of swap(): the same exact-pair check, minimum swap size, fee split and constant-product math, but no signer. The calling contract must first transfer amountIn of tokenIn to saturnliquidity.getLiquidityAddress(); this method pulls nothing and does not check that the input arrived. From that input the admin fee is sent to the admin wallet, the provider fee is accrued in saturnfees, the holder slice is accrued in saturnholders (only when tokenIn has stakers; otherwise it stays in the pool), the rest is added to the reserve, and tokenOut is sent to recipient. The reentrancy guard is held on recipient for the call. Emits ContractSwapExecuted.

Parameters
NameTypeDescription
poolIdnumberThe pool to swap against.
amountInnumberRaw amount of tokenIn, already sent to saturnliquidity. At least saturnpools.getMinRawForToken(tokenIn, saturnadmin.getMinScaledSwapUnits()).
tokenInstringSymbol of the token being sold.
tokenOutstringSymbol of the token being bought.
minAmountOutnumberMinimum raw amount of tokenOut; must be > 0. saturnlimit passes the order's minAmountOut and saturntwamm the chunk's floor; saturnflash, saturnarb, saturnvaults and saturnstakearb pass 1 and check their profit after the last leg.
recipientaddressReceives tokenOut. Every current caller passes its own address.
Returns
number — Raw amount of tokenOut sent to recipient.
What to expect
Reverts on: "Only authorized contracts", "Amount must be > 0", "Same token", "Invalid recipient", "Explicit slippage required: minAmountOut must be > 0", "Reentrancy detected" (recipient already holds the guard), "Pool not active", "Token pair mismatch", "Below minimum swap", "Zero reserve in" / "Zero reserve out", "Output rounds to zero", "Slippage exceeded", "Admin fee rounds to zero", "Reinvest rounds to zero", "Cannot drain pool".
Core DEX · Contract #5

SaturnFees

saturnfees saturnfees-4.1.3

Keeps the books for the provider's share of every swap fee until it is claimed. It holds no tokens: the fees sit in saturnliquidity with the rest of the pool liquidity. Each pool has two pending balances — one per token in the pair — in 8-decimal scaled units, plus a lifetime counter per token that never decreases. The provider claims both sides at once with claimProviderFees(); since saturnfees-4.1.3 the fees of a burned pool (saturnlplock) go to whoever holds its SATURN certificate instead. While a bond is outstanding or a rental runs, the fee stream is redirected to saturnbonds or saturnrental (getFeeRedirectActive(), getFeeRedirectContract()). Pools owned by saturnsyndicate or saturnlaunchpad have that contract as provider, and it claims through claimProviderFeesForContract().

Claiming

claimProviderFees()

WRITE
claimProviderFees(from: address, poolId: number)

Sweeps all pending provider fees of a pool, both tokens at once, to the caller. Pending balances are scaled down to raw units and paid out of saturnliquidity custody. Who may claim: for a normal pool, its provider (saturnpools.getPoolProvider, which follows the SATURN certificate since saturnpools 4.1.10); since saturnfees-4.1.3, for a burned pool (saturnlplock), only the current holder of its SATURN certificate, the pool's fee key. The call reverts while a fee redirect is active (a bond or rental owns the fee stream).

Parameters
NameTypeDescription
fromaddressPool provider, or the certificate holder of a burned pool (must be witness).
poolIdnumberPool whose fees to claim.
Returns
void — Success = both pending balances zeroed and paid to from in raw units. Emits FeesClaimed(poolId, tokenA, tokenB, realClaimA, realClaimB).
What to expect
Reverts: "Reentrancy detected", "Not authorized" (from did not sign), "Fees redirected by active bond or rental", "Only the certificate holder (fee key) of a burned pool" (burned pool, from does not hold its certificate), "Only pool provider" (unburned pool, from is not the provider). Zero-balance claims do not revert but still cost gas (a claim costs about 0.03 KCAL on mainnet) — query getProviderClaimablePair() 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 (BigInt(pA) > 0n || BigInt(pB) > 0n) {
  const tx = ScriptBuilder
    .begin()
    .allowGas(from, null, gasPrice, gasLimit)
    .callContract("saturnfees", "claimProviderFees", [from, poolId])
    .spendGas(from)
    .endScript();
  // sign with the caller's wallet and send
}

Views

getProviderClaimable()

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

Returns the pending provider fee of one token of a pool, in 8-decimal scaled units. It is claimable by the pool's provider, by the certificate holder of a burned pool, or by the bond/rental contract while a redirect is active. Call this once for tokenA and once for tokenB (or use getProviderClaimablePair()). 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"
}

getProviderClaimablePair()

READ
getProviderClaimablePair(poolId: number): string

Both pending provider balances of a pool in one call, packed as "tokenA:<sym>_pendingA:<n>_tokenB:<sym>_pendingB:<n>". Amounts are in scaled (8-decimal) units — pass them through saturnpools.scaleDown() to display raw token amounts.

Parameters
NameTypeDescription
poolIdnumberPool to inspect.
Returns
string — e.g. "tokenA:KCAL_pendingA:123456_tokenB:SOUL_pendingB:7890".
What to expect
Never reverts; a pool that has never earned fees reports 0 on both sides. Also re-exported by saturnrouter.
Example
const s = await readContract("saturnfees", "getProviderClaimablePair", [poolId]);
const f = Object.fromEntries(s.split("_").map((kv) => kv.split(":")));
// f.tokenA, f.pendingA, f.tokenB, f.pendingB

getProviderLifetimeFees()

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

Cumulative provider fees accrued to a pool in one token since saturnfees-4.1.2 added the counter (fees from before that build are not counted), in 8-decimal scaled units. Unlike getProviderClaimable() it is never reduced by claims or redirects, so it is the right number for "fees earned to date" statistics and for the lifetime-fee metric that saturnpredict markets settle on.

Parameters
NameTypeDescription
poolIdnumberPool to inspect.
tokenSymbolstringOne of the pool's two tokens.
Returns
number — Scaled cumulative fee total.
What to expect
Never reverts. Monotonically non-decreasing while the pool exists.
Example
const lifetime = await readContract("saturnfees", "getProviderLifetimeFees", [poolId, "SOUL"]);

getFeeRedirectContract()

READ
getFeeRedirectContract(poolId: number): string

Name of the contract that owns (or last owned) the pool's provider-fee redirect: "saturnbonds" or "saturnrental", the only callers setFeeRedirect accepts. Empty string if no redirect was ever set. Syndicate and launchpad pools are not redirects: those contracts are the pool's provider. Read it together with getFeeRedirectActive() to explain to a provider why claimProviderFees() is refused.

Parameters
NameTypeDescription
poolIdnumberPool to inspect.
Returns
string — Redirect owner contract name, or "".
What to expect
Never reverts. The value may linger after a redirect ended; getFeeRedirectActive() is the authoritative flag.
Example
const owner = await readContract("saturnfees", "getFeeRedirectContract", [poolId]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnfees-4.1.3". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnfees-4.1.3".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnfees", "getContractVersion", []);
// "saturnfees-4.1.3"

Admin & Internal

accrueProviderFee()

WRITE
accrueProviderFee(poolId: number, tokenIn: string, scaledProviderFee: number)

Internal: only saturnswap can call this. Credits a swap's provider fee (8-decimal scaled) to the pool's pending balance for tokenIn (getProviderClaimable) and to its lifetime counter (getProviderLifetimeFees).

Parameters
NameTypeDescription
poolIdnumberPool the swap ran in.
tokenInstringSwap input token (the fee is taken in it).
scaledProviderFeenumberProvider fee in 8-decimal scaled units; since saturnswap-4.4.3 it is scaleUp(realProviderFee), the raw fee actually set aside.
What to expect
Reverts: "Only swap engine". Emits FeeAccrued(poolId, tokenIn, scaledProviderFee).

setFeeRedirect()

WRITE
setFeeRedirect(poolId: number, active: number)

Internal: only saturnbonds or saturnrental can call this. active = 1 hands the pool's provider-fee stream to the calling contract (bond purchase, rental start); any other value turns it off again (bond settlement, rental end). While it is on, claimProviderFees reverts.

Parameters
NameTypeDescription
poolIdnumberPool whose fee stream is redirected.
activenumber1 = redirect to the caller, anything else = end the caller's redirect.
What to expect
Reverts: "Only bond/rental contracts", "Caller does not hold a lock on this pool" (saturnpools.getPoolFinancialLockCount is 0), "Fees already redirected for this pool" (turning on twice), "Not the redirect owner" (ending another contract's redirect). Emits FeeRedirectChanged(poolId, contractName, active). Turning off leaves getFeeRedirectContract() set.

claimRedirectedFees()

WRITE
claimRedirectedFees(poolId: number, tokenSymbol: string, recipient: address): number

Internal: only the contract that owns the pool's active fee redirect (saturnbonds or saturnrental) can call this. Pays the pool's pending provider fee in tokenSymbol to recipient: scales it down to raw units, zeroes it, and sends it out of saturnliquidity custody.

Parameters
NameTypeDescription
poolIdnumberRedirected pool.
tokenSymbolstringOne of the pool's two tokens.
recipientaddressWho receives the fees: the renter for saturnrental; saturnbonds itself or the bond issuer for saturnbonds.
Returns
number — Raw amount paid (0 when nothing was pending).
What to expect
Reverts: "No redirect active", "Not the redirect owner". Emits FeesRedirectedClaimed(poolId, tokenSymbol, realClaim). Not reentrancy-guarded itself (AUDIT-H3): callers must already hold saturnadmin.acquireGuard.

claimProviderFeesForContract()

WRITE
claimProviderFeesForContract(providerContract: address, poolId: number)

Internal: only saturnsyndicate or saturnlaunchpad can call this. Claims both sides of a pool whose provider is that contract (a contract cannot sign as a wallet witness): zeroes the pending balances and sends the raw amounts to providerContract.

Parameters
NameTypeDescription
providerContractaddressThe calling contract's address; must be the pool's provider.
poolIdnumberPool whose fees are claimed.
What to expect
Reverts: "Only contract providers", "Invalid provider", "Fees redirected by active bond or rental", "Only pool provider" (providerContract is not saturnpools.getPoolProvider(poolId)). Emits FeesClaimed(poolId, tokenA, tokenB, realClaimA, realClaimB).

claimAndReturnFees()

WRITE
claimAndReturnFees(poolId: number, tokenSymbol: string): number

Internal: only saturnliquidity can call this (removePool). Zeroes the pool's pending provider fee in tokenSymbol and returns it in raw units without moving tokens; removePool adds it to the payout.

Parameters
NameTypeDescription
poolIdnumberPool being removed.
tokenSymbolstringOne of the pool's two tokens.
Returns
number — Raw pending amount that was zeroed.
What to expect
Reverts: "Only liquidity manager". No event. Not reentrancy-guarded itself (AUDIT-H7): removePool holds the user's guard.

adminZeroAccumulatorForMigration()

WRITE
adminZeroAccumulatorForMigration(poolId: number)

Admin only. Only the protocol admin (saturnadmin.getAdmin()) can call this. Migration tool for legacy pools whose pending provider fees were also counted in their reserves: zeroes both pending balances of the pool and subtracts the same scaled amounts from its reserves (saturnpools.adminReduceReservesForMigration). The lifetime counter is not touched.

Parameters
NameTypeDescription
poolIdnumberPool to migrate.
What to expect
Reverts: "Only admin", and from saturnpools "Migration would underflow reserveA" / "Migration would underflow reserveB" / "Migration underflow: TOKEN". Emits LegacyAccumulatorZeroed(poolId, zeroedScaledA, zeroedScaledB). The zeroed pending fees are no longer claimable.
Core DEX · Contract #6

SATURN

SATURN saturnnft-4.1.5

The SATURN token is a non-fungible certificate representing ownership of a pool. SaturnLiquidity.createPool() mints one to the creator (in its own series, max supply 1); removePool() burns it when the remover holds it. Syndicate and launchpad pools get no certificate. Since saturnnft-4.1.5 the pool's rights follow the certificate: every transfer that does not involve the lending vault (saturnvault) makes the new holder the pool's provider (saturnpools.followCertificate), so the buyer can add liquidity, claim provider fees, re-price and remove the pool, and the seller no longer can. Send it with the standard Phantasma NFT transfer (Runtime.TransferToken). The sender must sign ("witness failed"), and the transfer reverts while the pool carries a bond, rental or fee option ("Pool is under a bond, rental or fee option: move the certificate after it ends") or a reward campaign ("Pool is enrolled in a reward campaign: move the certificate after it ends"). While a pool backs a loan, its certificate is held by the lending vault, whose sends skip the sync. A removed pool's certificate moves freely. A certificate with no saturnpools.getNftPoolId entry (minted before that map) moves without syncing; its holder calls saturnpools.claimPoolProvider once (every active mainnet pool has the entry today). The certificate of a burned pool (saturnlplock) is its fee key: it cannot be burned ("This certificate is the fee key of a burned pool and cannot be destroyed") and saturnfees pays that pool's fees to its holder. mintPoolCertificate and burnPoolCertificate can only be called by SaturnLiquidity.

Views

getTotalNftMinted()

READ
getTotalNftMinted(): number

Certificates minted by createPool() minus those burned by removePool(). It matches the number of active certificate-bearing pools (20 on mainnet today) unless a pool was removed while its certificate was held elsewhere or had already been burned by its holder; those removals are not subtracted. Syndicate and launchpad pools have no certificate and are not counted.

Returns
number — Counter of certificates minted minus certificates burned by removePool.
What to expect
Never reverts. +1 on every createPool(); -1 only when removePool() actually burns the certificate (the remover holds it). Useful for a protocol dashboard.
Example
const certificates = 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 one pool: saturnpools.getNftPoolId(nftId) gives the poolId, or read the NFT's properties (poolId, tokenA, tokenB, name) through the RPC getNFT call. Token IDs are 256-bit numbers (up to 78 digits); keep them as strings or BigInt, since a JavaScript Number loses precision (getUserPoolsData() returns them as strings). While a pool backs a loan, its certificate sits in the lending vault (saturnvault) and is not listed for the borrower.

Parameters
NameTypeDescription
fromaddressThe wallet to inspect.
Returns
number* — Iterable of SATURN NFT token IDs held by the address.
What to expect
Never reverts; yields nothing for wallets that hold no certificates. Use it to populate a "My Pools" dashboard: resolve each id with saturnpools.getNftPoolId(nftId) (0 = unknown) or the NFT's poolId property.
Example
const nftIds = await readContract("SATURN", "getUserPools", [userAddress]); // decode as BigInt, not Number
for (const nftId of nftIds) {
  const poolId = await readContract("saturnpools", "getNftPoolId", [nftId]); // 0 = unknown
  const nft = await api.getNFT("SATURN", String(nftId), true);
  const props = Object.fromEntries(nft.properties.map((p) => [p.key, p.value]));
  console.log("Pool #" + poolId, props.tokenA + "/" + props.tokenB, props.name);
}

holdsPoolCertificate()

READ
holdsPoolCertificate(from: address, poolId: number): number

Returns 1 if `from` currently owns the SATURN NFT that certifies pool `poolId`, 0 otherwise, including when the pool never had a certificate (unknown poolId, or a syndicate / launchpad pool). A removed pool keeps its certificate id, so if that certificate was not burned its holder still gets 1: check saturnpools.getPoolActive too. removePool(), saturnfees (burned pools), saturnlplock and the lending adapter use this check. Since saturnpools 4.1.10 the provider normally equals the holder. The exceptions are a pledged pool, whose certificate the lending vault holds while the borrower stays provider, and a certificate with no getNftPoolId entry that moved without claimPoolProvider.

Parameters
NameTypeDescription
fromaddressWallet to test.
poolIdnumberPool whose certificate is checked.
Returns
number — 1 = holds the certificate, 0 = does not.
What to expect
Never reverts. Walks the wallet's SATURN holdings, so it stays cheap for wallets with a handful of pools.
Example
const isOwner = await readContract("SATURN", "holdsPoolCertificate", [userAddress, poolId]);

getUserPoolsData()

READ
getUserPoolsData(from: address): string*

Generator variant of getUserPools() that yields each owned SATURN NFT id as a decimal string row (one id per row), so the 256-bit ids arrive without precision loss. Handy for clients that already consume the pipe-delimited *Data feeds elsewhere. Resolve each id to its pool via the NFT's ROM (poolId, tokenA, tokenB fields) exactly as with getUserPools().

Parameters
NameTypeDescription
fromaddressWallet to enumerate.
Returns
string* — Stream of NFT ids as decimal strings.
What to expect
Returns nothing for wallets that own no certificates.
Example
const nftIds = await readContract("SATURN", "getUserPoolsData", [userAddress]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnnft-4.1.5". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnnft-4.1.5".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("SATURN", "getContractVersion", []);
// "saturnnft-4.1.5"

getName()

READ
getName(): string

Token name property of the SATURN certificate contract.

Returns
string — "Saturn Pool NFT".
What to expect
Never reverts. Fixed token property.
Example
const v = await readContract("SATURN", "getName", []);
// "Saturn Pool NFT"

getSymbol()

READ
getSymbol(): string

Token symbol property. Use it ("SATURN") with the RPC getNFT / getTokenBalance calls.

Returns
string — "SATURN".
What to expect
Never reverts. Fixed token property.
Example
const v = await readContract("SATURN", "getSymbol", []);
// "SATURN"

isTransferable()

READ
isTransferable(): bool

true: certificates can be sent with the standard Phantasma NFT transfer. Since saturnnft-4.1.5 a transfer also moves the pool's provider role to the receiver, and it reverts while the pool carries a bond, rental, fee option or reward campaign (see the contract description).

Returns
bool — true.
What to expect
Never reverts. Fixed token property.
Example
const v = await readContract("SATURN", "isTransferable", []);
// true

isFungible()

READ
isFungible(): bool

false: every certificate is a unique NFT tied to one pool.

Returns
bool — false.
What to expect
Never reverts. Fixed token property.
Example
const v = await readContract("SATURN", "isFungible", []);
// false

isBurnable()

READ
isBurnable(): bool

true: a holder can burn a certificate directly, except the certificate of a burned pool (saturnlplock), whose burn reverts with "This certificate is the fee key of a burned pool and cannot be destroyed". Burning a live pool's certificate does not remove the pool: its provider keeps it, and removePool still works through the provider.

Returns
bool — true.
What to expect
Never reverts. Fixed token property.
Example
const v = await readContract("SATURN", "isBurnable", []);
// true

getMaxSupply()

READ
getMaxSupply(): number

Token-wide supply cap: 0, meaning no cap. Each certificate is minted in its own series with a max supply of 1.

Returns
number — 0 (no cap).
What to expect
Never reverts. Fixed token property.
Example
const v = await readContract("SATURN", "getMaxSupply", []);
// 0

getOwner()

READ
getOwner(): address

Owner of the SATURN token contract: the wallet allowed to upgrade it (onUpgrade) and to infuse certificates (onInfuse). Today it is the same wallet as saturnadmin.getAdmin() on mainnet and devnet, but it is stored separately: saturnadmin.updateAdmin does not change it.

Returns
address — SATURN owner wallet.
What to expect
Never reverts.
Example
const owner = await readContract("SATURN", "getOwner", []);

Admin & Internal

mintPoolCertificate()

WRITE
mintPoolCertificate(from: address, poolId: number, tokenA: string, tokenB: string)

Internal: only saturnliquidity can call this (createPool). Creates a new series with max supply 1 and mints one certificate to `from`. ROM: name "Saturn v4.1 - Pool #<poolId> <tokenA>/<tokenB> - <rarity>", the ownership description, poolId, tokenA, tokenB, imageURL, infoURL, royalties 0. Records the id both ways in saturnpools (setPoolNftId: getPoolNftId / getNftPoolId) and adds 1 to getTotalNftMinted().

Parameters
NameTypeDescription
fromaddressPool creator; receives the certificate.
poolIdnumberThe new pool.
tokenAstringPool token A.
tokenBstringPool token B.
What to expect
Reverts: "Only liquidity manager". Emits CertificateMinted(poolId, nftId, tokenA, tokenB, rarity). This mint is why createPool burns about 2,500 KCAL of gas.

burnPoolCertificate()

WRITE
burnPoolCertificate(from: address, poolId: number)

Internal: only saturnliquidity can call this (removePool). Burns the pool's certificate if `from` holds it and subtracts 1 from getTotalNftMinted(); if the certificate is elsewhere or already gone it does nothing, so removal never gets stuck on it.

Parameters
NameTypeDescription
fromaddressThe wallet removing the pool.
poolIdnumberPool being removed.
What to expect
Reverts: "Only liquidity manager". Emits CertificateBurned(poolId, nftId) only when it burns. The burn trigger refuses a burned pool's certificate, but removePool never gets that far for a burned pool.
Core DEX · Contract #7

SaturnRewards

saturnrewards saturnrewards-4.1.5

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 multiplied by the seconds remaining in the campaign, and that liquidity-seconds weight fixes the provider's proportional share of the reward pot — enrolling earlier earns more, later changes to the pool's depth do not matter. Enrolled pools are locked (their fee cannot be changed and they cannot be removed) until the provider claims or withdraws, and a pool under a financial product cannot enroll. The campaign creator can reclaim any unclaimed rewards 30 days after the end time; after that, pools that never claimed get nothing and unlock with withdrawFromCampaign(). Each enrollment settles once (4.1.5): after a claim, a second claim or a withdraw reverts "Already claimed".

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: "Token does not exist: <symbol>" (rewardToken), "Reward must be > 0", "Min duration: 1 day (86400s)", "Max duration: 1 year", or "No pools exist for this pair". The reward must be a fungible token; if the wallet holds less than rewardAmount the chain's token transfer fails. The campaign takes id getNextCampaignId() and runs from now to now + durationSeconds. Event decoding: CampaignCreated.startTime is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrewards", "createCampaign",
    [from, "SOUL", "KCAL", "SOUL",
      1000000000,   // rewardAmount: 10 SOUL (8 decimals)
      7 * 86400])   // durationSeconds: 7 days
  .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 multiplied by the seconds remaining until the campaign's end time; that liquidity-seconds weight is added to the campaign's total and fixes the provider's share — later changes to the pool's depth do not matter, but enrolling earlier earns more. Enrollment locks the pool (cannot change fee, cannot remove) until the reward is claimed or the pool withdraws. A pool that is under a financial product (bond, rental, option, syndicate, launchpad or loan collateral) cannot enroll.

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", "Pool not active", "Campaign not active", "Campaign already ended", "Pool pair does not match campaign pair", "Already enrolled", "Pool has no liquidity", or "Pool has active financial products (bond/rental/option) - cannot enroll".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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. Pays the provider's share of the reward pot: share = poolWeight × totalReward / totalLocked, in raw reward units, rounded down. poolWeight and totalLocked are the liquidity-seconds from getPoolCampaignLiquidity() and getCampaignTotalLocked(). The share is capped at what is still unclaimed (totalReward − getCampaignRewardClaimed()). The claim then removes this campaign's lock from the pool. Each enrollment settles once (4.1.5): the claim is recorded even when the capped share is 0, so it cannot be repeated. After endCampaign() nothing is left, so a late claim pays 0 (it still removes the lock); withdrawFromCampaign() is the cleaner way to unlock the pool.

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" (this enrollment is already settled), "Campaign not ended yet", "No locked liquidity", or "Reward rounds to zero" (the share before the cap is 0). getPoolEnrolledInCampaign() stays 1 after a claim. Preview the payout with getExpectedReward(), but note it ignores the cap.
Example
const expected = await readContract("saturnrewards", "getExpectedReward", [poolId, campaignId]);
const active = await readContract("saturnrewards", "getCampaignActive", [campaignId]);
const paid = await readContract("saturnrewards", "getPoolCampaignClaimed", [poolId, campaignId]);
// active 0 = endCampaign() ran and the pot is empty: call withdrawFromCampaign() instead
if (expected > 0 && active == 1 && paid == 0) {
  const tx = ScriptBuilder
    .begin()
    .allowGas(from, null, gasPrice, gasLimit)
    .callContract("saturnrewards", "claimCampaignReward", [from, poolId, campaignId])
    .spendGas(from)
    .endScript();
}

withdrawFromCampaign()

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

Unenrolls the pool and removes this campaign's lock at once. It forfeits the pool's share: its weight is subtracted from getCampaignTotalLocked(), so the pools that remain split the pot. There is no time check: it works before or after the end time, and after endCampaign() it is the clean way to unlock a pool that never claimed (a late claim pays 0). Not allowed once the pool has claimed. While the campaign is still running the pool may enroll again, with a new weight based on its liquidity and the time left.

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". Before endCampaign(), use this only if the provider really wants to give up their share.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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 (2,592,000 s) after the end time. Marks the campaign inactive, sets getCampaignRewardClaimed() to the full reward total, removes the id from getAllCampaignIds() / getActiveCampaignIds(), and sends the unclaimed remainder (totalReward − claimed, raw units) to the creator. Pools that never claimed keep their campaign lock until the provider calls withdrawFromCampaign() or claims (the claim pays 0).

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, null, gasPrice, gasLimit)
  .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. Do not split on every underscore: the pair key contains one ("KCAL_SOUL"). Split on the "_<label>:" markers instead. locked is in liquidity-seconds.

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]);
// "pair:KCAL_SOUL_reward:KCAL_total:10000000000000000_claimed:0_start:1790008570_end:1797784570_active:1_locked:53450098582043255861"
const f = Object.fromEntries(info.split(/_(?=[a-z]+:)/).map((kv) => {
  const i = kv.indexOf(":");
  return [kv.slice(0, i), kv.slice(i + 1)];
}));
// f.pair = "KCAL_SOUL", f.total = "10000000000000000"

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 enrollment weights (liquidity-seconds) of every pool currently enrolled in the campaign. Divide a pool's getPoolCampaignLiquidity() by this to get its share.

Parameters
NameTypeDescription
campaignIdnumberCampaign ID.
Returns
number — Sum of enrolled liquidity-seconds weights.
What to expect
Grows as pools enroll and shrinks when a pool withdraws (before or after the end time). A claim does not change it. 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 the ids of campaigns that have not been ended. endCampaign() removes the id, so in practice it matches getActiveCampaignIds(). Walk 1 .. getNextCampaignId() − 1 to reach ended campaigns.

Returns
number* — Iterable of campaign IDs.
What to expect
Includes campaigns past their end time that the creator has not ended; compare getCampaignEndTime() with now to separate enrollable from claimable.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnrewards", "getAllCampaignIds", [])
  .endScript();

getCampaignCreator()

READ
getCampaignCreator(campaignId: number): address

Wallet that funded the campaign — the only address allowed to call endCampaign().

Parameters
NameTypeDescription
campaignIdnumberCampaign to inspect.
Returns
address — Creator address.
What to expect
Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
Example
const creator = await readContract("saturnrewards", "getCampaignCreator", [campaignId]);

getCampaignRewardClaimed()

READ
getCampaignRewardClaimed(campaignId: number): number

Raw amount of the reward token paid out to providers so far. When the creator ends the campaign the value is set to the full reward total (the unclaimed remainder went back to the creator).

Parameters
NameTypeDescription
campaignIdnumberCampaign to inspect.
Returns
number — Raw reward units already distributed.
What to expect
Never reverts.
Example
const claimed = await readContract("saturnrewards", "getCampaignRewardClaimed", [campaignId]);

getCampaignStartTime()

READ
getCampaignStartTime(campaignId: number): number

Unix timestamp at which the campaign was created; enrollment weights are measured against the window between now and getCampaignEndTime().

Parameters
NameTypeDescription
campaignIdnumberCampaign to inspect.
Returns
number — Unix seconds.
What to expect
Never reverts.
Example
const start = await readContract("saturnrewards", "getCampaignStartTime", [campaignId]);

getCampaignPairKey()

READ
getCampaignPairKey(campaignId: number): string

Canonical pair key ("A_B", tokens sorted) the campaign rewards. Pools whose saturnpools.getPoolPairKey() equals it are eligible to enroll.

Parameters
NameTypeDescription
campaignIdnumberCampaign to inspect.
Returns
string — Canonical pair key.
What to expect
Never reverts.
Example
const pairKey = await readContract("saturnrewards", "getCampaignPairKey", [campaignId]);

getActiveCampaignIds()

READ
getActiveCampaignIds(): number*

Yields the ids of campaigns whose active flag is still 1 (not yet ended by the creator). Note that a campaign past its end time stays active until endCampaign() is called, so check getCampaignEndTime() to separate "enrollable" from "claimable".

Returns
number* — Stream of campaign ids.
What to expect
Never reverts; empty when no campaign is active.
Example
const ids = await readContract("saturnrewards", "getActiveCampaignIds", []);

getActiveCampaignsData()

READ
getActiveCampaignsData(): string*

One pipe-delimited row per active campaign: campaignId|pairKey|rewardToken|rewardTotal|rewardClaimed|startTime|endTime|active|totalLocked. totalLocked is the liquidity-seconds sum. Builds a campaigns table in one round-trip.

Returns
string* — Stream of "campaignId|pairKey|rewardToken|rewardTotal|rewardClaimed|startTime|endTime|active|totalLocked" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnrewards", "getActiveCampaignsData", []);
// "2|KCAL_SOUL|SOUL|100000000000|0|1757700000|1760292000|1|0"

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnrewards-4.1.5". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnrewards-4.1.5".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnrewards", "getContractVersion", []);
// "saturnrewards-4.1.5"

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
Set back to 0 only by withdrawFromCampaign(). A claim leaves it at 1, so it does not tell you whether the pool has claimed.
Example
const enrolled = await readContract("saturnrewards", "getPoolEnrolledInCampaign", [poolId, campaignId]);

getPoolCampaignLiquidity()

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

Returns the enrollment weight of this pool: its scaled liquidity at enrollment multiplied by the seconds that remained in the campaign (liquidity-seconds). 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 — Liquidity-seconds weight fixed at enrollment.
What to expect
0 if the pool never enrolled or has withdrawn. Keeps its value after a claim.
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 means the pool has claimed. 0 does not prove it can still claim: a claim made after endCampaign() pays and records 0 but still settles the enrollment, and there is no getter for that settled flag. If getCampaignActive() is 0, offer withdrawFromCampaign() instead of a claim.
Example
const claimed = await readContract("saturnrewards", "getPoolCampaignClaimed", [poolId, campaignId]);

getExpectedReward()

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

Computes poolWeight × totalReward / totalLocked in raw reward units, using the current totalLocked. The number will drift if other pools enroll or withdraw — always requery just before submitting a claim. It does not apply the claim's cap: it keeps returning the share after the pool has claimed, and after endCampaign(), when the real payout is 0.

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 saturnrouter-4.1.1

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 a read — no witness, no gas — with one catch: getBestPoolForSwapV2() and the getMinRawFor*() helpers first call saturnpools.computeAndStoreScaleFactor(), so reading them for a token whose scale was never cached (saturnpools.getScaleFactor() = 0, i.e. a token that never had a pool) fails with a DataFees / FeeEscrow error instead of returning a number. There is no on-chain quote view: take the pool from getBestPoolForSwapV2(), then compute the output from getPoolFullInfo() with the swap engine's formula (see SaturnSwap.swap).

Swap Routing

getBestPoolForSwapV2()

READ
getBestPoolForSwapV2(tokenIn: string, tokenOut: string, amountIn: number, maxPools: number): number

Scans up to maxPools pools registered for the pair (in registration order) and returns the poolId that would give the highest net output for amountIn after that pool's fee. Inactive pools and pools with empty reserves score 0 and are skipped. This is the first call your swap flow should make — pass the result into SaturnSwap.swap(). It returns the pool id only, not the output: scoring uses the swap engine's math in 8-decimal scaled units, out = (in − in × fee / 10000) × reserveOut / (reserveIn + in − in × fee / 10000), so compute the amount yourself the same way. maxPools bounds the gas of the scan; pass getPoolCountForPair() to consider every pool (hard cap 100).

Parameters
NameTypeDescription
tokenInstringSymbol of the token being sold.
tokenOutstringSymbol of the token being bought.
amountInnumberRaw amount the user wants to sell.
maxPoolsnumberHow many pools of the pair to score, 1..100.
Returns
number — Pool ID with the best output, or 0 if no scanned pool can fill the trade.
What to expect
Reverts with "maxPools must be > 0" or "maxPools upper cap is 100". Returns 0 when the pair has no pool with reserves. Read through invokeRawScript it fails with a DataFees / FeeEscrow error for a token whose scale is not cached yet (saturnpools.getScaleFactor = 0); every token that has had a pool is cached. Pools beyond the first maxPools registrations are not considered — with more than 100 pools for one pair, score the rest yourself with getPoolIdForPairAtIndex(), getPoolFullInfo() and the swap formula above. getPoolPrice() ignores the fee and the trade size.
Example
const count = await readContract("saturnrouter", "getPoolCountForPair", ["SOUL", "KCAL"]);
const poolId = await readContract("saturnrouter", "getBestPoolForSwapV2",
  ["SOUL", "KCAL", 10000000000, Math.max(1, Math.min(count, 100))]); // maxPools 0 reverts
if (poolId === 0) throw new Error("No liquidity available");

getBestPoolForSwap()

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

DEPRECATED — always reverts with "Deprecated in 4.1.0: use getBestPoolForSwapV2(tokenIn, tokenOut, amountIn, maxPools)". Kept in the ABI for compatibility only.

Parameters
NameTypeDescription
tokenInstringIgnored.
tokenOutstringIgnored.
amountInnumberIgnored.
Returns
number — Never returns — the call reverts.
What to expect
Always reverts. Call getBestPoolForSwapV2() instead.

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
Reverts with "Token not in pool" when tokenIn is neither of the pool's tokens. No fee or slippage adjustment — this is the marginal price before any trade. Both reserves are 8-decimal scaled, so the ratio is the human price whatever the tokens' decimals (KCAL included). Exception, devnet only: pools created before the scale cache fix keep reserves in the older unit (the six KCAL pools #15, #19, #20, #78, #84 and #85, and AMIPOLAKAO, whose getScaleDivisor is 0), so their price is off by 10^(decimals − 8). Mainnet has no such pool.
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, campaign lock count and financial lock count, packed into a single underscore-delimited string. The obsolete pawned field was dropped in 4.1.1; read saturnpools.getPoolPawned() to see whether a pool backs a loan.

Parameters
NameTypeDescription
poolIdnumberPool ID.
Returns
string — Format: "tokenA:<s>_tokenB:<s>_resA:<n>_resB:<n>_fee:<n>_active:<0|1>_campLocks:<n>_finLocks:<n>", reserves in 8-decimal scaled units.
What to expect
One call instead of eight getPool* reads. Mainnet pool 33 returns "tokenA:RA_tokenB:TAZ_resA:472997583555_resB:12690472225382_fee:30_active:1_campLocks:0_finLocks:0".
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]);

getProviderClaimablePair()

READ
getProviderClaimablePair(poolId: number): string

Passthrough to saturnfees.getProviderClaimablePair(): both pending provider balances of a pool packed as "tokenA:<sym>_pendingA:<n>_tokenB:<sym>_pendingB:<n>" (scaled units).

Parameters
NameTypeDescription
poolIdnumberPool to inspect.
Returns
string — Packed pending balances.
What to expect
Never reverts.
Example
const pending = await readContract("saturnrouter", "getProviderClaimablePair", [poolId]);

getPoolProvider()

READ
getPoolProvider(poolId: number): address

Passthrough to saturnpools.getPoolProvider(): the wallet (or contract, for syndicate and launchpad pools) recorded as the pool's provider. For ownership checks prefer SATURN.holdsPoolCertificate(), which follows the NFT.

Parameters
NameTypeDescription
poolIdnumberPool to inspect.
Returns
address — Provider address.
What to expect
Reverts for a pool id that was never created: the empty provider slot cannot be read as an address.
Example
const provider = await readContract("saturnrouter", "getPoolProvider", [poolId]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnrouter-4.1.1". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnrouter-4.1.1".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnrouter", "getContractVersion", []);
// "saturnrouter-4.1.1"

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. As a read it reverts ("DataFees: FeeEscrow failure") for a token whose scale is not cached yet (saturnpools.getScaleFactor = 0: a token that has never been in a pool), because caching the scale writes storage and a read has no data-fee budget. Send saturnpools.computeAndStoreScaleFactor(token) first, or compute 10^decimals x the admin minimum / 1e8 yourself. Do not fall back to saturnpools.getMinRawForToken: for an uncached token it returns the absolute floor (100 raw).
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; saturnswap.swap reverts with "Below minimum swap" under it. Today it is 0.01 token: 1,000,000 raw SOUL (8 decimals), 10,000,000 raw RA or TAZ (9), 100,000,000 raw KCAL (10). Read for a token that never had a pool it fails (see getBestPoolForSwapV2).
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. As a read it reverts ("DataFees: FeeEscrow failure") for a token whose scale is not cached yet (saturnpools.getScaleFactor = 0: a token that has never been in a pool), because caching the scale writes storage and a read has no data-fee budget. Send saturnpools.computeAndStoreScaleFactor(token) first, or compute 10^decimals x the admin minimum / 1e8 yourself. Do not fall back to saturnpools.getMinRawForToken: for an uncached token it returns the absolute floor (100 raw).
Example
const minAdd = await readContract("saturnrouter", "getMinRawForAddLiquidity", ["SOUL"]);

Protocol Config Passthroughs

getScaleFactor()

READ
getScaleFactor(tokenSymbol: string): number

Re-export of SaturnPools.getScaleFactor. scaled = raw × factor / divisor: the factor is 10^(8 − decimals) for tokens with 8 decimals or fewer and 1 above 8, where saturnpools.getScaleDivisor() holds 10^(decimals − 8) (KCAL: factor 1, divisor 100). This router has no divisor passthrough, so read saturnpools.getScaleDivisor or use saturnpools.scaleUp / scaleDown.

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 / holder breakdown in one string. The holder slice is taken only when the swap's input token has stakers in saturnholders; otherwise it stays in the pool as reinvest.

Returns
string — "reinvest:X_provider:Y_admin:Z_holder:H" (today reinvest:60_provider:10_admin:20_holder:10).
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 saturnclpools-4.2.6

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, and since 4.2.6 each side must meet the v4 minimum pool size (saturnadmin.getMinScaledPoolUnits: 100 whole tokens per side live). Also since 4.2.6 only the amount the 8-decimal scaled reserve represents is taken; for a token with more than 8 decimals the sub-unit remainder stays in your wallet. 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 on: "Not authorized", "Token does not exist: <symbol>", "Same token", "amountA must be > 0" / "amountB must be > 0", "priceMin must be > 0", "priceMax must exceed priceMin", "Fee too low" / "Fee too high" (live range 30..3000), "tokenA below the minimum pool size" / "tokenB below the minimum pool size", "Initial price below priceMin" or "Initial price above priceMax". The initial price is scaledB * 1e8 / scaledA. Emits ClPoolCreated with the scaled reserves; the method returns nothing, so read the poolId there. The reentrancy guard is held for the duration; do not call re-entrantly.
Example
// 100 SOUL + 500 KCAL: price 5 KCAL per SOUL, range 4..6
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnclpools", "createClPool", [
    from, "SOUL", "KCAL",
    10000000000,     // amountA: 100 SOUL (8 decimals); live minimum is 100 whole tokens per side
    5000000000000,   // amountB: 500 KCAL (10 decimals)
    400000000,       // priceMin: 4 KCAL per SOUL (× 1e8)
    600000000,       // priceMax: 6 KCAL per SOUL (× 1e8)
    30               // feePer10k: 0.3% (live range 30..3000)
  ])
  .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
You receive the reserves scaled down to raw units plus the raw fees. Reverts on: "Not authorized", "Pool not active" or "Only pool provider". Emits ClPoolRemoved with the exact amounts returned for both tokens plus the fee amounts bundled in.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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. Since 4.2.6 only the tokenA the 8-decimal scaled reserve can represent is taken; for a token with more than 8 decimals the sub-unit remainder stays in your wallet.

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
tokenB taken = scaleDown(scaleUp(amountA) * reserveB / reserveA) in raw units. Reverts on: "Not authorized", "Pool not active", "Only pool provider", "amountA must be > 0", "maxAmountB must be > 0", "Rounds to zero", or "Exceeds max: <requiredB>" (the raw tokenB needed). The exact amounts deposited are logged in the ClLiquidityAdded event.
Example
// SOUL/KCAL pool. Reserves are 8-decimal scaled, so convert with each token's decimals.
const resA = BigInt(await readContract("saturnclpools", "getClPoolReserveA", [poolId]));
const resB = BigInt(await readContract("saturnclpools", "getClPoolReserveB", [poolId]));
const addA = 500000000n;                  // 5 SOUL (8 decimals: raw = scaled)
const scaledB = (addA * resB) / resA;     // tokenB needed, scaled
const rawB = scaledB * 100n;              // KCAL has 10 decimals: raw = scaled × 10^(10-8)
const maxB = rawB + rawB / 100n;          // +1% headroom

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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 (amountIn * feePer10k / 10000, raw) is deducted from amountIn before the AMM math runs, and is accumulated in the pool for the provider to claim later. Since 4.2.6 a swap whose fee rounds to zero is refused, and the sub-unit remainder of the input that the 8-decimal scaled reserve cannot hold goes to the provider with the fee. 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. The whole amount is taken. Must be large enough that amountIn * feePer10k / 10000 >= 1 (at a 0.3% fee: at least 334 raw).
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 on: "Not authorized", "Pool not active", "Amount must be > 0", "Same token", "Token pair mismatch", "Zero reserve in" / "Zero reserve out", "Swap too small: the fee rounds to zero", "Fee consumes input", "Output rounds to zero", "Slippage exceeded", "Cannot drain pool", "Swap would push below priceMin" or "Swap would push above priceMax". Emits ClSwapExecuted (fee is the raw fee including the remainder; newPrice × 1e8). Always quote via getClPoolPrice() and getClPoolInfo() before submitting to set a reasonable minAmountOut.
Example
// Sell 1 SOUL for KCAL on a SOUL/KCAL pool with 1% slippage.
// Reserves are 8-decimal scaled; KCAL has 10 decimals (raw = scaled × 100).
const resA = BigInt(await readContract("saturnclpools", "getClPoolReserveA", [poolId]));
const resB = BigInt(await readContract("saturnclpools", "getClPoolReserveB", [poolId]));
const fee  = BigInt(await readContract("saturnclpools", "getClPoolFeePer10k", [poolId]));
const amountIn = 100000000n;                              // 1 SOUL (8 decimals)
const afterFee = amountIn - (amountIn * fee) / 10000n;
const scaledOut = (afterFee * resB) / (resA + afterFee);  // xy=k output, scaled
const minOut = (scaledOut * 100n * 99n) / 100n;           // raw KCAL, less 1%

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnclpools", "swapClPool", [
    from, poolId, amountIn, "SOUL", "KCAL", minOut
  ])
  .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 on: "Not authorized" or "Only pool provider". It does not revert when nothing has accrued: it then moves nothing and emits ClFeesClaimed with 0 and 0. Emits ClFeesClaimed with feesA and feesB amounts.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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. Mainnet and devnet report "saturnclpools-4.2.6".
What to expect
Never reverts. Pure view.
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:10000000000_resB:50000000000_priceMin:400000000_priceMax:600000000_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.
What to expect
Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.

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 reserve of tokenA in 8-decimal scaled units: divide by 1e8 for whole tokens, or use saturnpools.scaleDown(value, tokenA) for raw units.

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" (saturnpools.getCanonicalPairKey("SOUL", "KCAL") returns "KCAL_SOUL"). 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|P2K...|SOUL|KCAL|10000000000|50000000000|400000000|600000000|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 saturntwamm-4.2.4

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 sees a series of small trades instead of one large one, and every chunk must meet the owner's price floor (minOutputPerChunk, scaled to the chunk's size since 4.2.4), so a chunk that someone else's swaps push below that price reverts. Output accumulates inside the contract; the owner can claim it at any time, and the chunk that completes the stream pays everything left to the owner automatically. 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 mandatory price floor (it must be greater than 0): the minimum raw output of one on-time chunk (amountIn × minChunkSeconds / durationSeconds). Every chunk must pay the same per unit of input, so a late chunk twice that size needs twice the output and a smaller last chunk proportionally less: floor = minOutputPerChunk × chunkIn × durationSeconds / (amountIn × minChunkSeconds), at least 1. Size it from the pool's quote for one on-time chunk minus the slippage you accept. The floor applies to the chunk's output before the executor's bounty is taken. Placement also refuses a stream whose on-time chunk is below the swap minimum (saturnrouter.getMinRawForSwap(tokenIn)).

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 tokenOut for one on-time chunk (the stream's price floor), scaled to each chunk's actual size; must be > 0.
bountyPer10knumberBasis-points share of each chunk's gross output paid to the executor (0–500, i.e. 0–5%).
What to expect
Reverts if from did not sign ("Not authorized"); amountIn == 0; durationSeconds < 60 or > 31,536,000; minChunkSeconds < 60 or > durationSeconds; bountyPer10k < 0 or > 500; minOutputPerChunk == 0 ("Explicit slippage required: minOutputPerChunk must be > 0"); tokenIn == tokenOut; the target pool is not active; tokenIn/tokenOut are not both in the pool's pair; or amountIn × minChunkSeconds / durationSeconds is below the swap minimum ("Each chunk would be below the minimum swap"). The caller must hold amountIn ("Insufficient balance"). Emits StreamPlaced; new streams get floorMode 1 (price floor). Event decoding: StreamPlaced.startTime is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
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
// Floor: the pool's quote for one on-time chunk (100 × 60 / 3600 = 1.66666666 SOUL) minus the slippage you accept
const minOut     = 350000000000n; // 35 KCAL (10 decimals) per on-time chunk, scaled to each chunk's size. Mainnet pool 12 quoted 36.16 KCAL on 2026-09-28; other SOUL/KCAL pools pay less, so quote your own pool
const bounty     = 100;          // 1% of each chunk's output to whoever fires it

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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 = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturntwamm", "cancelStream", [from, streamId])
  .spendGas(from)
  .endScript();

Chunk Execution (Bounty Path)

executeStreamingChunk()

WRITE
executeStreamingChunk(from: address, streamId: number)

Executes the next chunk of an active stream. Permissionless: any signer can call it once minChunkSeconds have passed since the last chunk (the first chunk is due minChunkSeconds after placement), and the caller earns the bounty. The chunk is everything owed since the last one, floor(amountInTotal × (now − startTime) / durationSeconds) − streamedSoFar, and from endTime on the whole remainder. If what would be left afterwards is below the swap minimum (saturnrouter.getMinRawForSwap(tokenIn)), it is swept into this chunk, so no stream ends with stuck dust. The chunk goes from escrow to saturnliquidity and through saturnswap.swapFromContract on the stream's pool with the chunk's floor as minAmountOut (floorMode 1: minOutputPerChunk × chunkIn × durationSeconds / (amountInTotal × minChunkSeconds), at least 1; floorMode 0, streams placed before 4.2.4: minOutputPerChunk). The executor receives bountyPer10k / 10,000 of the chunk's output in tokenOut, paid immediately; the rest accumulates for the owner. The chunk that empties the stream sets status 1 (completed), removes it from the active list and pays the owner all accumulated output in the same transaction (emits StreamClaimed).

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 on "Unknown stream" (id 0 or never assigned), "Stream not active", "Chunk pacing not elapsed" (now < lastExecutionTime + minChunkSeconds), "No streamable amount yet", "Chunk below minimum swap: wait for more to accrue", "Slippage exceeded" (the pool pays less than the chunk's floor), "Pool not active" or "Not authorized" (from must sign). A failed call costs only gas. Emits StreamChunkExecuted(streamId, chunkIn, chunkOut, bounty, elapsed, streamedSoFar). Check getStreamLastExecutionTime() + getStreamMinChunkSeconds() and quote the chunk before sending.
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 + 3) { // block time trails the clock: keep a few seconds of margin
  // quote the chunk first: it must clear the swap minimum and its floor
  const tx = ScriptBuilder
    .begin()
    .allowGas(executor, null, gasPrice, gasLimit)
    .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. Since 4.2.4 the chunk that completes a stream pays the owner automatically and cancelStream pays out the accumulated output, so claims matter while a stream runs; a stream completed under 4.2.3 may still hold output to claim.

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 = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturntwamm", "claimStreamingOutput", [from, streamId])
  .spendGas(from)
  .endScript();

Stream Views

getContractVersion()

READ
getContractVersion(): string

Returns the current TWAMM contract version string. Mainnet and devnet report saturntwamm-4.2.4; the floor and dust rules on this page need 4.2.4.

Returns
string — Version string, e.g. "saturntwamm-4.2.4".
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.
What to expect
Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.

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. Set to zero when claimStreamingOutput is called, and when the completing chunk or cancelStream pays the owner.

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 ends (startTime + durationSeconds; there is no separate duration getter). From then on the next chunk is the whole remainder, still subject to pacing, the swap minimum and the floor.

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 (the placement time until the first chunk). 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 minOutputPerChunk as placed: the minimum raw tokenOut for one on-time chunk. For floorMode 1 streams (placed on 4.2.4) it is a price, scaled to each chunk's size; for floorMode 0 streams it is a fixed amount per chunk. Placement requires it to be > 0.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — Raw tokenOut floor for one on-time chunk.

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. An id never assigned (including 0) also reads 0, so check 0 < streamId < getNextStreamId() or use the active-stream views.

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

getStreamFloorMode()

READ
getStreamFloorMode(streamId: number): number

How the stream's floor works. 1 = minOutputPerChunk is a price (streams placed on 4.2.4): a chunk of chunkIn raw must pay at least minOutputPerChunk × chunkIn × durationSeconds / (amountInTotal × minChunkSeconds), at least 1. 0 = a fixed minOutputPerChunk per chunk (streams placed before the 4.2.4 upgrade). A keeper needs it to compute the floor a chunk must clear; durationSeconds = getStreamEndTime − getStreamStartTime.

Parameters
NameTypeDescription
streamIdnumberStream ID.
Returns
number — 1 = price floor, 0 = fixed floor.
Example
const mode = await readContract("saturntwamm", "getStreamFloorMode", [streamId]);

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. A keeper still needs per-stream reads the rows do not carry: getStreamStartTime, getStreamLastExecutionTime, getStreamMinChunkSeconds, getStreamAmountStreamedSoFar, getStreamMinOutputPerChunk, getStreamBountyPer10k and getStreamFloorMode (several calls fit in one invokeRawScript). 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 saturnflash-4.2.4

Flash arbitrage with borrowed protocol liquidity: the executor needs no capital. executeFlashArb borrows amountIn of tokenStart from saturnliquidity (the custody balance that holds every pool's reserves), swaps it tokenStart → tokenMid on poolIdBuy and tokenMid → tokenStart on poolIdSell in the same transaction, returns the principal to saturnliquidity, pays the flash fee (5 per 10,000 of amountIn by default, admin-tunable 1–100) to the protocol admin wallet and sends the net profit to the executor. If the round trip does not return more than the principal plus the fee (and at least minNetProfit), the whole transaction reverts and only gas is spent. TOMB requires literal call targets, so Aave-style flash loans with a borrower callback are not possible: the only thing this contract does with borrowed funds is this two-pool route between Saturn v4 pools of the same pair. Build bots that call executeFlashArb when the price gap between two such pools exceeds both pools' swap fees plus the flash fee. To arbitrage with your own tokens and no flash fee use saturnarb; to borrow staked tokens instead use saturnstakearb.

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), returns amountIn to saturnliquidity, pays flashFee = amountIn × flashFeePer10k / 10000 to the protocol admin wallet (saturnadmin.getAdmin()) and transfers netProfit = finalAmount − amountIn − flashFee to from. The executor puts up no capital: the transaction reverts atomically if finalAmount ≤ amountIn, if the gross profit does not exceed the flash fee, or if netProfit < minNetProfit, so a failed attempt costs only gas. tokenMid is the other token of poolIdBuy; poolIdSell must be a different active pool holding the same two tokens (in either order). Both legs are ordinary saturnswap contract swaps, so each pays its pool's full swap fee (currently provider 10%, admin 20%, holders 10% when the input token has stakers, the rest reinvested into that pool) before the profit check. Only saturnpools (v4) pools can be used, not saturnclpools ranges or v3 SATRN pools.

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 amount of tokenStart to borrow. Must be > 0, at most getMaxBorrowable(tokenStart), big enough that the flash fee is at least 1 raw unit (2,000 raw at the default 5 per 10,000) and that each leg clears saturnrouter.getMinRawForSwap() for its input token (0.01 token today).
minNetProfitnumberMinimum acceptable net profit in raw units of tokenStart after deducting the flash fee. Checked as netProfit >= minNetProfit; 0 accepts any profit of at least 1 raw unit. Gas is paid separately in KCAL, so convert your gas cost into tokenStart if minNetProfit should cover it.
Returns
number — Net profit in raw units of tokenStart delivered to the executor.
What to expect
Reverts with "Not authorized" (from did not sign), "Flash arb is paused", "Amount must be > 0", "Same pool", "Token does not exist: <symbol>", "tokenStart not in poolBuy pair", "tokenStart not in poolSell pair", "tokenMid not in poolSell pair", "Flash fee rounds to zero - amount too small", "Insufficient liquidity for flash arb", any saturnswap revert on either leg ("Pool not active", "Below minimum swap", "Output rounds to zero", "Admin fee rounds to zero", "Cannot drain pool"), then "No arbitrage profit" (finalAmount ≤ amountIn), "Profit insufficient to cover flash fee" or "Net profit below minimum". A nested call for the same address fails with "Reentrancy detected". On revert every state change unwinds: nothing is borrowed, no fee is charged, only gas is spent. On success it emits FlashArbExecuted (poolIdBuy, poolIdSell, tokenStart, tokenMid, amountIn, midAmount, finalAmount, flashFee, netProfit) and adds to getTotalFlashArbs, getTotalFlashFeesCollected and the executor's count and profit.
Example
// Pre-flight reads (free): paused? enough to borrow? fee?
const enabled = await readContract("saturnflash", "getFlashEnabled", []);
const max = await readContract("saturnflash", "getMaxBorrowable", ["SOUL"]);
const amountIn = 10000000000; // 100 SOUL (8 decimals), sized by simulating both pools
const fee = await readContract("saturnflash", "quoteFlashFee", [amountIn]); // 5000000 = 0.05 SOUL at 5/10k
if (enabled !== 1 || amountIn > max) throw new Error("cannot flash-borrow now");

const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnflash", "executeFlashArb", [
    from,
    poolIdBuy,     // leg 1: SOUL -> tokenMid here (tokenMid is cheap in SOUL)
    poolIdSell,    // leg 2: tokenMid -> SOUL here (same pair, tokenMid dearer)
    "SOUL",        // tokenStart: borrowed, repaid and paid out in SOUL
    amountIn,      // 100 SOUL borrowed from saturnliquidity
    1000000        // minNetProfit: 0.01 SOUL after the flash fee, else revert
  ])
  .spendGas(from)
  .endScript();
// returns netProfit in raw SOUL; also emitted in FlashArbExecuted

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 when amount × flashFeePer10k < 10000 (below 2,000 raw at the default rate); executeFlashArb refuses such an amount with "Flash fee rounds to zero - amount too small".
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 saturnliquidity's whole raw balance of tokenSymbol (every pool's reserves plus unclaimed provider fees and holder rewards), which is the ceiling executeFlashArb checks amountIn against. It is not a per-pool limit and not a trade size: the profitable amountIn is set by the reserves of the two pools you route through and is usually far smaller. Check amountIn against it to rule out "Insufficient liquidity for flash arb".

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"]);
if (amountIn > max) throw new Error("not enough SOUL in saturnliquidity"); // size amountIn from the two pools' reserves, not from max

getFlashFeePer10k()

READ
getFlashFeePer10k(): number

Returns the current flash fee rate in basis points out of 10,000 (e.g. 5 = 0.05%). The fee is amountIn × rate / 10000, taken out of the round trip's gross profit and paid to the protocol admin wallet; the admin can set the rate between 1 and 100.

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. saturnflash holds tokens only inside an executeFlashArb call, so its balance is normally 0; you never send it tokens and there is nothing to approve.

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 paid to the protocol admin wallet across all successful executeFlashArb calls. Raw amounts of different tokenStart symbols are added together, so this is an activity counter, not a value in one token.

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 (after the flash fee, before gas) paid to an executor across all successful executeFlashArb calls. Raw amounts of different tokenStart symbols are added together, so keep per-token profit from your own FlashArbExecuted events.

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]);

Admin & Internal

updateFlashFee()

WRITE
updateFlashFee(newFeePer10k: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must sign. Sets the flash fee that executeFlashArb charges on amountIn, per 10,000: from 1 (0.01%) to 100 (1%). The constructor default is 5 (0.05%). The fee goes to the admin wallet. Emits FlashFeeUpdated. Read the current value with getFlashFeePer10k.

Parameters
NameTypeDescription
newFeePer10knumberNew flash fee per 10,000, 1 to 100.
What to expect
Reverts on: "Only admin", "Min fee: 0.01% (1 per 10k)", "Max fee: 1% (100 per 10k)", "Reentrancy detected" (the admin's guard is already held).

setFlashEnabled()

WRITE
setFlashEnabled(enabled: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must sign. Emergency switch: 0 pauses executeFlashArb (it then reverts with "Flash arb is paused"), 1 turns it back on. Views keep working. Emits FlashEnabledChanged. Read the current value with getFlashEnabled.

Parameters
NameTypeDescription
enablednumber1 = enabled, 0 = paused.
What to expect
Reverts on: "Only admin", "enabled must be 0 or 1", "Reentrancy detected" (the admin's guard is already held).
Advanced Pools & Capital · Contract #21

SaturnHolders

saturnholders saturnholders-4.4.2

MasterChef-style stake-to-earn vault. Stakers deposit a token and earn that same token from two sources, both credited to one per-token accumulator: (1) the holder slice of swap fees: saturnswap calls accrueHolderFee on every swap whose input token has stakers (share = saturnadmin.getHolderPct(), 10% of the pool fee on mainnet and devnet today; when nobody stakes the input token the slice stays in the pool as reinvest), and (2) half of the profit of every saturnstakearb.executeArb that borrows this token's idle stake (flashLendStake / settleArbLoan). Rewards are paid in the staked token (stake SOUL, earn SOUL from swaps that sell SOUL) out of saturnliquidity, and are collected with claim() or automatically on stake() and unstake(); swap-fee and arbitrage rewards come out in the same claim. Staked tokens sit at this contract and cannot move while staked, which keeps the claim math safe from wash-claims. A stake-arb loan opens and closes inside one transaction and settleArbLoan re-checks that this contract still holds at least getTotalStaked, so arbitrage never shrinks a stake or blocks an unstake; only a saturntaz pledge lock (getPledgeLocked) can hold part of a stake.

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. It only has to exist on Phantasma (saturnpools.validateTokenSymbol checks Token.exists); it earns swap fees from Saturn v4 swaps that sell it and profit from stake arbitrage in its pools.
amountnumberRaw-unit amount to deposit. Must be > 0.
What to expect
Reverts on: "Not authorized" (from did not sign), "Amount must be > 0", "Token does not exist: <symbol>", "Reentrancy detected" (from is already inside another Saturn call), or a failed transfer when from holds less than amount. Pending rewards of an existing position are paid out first (HolderStaked reports them as pendingPaid). The first stake of a symbol adds it to getStakedTokenSymbols.
Example
const from = "S3...userAddress";
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnholders", "stake", [
    from,
    "SOUL",
    10000000000    // amount: 100 SOUL (8 decimals)
  ])
  .spendGas(from)
  .endScript();
// about 0.025 KCAL of gas measured on devnet

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. Any part of the stake locked by a saturntaz pledge (getPledgeLocked) cannot be unstaked until saturntaz calls unlockPledge. A saturnstakearb loan never blocks an unstake: it opens and closes inside one transaction and the principal is re-checked before that transaction commits.

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 on: "Not authorized" (from did not sign), "Amount must be > 0", "Insufficient stake" (stake < amount), "Insufficient unlocked stake - some is pledged via saturntaz" (amount > stake − pledgeLocked), or "Reentrancy detected". Pending rewards are always settled first — they are never forfeited.
Example
const from = "S3...userAddress";
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnholders", "unstake", [
    from,
    "SOUL",
    5000000000     // amount: 50 SOUL (8 decimals), <= getStakedAmount - getPledgeLocked
  ])
  .spendGas(from)
  .endScript();
// pending rewards are paid in the same transaction

Claim Rewards

claim()

WRITE
claim(from: address, tokenSymbol: string)

Pays out all pending rewards for from's tokenSymbol stake without altering the stake balance. Rewards (the swap-fee slice and the saturnstakearb profit share, both in tokenSymbol) 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 on: "Not authorized" (from did not sign), "No stake" (from has no stake in tokenSymbol) or "Reentrancy detected". If pending is zero, the call succeeds silently (no transfer) and the bookmark is still updated.
Example
const from = "S3...userAddress";
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnholders", "claim", [from, "SOUL"])
  .spendGas(from)
  .endScript();
// check getPendingRewards(from, "SOUL") first; 0 pending still succeeds

Swap-Fee Accrual (saturnswap only)

accrueHolderFee()

WRITE
accrueHolderFee(tokenSymbol: string, scaledFee: number)

Contract-only hook: accepts calls only from saturnswap, which calls it on every swap, passing the input token and the holder slice of that swap's fee (in saturnpools' 8-decimal scaled units). It converts the fee to raw units (saturnpools.scaleDown) and raises the token's accumulator by realFee × 1e12 / getTotalStaked(tokenSymbol), so every staker of tokenSymbol earns pro-rata; it moves no tokens (the fee itself stays at saturnliquidity). saturnswap only takes a holder slice when getTotalStaked(tokenIn) > 0, at saturnadmin.getHolderPct() percent of the pool fee (10% today). Documented so bots and indexers can read the HolderAccrued event; a wallet or another contract cannot call it.

Parameters
NameTypeDescription
tokenSymbolstringThe swap's input token; its stakers are credited.
scaledFeenumberHolder slice of the swap fee in 8-decimal scaled units.
What to expect
Reverts with "Only swap engine" for any caller except saturnswap. Returns without effect when scaledFee is 0, nothing is staked, or the fee rounds to 0 raw units. Otherwise emits HolderAccrued (tokenSymbol, scaledFee, realFee, totalStake) and adds realFee to getLifetimeAccrued.

Pledge Lock (saturntaz)

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;

lockForPledge()

WRITE
lockForPledge(user: address, tokenSymbol: string, amount: number)

Contract-only hook: accepts calls only from saturntaz (Saturn Lending's non-custodial RA pledge). Locks amount of user's staked tokenSymbol against unstaking. The tokens stay staked and keep earning; only unstake() is limited to stake − pledgeLocked. Wallets pledge through saturntaz, not here.

Parameters
NameTypeDescription
useraddressStaker whose stake is locked.
tokenSymbolstringStaked token (RA in practice).
amountnumberRaw amount to lock. Must be > 0.
What to expect
Reverts on: "Only saturntaz", "Amount must be > 0", or "Pledge would exceed unlocked stake" (current lock + amount > stake).

unlockPledge()

WRITE
unlockPledge(user: address, tokenSymbol: string, amount: number)

Contract-only hook: accepts calls only from saturntaz, which calls it when a pledge is withdrawn after its window. Reduces user's pledge-locked amount so it can be unstaked again.

Parameters
NameTypeDescription
useraddressStaker whose lock is released.
tokenSymbolstringStaked token.
amountnumberRaw amount to unlock. Must be > 0.
What to expect
Reverts on: "Only saturntaz", "Amount must be > 0", or "Unlock exceeds pledge-locked amount".

Stake-Arb Hooks (saturnstakearb only)

flashLendStake()

WRITE
flashLendStake(borrower: address, tokenSymbol: string, amount: number)

Contract-only hook: accepts calls only from saturnstakearb, as step 1 of executeArb. Marks a loan open for tokenSymbol (getLoanOpen = 1) and transfers amount of the staked tokens to borrower (saturnstakearb passes its own address). Staker balances and getTotalStaked do not change. A bot never calls this directly; it calls saturnstakearb.executeArb, which must end with settleArbLoan in the same transaction.

Parameters
NameTypeDescription
borroweraddressReceiver of the borrowed stake (saturnstakearb's own address).
tokenSymbolstringStaked token to lend.
amountnumberRaw amount. Must be > 0 and ≤ getTotalStaked(tokenSymbol).
What to expect
Reverts on: "Only stake-arb executor" (any caller except saturnstakearb), "Loan already open" (a second borrow of the same token while the first is still open; executeArb calls one after another are fine), "Amount must be > 0", or "Exceeds staked capital".

settleArbLoan()

WRITE
settleArbLoan(tokenSymbol: string, holderShare: number)

Contract-only hook: accepts calls only from saturnstakearb, as the last step of executeArb. Solvency check: requires this contract's tokenSymbol balance to be ≥ getTotalStaked(tokenSymbol) again, so the borrowed principal must be back in full or the whole transaction reverts. Then credits holderShare (the stakers' half of the arbitrage profit, already sent to saturnliquidity by saturnstakearb) to every staker pro-rata through the same accumulator as swap fees (holderShare × 1e12 / totalStaked), adds it to getLifetimeAccrued, and closes the loan.

Parameters
NameTypeDescription
tokenSymbolstringToken whose loan is closed.
holderSharenumberStakers' profit share in raw units of tokenSymbol.
What to expect
Reverts on: "Only stake-arb executor", "No open loan", or "Principal not restored" (balance < totalStaked). Emits no event of its own; read saturnstakearb's StakeArbExecuted.

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: the swap-fee slice plus the stakers' half of any saturnstakearb profit, as stake × (getAccFeePerToken − getUserRewardDebt) / 1e12. 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 of the reward accumulator, the cap on saturnstakearb.executeArb's amountIn, and the balance settleArbLoan requires this contract to hold again after every stake-arb loan. Pledge-locked stake is included (it can be lent, since it always returns in the same transaction). saturnswap takes the holder slice of a swap fee only when this is > 0 for the input token.

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. Symbols whose stake has since dropped to zero stay listed, so check totalStaked in getStakedTokensData before treating one as an active market (or a stake-arb source).

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 script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnholders", "registerStakedToken", ["LEGACY"])
  .spendGas(from)
  .endScript();
// no witness check: any wallet can pay the gas

getContractVersion()

READ
getContractVersion(): string

Returns the deployed version string for this contract.

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

getContractAddress()

READ
getContractAddress(): address

Returns the on-chain address of this contract. Use it for balance lookups (getAccount). There are no token approvals: stake() moves the tokens with the staker's own signature.

Returns
address — The deployed address of the saturnholders contract.
Example
const addr = await readContract("saturnholders", "getContractAddress", []);
Advanced Pools & Capital · Contract #23

SaturnLpLock

saturnlplock saturnlplock-1.0.0

A pool owner's on-chain promise not to withdraw a v4 pool. lockPool time-locks the pool's liquidity for a set number of seconds; burnPool burns it forever. The pool's provider or the holder of its SATURN certificate calls them (the same wallets saturnliquidity.removePool accepts), and saturnpools enforces the promise: its burn flag and lock end (getPoolBurned, getPoolLockUntil) can only be written by this contract, and saturnpools.clearPoolReserves refuses a burned pool forever ("Pool liquidity is burned - it can never be withdrawn") and a locked pool until its unix time ("Pool liquidity is time-locked until <unix>"), so removePool cannot return the reserves. saturnpools.getPoolWithdrawable reads 0 while either applies. A lock or burn blocks only withdrawal and use as loan collateral (saturndexadapt refuses a pool whose getPoolWithdrawable is 0). Swaps, fee accrual, addLiquidity and fee claims go on, and bonds, rentals and fee options can still be listed on the pool afterwards. A burn also turns the SATURN certificate into the pool's fee key: saturnfees.claimProviderFees pays the burned pool's provider fees only to the certificate holder, the certificate can no longer be destroyed, and the provider can only lower the pool fee. A pool with an active bond, rental, fee option, syndicate, launchpad or loan (saturnpools.getPoolFinancialLockCount > 0) cannot start a new lock, and cannot be burned unless a live lock already covers it. Fees are paid in TAZ (9 decimals) straight to getFeeWallet(). Live on mainnet and devnet: every lockPool call costs 50 TAZ (getLockFee() = 50,000,000,000 raw), extensions included; a burn is free (getBurnFee() = 0); a lock lasts 86,400 s (1 day) to 315,360,000 s (10 years). The lending reference pools (saturndexadapt.getReferencePool: RA/TAZ pool 33 on mainnet, 223 on devnet) are burned. Settings change only through the saturnadmin owner.

Lock & Burn

lockPool()

WRITE
lockPool(from: address, poolId: number, durationSeconds: number)

Time-locks the pool's liquidity until now + durationSeconds (unix seconds, block time). Until then saturnliquidity.removePool reverts with "Pool liquidity is time-locked until <unix>" and the pool cannot back a loan. from must sign and be the pool's provider (saturnpools.getPoolProvider) or hold its SATURN certificate. Every call, extensions included, sends getLockFee() raw TAZ from the caller to getFeeWallet() (50 TAZ today). With no live lock, the pool must carry no bond, rental, fee option, syndicate, launchpad or loan (getPoolFinancialLockCount = 0). With a live lock the call is an extension: the new end, counted from now, must be later than the current end, so a lock can never be shortened, and the financial-product check is skipped. The end is stored in saturnpools (getPoolLockUntil). This contract records the caller (getLockedBy) and time (getLockedAt) of the latest call, adds 1 to getLockCount and lists the pool in getLockedPoolIds.

Parameters
NameTypeDescription
fromaddressThe pool's provider or SATURN certificate holder. Must sign. Pays the lock fee, so it needs getLockFee() raw TAZ.
poolIdnumberAn active v4 pool (saturnpools.getPoolActive = 1) that is not burned.
durationSecondsnumberLock length in seconds, counted from now. getMinLockSeconds() to getMaxLockSeconds() (86,400 = 1 day to 315,360,000 = 10 years today). To extend, pass more than the time left: getLockUntil(poolId) − now + the extra seconds.
What to expect
On success getLockFee() raw TAZ moves from the caller to getFeeWallet(), saturnpools emits PoolLockChanged {poolId, lockCount: <new lock end>, lockType: "timelock"} and this contract emits PoolLocked(from, {poolId, lockUntil, feePaid}). Reverts with "Reentrancy detected", "Not authorized" (from did not sign), "Invalid address", "Pool not active", "Only the pool certificate holder or the original provider", "Lock shorter than the minimum", "Lock longer than the maximum", "Pool liquidity is already burned", "A live lock can only be extended" (the new end is not later than getLockUntil), "Pool has an active bond, rental, option or loan - end it first" (new lock while getPoolFinancialLockCount > 0) or "Insufficient TAZ for the fee". Gas: about 0.027 KCAL (a first lock on mainnet).
Example
// Lock a pool for 90 days. Costs getLockFee() raw TAZ (50 TAZ today), paid by from.
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlplock", "lockPool", [from, poolId,
    7776000])   // durationSeconds: 90 days (86,400 .. 315,360,000)
  .spendGas(from)
  .endScript();
// sign with the provider's or certificate holder's wallet and send

// Extend a live lock by 30 days: the duration counts from now, not from the old end
const until = await readContract("saturnlplock", "getLockUntil", [poolId]);
const now = Math.floor(Date.now() / 1000);
const durationSeconds = (until - now) + 30 * 86400;   // new end must be later than until
// build the same lockPool call with durationSeconds; it pays the full lock fee again

burnPool()

WRITE
burnPool(from: address, poolId: number)

Burns the pool's liquidity forever. There is no undo. saturnpools.getPoolBurned becomes 1 and saturnliquidity.removePool reverts with "Pool liquidity is burned - it can never be withdrawn" from then on. The reserves stay in the pool and keep trading. The SATURN certificate becomes the pool's fee key: saturnfees.claimProviderFees pays the burned pool's provider fees only to the certificate holder, the certificate cannot be destroyed, and saturnpools.updatePoolFee can only lower the fee. from must sign and be the provider or hold the certificate. Pays getBurnFee() TAZ to getFeeWallet() (0 today, so a burn is free). With no live time lock, the pool must carry no bond, rental, fee option, syndicate, launchpad or loan (getPoolFinancialLockCount = 0); with a live lock that check is skipped. The burn takes the pool off getLockedPoolIds and adds it to getBurnedPoolIds; a lock end already stored stays in getLockUntil.

Parameters
NameTypeDescription
fromaddressThe pool's provider or SATURN certificate holder. Must sign. Needs getBurnFee() raw TAZ when that is above 0.
poolIdnumberAn active v4 pool that is not burned yet.
What to expect
On success saturnpools emits PoolLockChanged {poolId, lockCount: 1, lockType: "burned"} and this contract emits PoolBurned(from, {poolId, feePaid}). Reverts with "Reentrancy detected", "Not authorized" (from did not sign), "Invalid address", "Pool not active", "Only the pool certificate holder or the original provider", "Pool liquidity is already burned", "Pool has an active bond, rental, option or loan - end it first" (no live lock and getPoolFinancialLockCount > 0) or "Insufficient TAZ for the fee" (only while getBurnFee() > 0).
Example
// Burn a pool's liquidity forever. This cannot be undone.
const info = await readContract("saturnlplock", "getPoolLockInfo", [poolId]);
if (info.startsWith("burned:1")) throw new Error("already burned");
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlplock", "burnPool", [from, poolId])
  .spendGas(from)
  .endScript();
// sign with the provider's or certificate holder's wallet and send
// afterwards the SATURN certificate is the pool's fee key: whoever holds it claims the fees

releaseExpiredLock()

WRITE
releaseExpiredLock(poolId: number)

Housekeeping that anyone may call; it needs no pool rights, only gas. Takes a pool whose lock has run out off getLockedPoolIds. Withdrawal never needs it: saturnpools compares getPoolLockUntil with the clock, so removePool works as soon as the lock ends. It changes nothing in saturnpools and keeps getLockedBy, getLockedAt and getLockCount. The last pool in the list moves into the freed slot, so the list order changes.

Parameters
NameTypeDescription
poolIdnumberA pool in getLockedPoolIds whose getLockUntil is at or before now.
What to expect
Emits PoolUnlocked(<saturnlplock address>, {poolId, lockUntil}). Reverts with "Pool is not on the lock list" (never locked, already released, or burned) or "Lock has not expired" (getLockUntil > now).
Example
// Tidy expired locks off the list (anyone may; costs only gas)
const ids = await readContract("saturnlplock", "getLockedPoolIds", []);
const now = Math.floor(Date.now() / 1000);
for (const id of ids) {
  if ((await readContract("saturnlplock", "getLockUntil", [id])) > now) continue;
  const tx = ScriptBuilder
    .begin()
    .allowGas(from, null, gasPrice, gasLimit)
    .callContract("saturnlplock", "releaseExpiredLock", [id])
    .spendGas(from)
    .endScript();
  // sign with any wallet and send
}

Per-Pool Views

getPoolLockInfo()

READ
getPoolLockInfo(poolId: number): string

The pool's burn and lock state in one call: "burned:<0|1>|until:<unix>|locked:<0|1>|locks:<n>". burned is getBurned, until is getLockUntil (0 = never locked), locked is getLocked (1 while until is later than now) and locks is getLockCount.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
string — "burned:<0|1>|until:<unix seconds>|locked:<0|1>|locks:<count>".
What to expect
Never reverts. An unknown pool reads "burned:0|until:0|locked:0|locks:0". On mainnet, pool 33 (the lending reference pool) reads "burned:1|until:0|locked:0|locks:0", and pool 22, burned during a live lock, reads "burned:1|until:1962897871|locked:1|locks:1" until that time (2032-03-14).
Example
const info = await readContract("saturnlplock", "getPoolLockInfo", [poolId]);
// "burned:0|until:1798120528|locked:1|locks:1"
const f = Object.fromEntries(info.split("|").map((kv) => kv.split(":")));
const burned = f.burned === "1", locked = f.locked === "1", until = Number(f.until);

getBurned()

READ
getBurned(poolId: number): number

1 when the pool's liquidity is burned, 0 otherwise. Reads saturnpools.getPoolBurned.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = burned (permanent), 0 = not burned.
What to expect
Never reverts. 0 for an unknown pool.
Example
const burned = await readContract("saturnlplock", "getBurned", [poolId]);

getLockUntil()

READ
getLockUntil(poolId: number): number

Unix time the pool's lock ends. Reads saturnpools.getPoolLockUntil. The value stays after the lock ends and after a burn, so compare it with the current time or read getLocked.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Unix seconds. 0 = never locked; a past time = the lock has run out.
What to expect
Never reverts.
Example
const until = await readContract("saturnlplock", "getLockUntil", [poolId]);
const live = until > Math.floor(Date.now() / 1000);

getLocked()

READ
getLocked(poolId: number): number

1 while the pool's time lock is live (getLockUntil later than the block time), 0 otherwise. It ignores burns: a burned pool reads 0 unless it was burned during a live lock. To know whether the pool can be withdrawn, read saturnpools.getPoolWithdrawable.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — 1 = live time lock, 0 = none or expired.
What to expect
Never reverts.
Example
const locked = await readContract("saturnlplock", "getLocked", [poolId]);

getLockedBy()

READ
getLockedBy(poolId: number): address

The wallet that made the pool's latest lockPool call (first lock or extension). Kept after the lock ends or is released.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
address — Address of the latest locker.
What to expect
Reverts with the VM error "Invalid cast: expected bytes, got None" for a pool that was never locked. Read it only when getLockCount(poolId) > 0.
Example
if ((await readContract("saturnlplock", "getLockCount", [poolId])) > 0) {
  const by = await readContract("saturnlplock", "getLockedBy", [poolId]);
}

getLockedAt()

READ
getLockedAt(poolId: number): number

Unix time of the pool's latest lockPool call (first lock or extension).

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Unix seconds, 0 when never locked.
What to expect
Never reverts.
Example
const at = await readContract("saturnlplock", "getLockedAt", [poolId]);

getLockCount()

READ
getLockCount(poolId: number): number

Number of successful lockPool calls on the pool: the first lock, every extension and every new lock after one ran out. Never goes down.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Lock call count, 0 when never locked.
What to expect
Never reverts.
Example
const n = await readContract("saturnlplock", "getLockCount", [poolId]);

getBurnedBy()

READ
getBurnedBy(poolId: number): address

The wallet that burned the pool's liquidity.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
address — Address that called burnPool.
What to expect
Reverts with the VM error "Invalid cast: expected bytes, got None" for a pool that is not burned. Read it only when getBurned(poolId) = 1.
Example
if ((await readContract("saturnlplock", "getBurned", [poolId])) === 1) {
  const by = await readContract("saturnlplock", "getBurnedBy", [poolId]);
}

getBurnedAt()

READ
getBurnedAt(poolId: number): number

Unix time the pool's liquidity was burned.

Parameters
NameTypeDescription
poolIdnumberThe pool ID.
Returns
number — Unix seconds, 0 when not burned.
What to expect
Never reverts.
Example
const at = await readContract("saturnlplock", "getBurnedAt", [poolId]);

Lists & Totals

getLockedPoolIds()

READ
getLockedPoolIds(): number*

Every pool on the lock list: locked through lockPool and not yet released or burned. Some locks may have run out; check getLockUntil or getLocked per pool (or call releaseExpiredLock). The order is not stable: a release moves the last pool into the freed slot.

Returns
number* — Pool IDs, one per yielded value; nothing when the list is empty.
What to expect
Never reverts.
Example
const ids = await readContract("saturnlplock", "getLockedPoolIds", []);   // e.g. [12, 31]

getBurnedPoolIds()

READ
getBurnedPoolIds(): number*

Every burned pool, in the order they were burned. The list only grows.

Returns
number* — Pool IDs, one per yielded value; nothing when no pool is burned.
What to expect
Never reverts. On mainnet the list starts [1, 26, 24, 25, 22, 18, 33].
Example
const ids = await readContract("saturnlplock", "getBurnedPoolIds", []);

getLockedPoolCount()

READ
getLockedPoolCount(): number

Length of getLockedPoolIds, expired locks not yet released included.

Returns
number — Number of pools on the lock list.
What to expect
Never reverts.
Example
const n = await readContract("saturnlplock", "getLockedPoolCount", []);

getBurnedPoolCount()

READ
getBurnedPoolCount(): number

Number of burned pools (length of getBurnedPoolIds).

Returns
number — Number of burned pools.
What to expect
Never reverts. It only grows.
Example
const n = await readContract("saturnlplock", "getBurnedPoolCount", []);

getTotalFeesCollected()

READ
getTotalFeesCollected(): number

Lifetime lock and burn fees this contract has sent to the fee wallet, in raw TAZ (9 decimals).

Returns
number — Raw TAZ. 1,000,000,000 = 1 TAZ.
What to expect
Never reverts. It only grows.
Example
const raw = await readContract("saturnlplock", "getTotalFeesCollected", []);
const taz = raw / 1e9;

Fees & Settings

getLockFee()

READ
getLockFee(): number

TAZ each lockPool call pays, extensions included, in raw TAZ (9 decimals).

Returns
number — Raw TAZ. Live on mainnet and devnet: 50,000,000,000 (50 TAZ).
What to expect
Never reverts. 0 would make locks free.
Example
const fee = await readContract("saturnlplock", "getLockFee", []);   // 50000000000 = 50 TAZ

getBurnFee()

READ
getBurnFee(): number

TAZ each burnPool call pays, in raw TAZ (9 decimals). 0 = burns are free.

Returns
number — Raw TAZ. Live on mainnet and devnet: 0.
What to expect
Never reverts.
Example
const fee = await readContract("saturnlplock", "getBurnFee", []);   // 0

getFeeWallet()

READ
getFeeWallet(): address

The address that receives lock and burn fees. They are sent straight there; this contract holds no TAZ.

Returns
address — Fee wallet. Live on mainnet and devnet: P2KBPHBKq1xuoSajuKxQCd7RfCfGFyoczoHQdVxacEUc9As (also the saturnadmin owner today).
What to expect
Never reverts.
Example
const wallet = await readContract("saturnlplock", "getFeeWallet", []);

getMinLockSeconds()

READ
getMinLockSeconds(): number

Shortest durationSeconds lockPool accepts.

Returns
number — Seconds. Live on mainnet and devnet: 86,400 (1 day).
What to expect
Never reverts.
Example
const min = await readContract("saturnlplock", "getMinLockSeconds", []);

getMaxLockSeconds()

READ
getMaxLockSeconds(): number

Longest durationSeconds lockPool accepts. Since the duration counts from now, no lock can end more than this far ahead.

Returns
number — Seconds. Live on mainnet and devnet: 315,360,000 (10 years).
What to expect
Never reverts.
Example
const max = await readContract("saturnlplock", "getMaxLockSeconds", []);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag. Mainnet and devnet report "saturnlplock-1.0.0".

Returns
string — Build tag, e.g. "saturnlplock-1.0.0".
What to expect
Never reverts.
Example
const v = await readContract("saturnlplock", "getContractVersion", []);   // "saturnlplock-1.0.0"

Admin

setLockFee()

WRITE
setLockFee(from: address, newFee: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin) must sign. Sets the TAZ each lockPool call pays, in raw TAZ (9 decimals; 50,000,000,000 = 50 TAZ). 0 makes locks free. Locks already made are not affected.

Parameters
NameTypeDescription
fromaddressThe saturnadmin owner. Must sign.
newFeenumberNew lock fee in raw TAZ, 0 or more.
What to expect
Reverts with "witness failed" (from did not sign), "Only admin" or "Fee must be >= 0". Emits LockSettingChanged(from, {setting: "lockFee", newValue: newFee}).

setBurnFee()

WRITE
setBurnFee(from: address, newFee: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin) must sign. Sets the TAZ each burnPool call pays, in raw TAZ (9 decimals). 0 (the live value) makes burns free.

Parameters
NameTypeDescription
fromaddressThe saturnadmin owner. Must sign.
newFeenumberNew burn fee in raw TAZ, 0 or more.
What to expect
Reverts with "witness failed", "Only admin" or "Fee must be >= 0". Emits LockSettingChanged(from, {setting: "burnFee", newValue: newFee}).

setFeeWallet()

WRITE
setFeeWallet(from: address, newWallet: address)

Admin only. The saturnadmin owner (saturnadmin.getAdmin) must sign. Sets the address that receives lock and burn fees.

Parameters
NameTypeDescription
fromaddressThe saturnadmin owner. Must sign.
newWalletaddressNew fee wallet. Must not be the null address.
What to expect
Reverts with "witness failed", "Only admin" or "Invalid wallet" (null address). Emits LockSettingChanged(from, {setting: "feeWallet", newValue: 0}); the event does not carry the address, so read getFeeWallet().

setLockBounds()

WRITE
setLockBounds(from: address, minSeconds: number, maxSeconds: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin) must sign. Sets the durationSeconds range lockPool accepts (live: 86,400 to 315,360,000). Locks already made keep their end time.

Parameters
NameTypeDescription
fromaddressThe saturnadmin owner. Must sign.
minSecondsnumberShortest lock in seconds. Must be > 0.
maxSecondsnumberLongest lock in seconds. Must be ≥ minSeconds.
What to expect
Reverts with "witness failed", "Only admin", "Min must be > 0" or "Max must be >= min". Emits LockSettingChanged twice: {setting: "minLockSeconds", newValue: minSeconds} and {setting: "maxLockSeconds", newValue: maxSeconds}.
Financial Products · Contract #9

SaturnBonds

saturnbonds saturnbonds-4.1.5

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. Active bonds trade on-chain: the holder lists a price with listBondForSale() and a buyer pays it atomically with acceptBondSale(). A listing nobody buys can be cancelled by its issuer after one hour or expired by anyone after 30 days. Bond terms run from getMinDuration() (an admin setting since 4.1.5: 86,400 s on mainnet, 60 s on devnet) up to 1 year.

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). All amounts are raw units of feeToken, which must be one of the pool's two tokens. In HYBRID mode (mode = 2) the provider deposits collateralAmount of feeToken now — this is held as a buffer to guarantee the buyer's payout. The pool is not locked until someone buys. Claim pending provider fees (saturnfees.claimProviderFees) before the bond is bought: feeToken fees still unclaimed then are paid out through the bond at settlement (the other token's go back to the issuer).

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 in seconds, from getMinDuration() (86,400 on mainnet) to 31,536,000 (1 year). Counted from purchase, not from listing.
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: "Pool not active", "Only pool provider", "Pool already has active bond or rental", "Pool already under another financial product", "Token does not exist: <symbol>", "Face value must be > 0", "Purchase price must be > 0", "Purchase price must be less than face value", "Below the minimum duration" (durationSeconds < getMinDuration()), "Max duration: 1 year", "Fee token must be a pool token", "Mode must be 1 (partial) or 2 (hybrid)", "Partial mode: no collateral", "Hybrid mode: collateral required", or "Insufficient collateral balance". A new bondId is created in status 0 (listed). Event decoding: BondListed.listedAt is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
Example
// Hybrid bond on pool 42 (a SOUL pair): 1,000 SOUL face, 900 SOUL price, 90-day term, 100 SOUL collateral
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "listBond",
    [from, 42,
      100000000000,   // faceValue: 1,000 SOUL (8 decimals)
      90000000000,    // purchasePrice: 900 SOUL
      "SOUL", 90 * 86400, 2,
      10000000000])   // collateralAmount: 100 SOUL
  .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", "Bond not in listed state", or "Listing must be at least 1 hour old before cancel".
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "cancelListing", [from, bondId])
  .spendGas(from)
  .endScript();

purchaseBond()

WRITE
purchaseBond(from: address, bondId: number)

Buyer pays the listing's purchasePrice (raw feeToken) directly to the issuer, becomes the bond holder, starts the term clock (maturity = now + durationSeconds), and turns on the pool's fee redirect in saturnfees: the provider can no longer claim the pool's fees, which are held until settleBond(). The pool becomes financially locked until settlement. The listing checks are run again first, since the pool may have changed since listing.

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: "Bond not available for purchase", "Cannot buy your own bond", "Pool not active", "Issuer no longer pool provider", "Pool already has active bond or rental", "Pool already under another financial product", "Fee token must be a pool token", or "Insufficient balance to purchase bond". Event decoding: BondPurchased.startTime is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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 pool's unclaimed feeToken fees (all of them, including any the provider left unclaimed before the purchase), adds the collateral in hybrid mode, pays min(total, 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, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "settleBond", [from, bondId])
  .spendGas(from)
  .endScript();

transferBond()

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

DEPRECATED — always reverts with "Deprecated in 4.1.0: use listBondForSale + acceptBondSale for atomic paid transfer". Free transfers were removed so that a resale and its payment settle in one transaction.

Parameters
NameTypeDescription
fromaddressIgnored.
toaddressIgnored.
bondIdnumberIgnored.
What to expect
Always reverts.

expireListing()

WRITE
expireListing(from: address, bondId: number)

Anyone can retire a bond listing that has sat unsold for 30 days. The bond moves to status 4 (expired), leaves the id lists, and any hybrid-mode collateral is returned to the issuer. Keeps the marketplace free of stale offers without needing the issuer.

Parameters
NameTypeDescription
fromaddressAny wallet (must be the transaction witness).
bondIdnumberListed bond to expire.
What to expect
Reverts on: "Bond not in listed state" or "Listing has not reached 30-day staleness" (now < listedAt + 2,592,000 s). Emits ListingExpired.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "expireListing", [from, bondId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

listBondForSale()

WRITE
listBondForSale(from: address, bondId: number, priceToken: string, priceAmount: number)

The current holder of an active bond puts it up for resale at a fixed price in any valid token. The bond keeps accruing for the holder until someone accepts; only one sale listing can exist per bond at a time.

Parameters
NameTypeDescription
fromaddressCurrent bond holder (witness).
bondIdnumberActive bond to resell.
priceTokenstringToken the buyer must pay with.
priceAmountnumberRaw price in priceToken.
What to expect
Reverts on: "Bond not active", "Only current bond holder", "Bond already listed for sale", "Price must be > 0", or "Token does not exist: <symbol>". Emits BondSaleListed. Event decoding: BondSaleListed.saleListedAt is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
Example
// Offer bond #7 for 500 SOUL
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "listBondForSale", [from, bondId, "SOUL", 50000000000])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

acceptBondSale()

WRITE
acceptBondSale(from: address, bondId: number)

Buys a bond that is listed for resale: priceAmount of priceToken moves from the buyer straight to the seller and the buyer becomes the bond holder — future settlement pays the buyer. Atomic: no off-chain payment, no escrow.

Parameters
NameTypeDescription
fromaddressBuyer (witness).
bondIdnumberBond with an open sale listing.
What to expect
Reverts on: "Bond not active", "Bond not for sale", "Cannot buy your own listing", or "Insufficient balance for sale price". Emits BondSaleAccepted (buyer) and BondSaleTransferred (seller); the sale listing is cleared.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "acceptBondSale", [from, bondId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

cancelBondSale()

WRITE
cancelBondSale(from: address, bondId: number)

The holder withdraws their resale listing. Allowed only once the listing is at least one hour old, which stops a seller from pulling the offer the moment a buyer's transaction is in flight.

Parameters
NameTypeDescription
fromaddressCurrent bond holder (witness).
bondIdnumberBond whose sale listing is cancelled.
What to expect
Reverts on: "Bond not active", "Bond not for sale", "Only current bond holder", or "Sale listing must be at least 1 hour old before cancel". Emits BondSaleCancelled.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnbonds", "cancelBondSale", [from, bondId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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, duration, status, and collateral. Parse with String.split on underscores, then on ":".

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
string — Format: "pool:<n>_face:<n>_price:<n>_token:<s>_mode:<1|2>_maturity:<ts|0>_duration:<s>_status:<0..4>_collateral:<n>". maturity is 0 until the bond is bought.
What to expect
Status codes: 0 listed, 1 active, 2 settled, 3 cancelled, 4 listing expired.
Example
const info = await readContract("saturnbonds", "getBondInfo", [bondId]);
// "pool:160_face:1000000000000_price:900000000000_token:KCAL_mode:1_maturity:0_duration:86400_status:0_collateral:0"

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. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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. Changes when a resale is accepted (acceptBondSale()); transferBond() is deprecated and always reverts.

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. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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, 4 = listing expired.
What to expect
Transitions: 0 → 1 (purchase), 1 → 2 (settle), 0 → 3 (cancel, after 1 hour), 0 → 4 (expireListing, after 30 days). A resale (listBondForSale / acceptBondSale) keeps status 1 and only changes the holder.
Example
const s = await readContract("saturnbonds", "getBondStatus", [bondId]);

getBondMaturityTime()

READ
getBondMaturityTime(bondId: number): number

Unix timestamp at which the bond can be settled. It is 0 while the bond is listed (status 0) and is set to purchase time + getBondDurationSeconds() when the bond is bought.

Parameters
NameTypeDescription
bondIdnumberBond ID.
Returns
number — Maturity timestamp, or 0 if unsold.
What to expect
Stays set after settlement. For a listed bond, read getBondDurationSeconds() to show the term.
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 the ids of bonds that are still open: listed (status 0) or active (status 1). Settling, cancelling or expiring a bond removes its id. Walk 1 .. getNextBondId() − 1 to reach closed bonds.

Returns
number* — Iterable of bond IDs.
What to expect
Filter by getBondStatus() to split the primary market (0) from active bonds (1); getBondSaleListed() marks active bonds offered for resale.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnbonds", "getAllBondIds", [])
  .endScript();

getBondDurationSeconds()

READ
getBondDurationSeconds(bondId: number): number

Term length chosen at listing, from the getMinDuration() in force then (86,400 s on mainnet) up to 31,536,000 s. maturityTime = purchase time + this value.

Parameters
NameTypeDescription
bondIdnumberId to inspect.
Returns
number — Seconds.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const term = await readContract("saturnbonds", "getBondDurationSeconds", [bondId]);

getBondListedAt()

READ
getBondListedAt(bondId: number): number

Unix time the bond was listed. cancelListing() needs listedAt + 1 hour; expireListing() needs listedAt + 30 days.

Parameters
NameTypeDescription
bondIdnumberId to inspect.
Returns
number — Unix seconds.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const listedAt = await readContract("saturnbonds", "getBondListedAt", [bondId]);

getBondSaleListed()

READ
getBondSaleListed(bondId: number): number

1 when the holder has an open resale listing for this active bond, 0 otherwise.

Parameters
NameTypeDescription
bondIdnumberId to inspect.
Returns
number — 1 = for sale, 0 = not.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const forSale = await readContract("saturnbonds", "getBondSaleListed", [bondId]);

getBondSalePriceToken()

READ
getBondSalePriceToken(bondId: number): string

Token the resale price is quoted in (empty when no sale listing).

Parameters
NameTypeDescription
bondIdnumberId to inspect.
Returns
string — Token symbol of the current resale listing, or of the last one after it was accepted or cancelled; "" if the bond was never listed for resale.
What to expect
Never reverts; "" for unknown ids. acceptBondSale and cancelBondSale clear the listed flag, price and time but not this token, so read getBondSaleListed() == 1 before showing it.
Example
const priceToken = await readContract("saturnbonds", "getBondSalePriceToken", [bondId]);

getBondSalePriceAmount()

READ
getBondSalePriceAmount(bondId: number): number

Raw resale price in getBondSalePriceToken() units; 0 when no sale listing.

Parameters
NameTypeDescription
bondIdnumberId to inspect.
Returns
number — Raw price.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const price = await readContract("saturnbonds", "getBondSalePriceAmount", [bondId]);

getBondSaleListedAt()

READ
getBondSaleListedAt(bondId: number): number

Unix time the resale listing was opened; cancelBondSale() requires one hour to have passed. 0 when no sale listing.

Parameters
NameTypeDescription
bondIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const saleListedAt = await readContract("saturnbonds", "getBondSaleListedAt", [bondId]);

getActiveBondIds()

READ
getActiveBondIds(): number*

Yields the ids of bonds in status 1 (purchased, not yet settled).

Returns
number* — Stream of bond ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnbonds", "getActiveBondIds", []);

getListedBondIds()

READ
getListedBondIds(): number*

Yields the ids of bonds in status 0 (listed, waiting for a buyer) — the open marketplace.

Returns
number* — Stream of bond ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnbonds", "getListedBondIds", []);

getActiveBondsData()

READ
getActiveBondsData(): string*

One pipe-delimited row per active bond: bondId|poolId|issuer|buyer|faceValue|purchasePrice|feeToken|collateral|mode|status|maturityTime|durationSeconds. Amounts are raw units of feeToken.

Returns
string* — Stream of "bondId|poolId|issuer|buyer|faceValue|purchasePrice|feeToken|collateral|mode|status|maturityTime|durationSeconds" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnbonds", "getActiveBondsData", []);
const bonds = rows.map((r) => r.split("|"));

getListedBondsData()

READ
getListedBondsData(): string*

Same row layout as getActiveBondsData() for bonds in status 0 (listed). Until purchase the buyer field is the text "[Null address]" and maturityTime is 0.

Returns
string* — Stream of "bondId|poolId|issuer|buyer|faceValue|purchasePrice|feeToken|collateral|mode|status|maturityTime|durationSeconds" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnbonds", "getListedBondsData", []);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnbonds-4.1.5". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnbonds-4.1.5".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnbonds", "getContractVersion", []);
// "saturnbonds-4.1.5"

getMinDuration()

READ
getMinDuration(): number

The shortest bond term listBond() accepts right now, in seconds. Returns 86,400 (1 day) while the admin has not set a value. Live: 86400 on mainnet, 60 on devnet.

Returns
number — Minimum durationSeconds for listBond(), in seconds.
What to expect
Never reverts. Read it before listBond(): a shorter durationSeconds reverts "Below the minimum duration". Bonds already listed keep their own term.
Example
const minTerm = await readContract("saturnbonds", "getMinDuration", []);
// 86400 on mainnet
const durationSeconds = Math.max(minTerm, 30 * 86400);

Admin & Internal

setMinDuration()

WRITE
setMinDuration(seconds: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must witness the transaction. Sets the shortest term listBond() accepts. 0 restores the 1-day default (86,400 s); any other value must be 60 .. 31,536,000 seconds. Bonds already listed keep the duration they were listed with. Live: 86,400 s on mainnet; devnet is set to 60 s so a bond can be settled within minutes.

Parameters
NameTypeDescription
secondsnumberNew minimum term in seconds: 0 (= 1 day) or 60 .. 31,536,000.
Returns
void — Success = new minimum stored.
What to expect
Reverts on: "Only admin", "Min duration: at least 60 seconds (0 = 1 day)", or "Min duration: at most 1 year". Takes no reentrancy guard and emits no event; read the value back with getMinDuration().
Financial Products · Contract #10

SaturnRental

saturnrental saturnrental-4.1.2

Franchise model for pools. Providers list their pool with a daily SOUL rate, a SOUL deposit, a fee-bound window, and a minimum and maximum term in days. A renter pays the deposit plus the minimum term's rent up front 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. The renter can prepay more days with extendRental() up to the maximum term. 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, and even the owner cannot change its fee directly.

Rental Lifecycle

listForRentV2()

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

Pool provider lists their pool for rent. Defines the price (SOUL per day), the SOUL deposit a renter must post, the fee envelope the renter may operate inside, and the minimum and maximum term. The live fee is recorded when a renter steps in, so it can be restored at the end. The pool must be free of bonds, rentals, options and any other financial product. Listing does not lock the pool. Claim pending provider fees (saturnfees.claimProviderFees) before a renter steps in: fees still unclaimed then go to the renter.

Parameters
NameTypeDescription
fromaddressPool provider (witness).
poolIdnumberActive pool to list.
dailyRatenumberRent per day in raw SOUL units (8 decimals).
depositRequirednumberRefundable deposit in raw SOUL units.
minFeePer10knumberLowest fee the renter may set (>= protocol minimum).
maxFeePer10knumberHighest fee the renter may set (<= protocol maximum).
minTermDaysnumberMinimum rental length, 1..365 days; paid up front by the renter.
maxTermDaysnumberHard cap on the rental length, minTermDays..3650 days; extensions cannot pass it.
What to expect
Reverts on: "Pool not active", "Only pool provider", "Pool already has active bond or rental", "Pool already under another financial product", "Daily rate must be > 0", "Deposit must be > 0", "Min term: 1 day", "Max min term: 365 days", "maxTermDays must be >= minTermDays", "maxTermDays ceiling: 10 years", "dailyRate * maxTermDays exceeds sanity ceiling" (1e18), "Min fee below protocol minimum", "Max fee above protocol maximum", or "Min fee must be <= max fee". A new rentalId is created in status 0 (listed); nothing is locked until rentPool().
Example
// 10 SOUL/day, 500 SOUL deposit, fee 0.3%–3%, 7–90 day term
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnrental", "listForRentV2", [from, poolId, 1000000000, 50000000000, 30, 300, 7, 90])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

listForRent()

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

DEPRECATED — always reverts with "Deprecated in 4.1.0: use listForRentV2(poolId, dailyRate, depositRequired, minFeePer10k, maxFeePer10k, minTermDays, maxTermDays)". The V2 form adds the maxTermDays cap.

Parameters
NameTypeDescription
fromaddressIgnored.
poolIdnumberIgnored.
dailyRatenumberIgnored.
depositRequirednumberIgnored.
minFeePer10knumberIgnored.
maxFeePer10knumberIgnored.
minTermDaysnumberIgnored.
What to expect
Always reverts. Call listForRentV2() instead.

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
Reverts on: "Only rental owner" or "Rental not in listed state". Status flips to 3 (cancelled) and the id is removed from getAllRentalIds() / getListedRentalIds(). There is no minimum listing age.
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 raw SOUL, gains fee control, and the pool's fee redirect is turned on in saturnfees: from now on the pool's provider fees (plus any the owner left unclaimed) can be claimed only by the renter. The pool's live fee is recorded as getRentalOriginalFee() and restored by endRental(). The live pool is checked again first, since listings do not lock it.

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(). Reverts on: "Rental not available", "Cannot rent your own pool", "Pool not active", "Owner no longer pool provider", "Pool already has active bond or rental", "Pool already under another financial product", or "Insufficient SOUL for deposit + rent".
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. The new paid-through time is the old one + additionalDays × 86,400 s, not now + additionalDays, so days that have already passed are paid for too. There is no deadline: it also works after paidThroughTime has passed, as long as the rental has not been ended and the new paid-through time stays within getRentalHardCapTime(). Extend before expiry to keep adjustFee() working and to stop either party from calling endRental().

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 and paidThroughTime is pushed out by additionalDays * 86400 seconds, but never past getRentalHardCapTime() (start + maxTermDays). rentalRentAccrued rises, so the owner will be able to collectRent() the additional amount. Reverts on: "Rental not active", "Only renter", "Must add at least 1 day", "Extension would exceed maxTermDays cap", "Extension rent exceeds sanity ceiling", or "Insufficient SOUL".
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. Reverts on: "Rental not active", "Only renter", "Rental expired - extend or end", "Below rental min fee", "Above rental max fee", "Fee redirect not active for this pool", or saturnpools' "Fee too low, min: <n>" / "Fee too high, max: <n>" if the admin has since narrowed the protocol range.
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 (raw token units). Safe to call frequently — if nothing has accrued since the last claim, nothing is sent. Also works after paidThroughTime, until the rental is ended. Reverts on: "Rental not active" or "Only renter".
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 (raw SOUL) is sent to the owner. Rent is prepaid, not streamed: the owner can collect the whole prepaid amount right after rentPool() or extendRental(). Reverts on: "Only rental owner", "Rental not active", or "No rent to collect" (available is 0).
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) and the id leaves getAllRentalIds(). Reverts on: "Rental not active", "Only owner or renter", "Rental term not expired - extend or wait" if you call it too early, or saturnpools' "Fee too low, min: <n>" / "Fee too high, max: <n>" if the admin has since moved the protocol range past getRentalOriginalFee().
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>_origFee:<originalFee>_minTerm:<days>_maxTerm:<days>_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.origFee,
// parts.minTerm, parts.maxTerm, 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. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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
Total due at rentPool() = dailyRate × getRentalMinTermDays() + getRentalDepositAmount(), all raw SOUL.
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 the ids of rentals that are still open: listed (status 0) or rented (status 1). cancelListing() and endRental() remove the id. Walk 1 .. getNextRentalId() − 1 to reach closed rentals.

Returns
number* — Iterable of rental IDs.
What to expect
Combine with getRentalStatus() to split listed (0) from rented (1).
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnrental", "getAllRentalIds", [])
  .endScript();

getRentalDepositAmount()

READ
getRentalDepositAmount(rentalId: number): number

SOUL deposit the renter must post (raw units); refunded to the renter at endRental().

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Raw SOUL.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const deposit = await readContract("saturnrental", "getRentalDepositAmount", [rentalId]);

getRentalMinFee()

READ
getRentalMinFee(rentalId: number): number

Lowest fee (per 10k) the renter may set on the pool while the rental is active.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Fee per 10,000.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const minFee = await readContract("saturnrental", "getRentalMinFee", [rentalId]);

getRentalMaxFee()

READ
getRentalMaxFee(rentalId: number): number

Highest fee (per 10k) the renter may set on the pool while the rental is active.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Fee per 10,000.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const maxFee = await readContract("saturnrental", "getRentalMaxFee", [rentalId]);

getRentalMinTermDays()

READ
getRentalMinTermDays(rentalId: number): number

Minimum term in days; rentPool() charges this many days of rent up front.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Days.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const minDays = await readContract("saturnrental", "getRentalMinTermDays", [rentalId]);

getRentalMaxTermDays()

READ
getRentalMaxTermDays(rentalId: number): number

Maximum term in days from the rental start; extendRental() cannot push paidThroughTime past it.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Days.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const maxDays = await readContract("saturnrental", "getRentalMaxTermDays", [rentalId]);

getRentalStartTime()

READ
getRentalStartTime(rentalId: number): number

Unix time rentPool() was called; 0 while the listing is unrented.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const start = await readContract("saturnrental", "getRentalStartTime", [rentalId]);

getRentalRentAccrued()

READ
getRentalRentAccrued(rentalId: number): number

Total rent (raw SOUL) the renter has prepaid so far — the initial term plus every extension. The owner's collectable balance is this minus getRentalRentCollected().

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Raw SOUL.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const accrued = await readContract("saturnrental", "getRentalRentAccrued", [rentalId]);

getRentalRentCollected()

READ
getRentalRentCollected(rentalId: number): number

Rent (raw SOUL) the owner has already withdrawn with collectRent() or received at endRental().

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Raw SOUL.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const collected = await readContract("saturnrental", "getRentalRentCollected", [rentalId]);

getRentalOriginalFee()

READ
getRentalOriginalFee(rentalId: number): number

The pool fee recorded when the rental started; restored by endRental(). 0 while unrented.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Fee per 10,000.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const originalFee = await readContract("saturnrental", "getRentalOriginalFee", [rentalId]);

getRentalHardCapTime()

READ
getRentalHardCapTime(rentalId: number): number

start + maxTermDays * 86400: the latest paid-through time any extension may reach. 0 while the listing is unrented.

Parameters
NameTypeDescription
rentalIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const hardCap = await readContract("saturnrental", "getRentalHardCapTime", [rentalId]);

getActiveRentalIds()

READ
getActiveRentalIds(): number*

Yields the ids of rentals in status 1 (rented).

Returns
number* — Stream of rental ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnrental", "getActiveRentalIds", []);

getListedRentalIds()

READ
getListedRentalIds(): number*

Yields the ids of rentals in status 0 (listed, available to rent).

Returns
number* — Stream of rental ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnrental", "getListedRentalIds", []);

getActiveRentalsData()

READ
getActiveRentalsData(): string*

One pipe-delimited row per active rental: rentalId|poolId|owner|operator|dailyRate|deposit|minFee|maxFee|minTermDays|maxTermDays|status. dailyRate and deposit are raw SOUL.

Returns
string* — Stream of "rentalId|poolId|owner|operator|dailyRate|deposit|minFee|maxFee|minTermDays|maxTermDays|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnrental", "getActiveRentalsData", []);

getListedRentalsData()

READ
getListedRentalsData(): string*

Same row layout as getActiveRentalsData() for listings in status 0; the operator field is the text "[Null address]".

Returns
string* — Stream of "rentalId|poolId|owner|operator|dailyRate|deposit|minFee|maxFee|minTermDays|maxTermDays|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnrental", "getListedRentalsData", []);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnrental-4.1.2". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnrental-4.1.2".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnrental", "getContractVersion", []);
// "saturnrental-4.1.2"
Financial Products · Contract #11

SaturnFeeOptions

saturnfeeopts saturnfeeopts-4.1.2

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 the token the writer chose; the premium is the writer's income. 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, anyone (usually the writer) calls expireOption() to reclaim it. A pool can carry only one bought option at a time: buyOption() needs the pool free of every financial lock, including another option. Several listings may sit on one pool, but once one is bought the others cannot be bought until it ends. An option does not redirect fees: the provider keeps earning them at whatever rate is set.

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. The fee to restore is not captured here: buyOption() records the pool's live fee at purchase, and that is what releaseOption() / expireOption() restore. The fee at writing only appears in the OptionWritten event.

Parameters
NameTypeDescription
fromaddressPool provider — must be a transaction witness.
poolIdnumberThe active pool you own.
targetFeePer10knumberFee rate the buyer can snap the pool to. Per 10,000. Must sit within saturnadmin.getPoolFeeRange() (30 .. 3000 on mainnet).
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 costs only gas. Reverts on: "Pool not active", "Only pool provider", "Pool already has active bond or rental", "Pool already under another financial product", "Target fee below protocol minimum", "Target fee above protocol maximum", "Token does not exist: <symbol>", "Premium must be > 0", "Min duration: 1 hour", or "Max duration: 30 days".
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnfeeopts", "writeOption", [
    from,
    poolId,
    50,              // target 0.50% fee
    10000000000,     // premium: 100 SOUL (8 decimals)
    "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
Reverts on: "Only option writer" or "Option not in listed state". Status flips to 3 (cancelled) and the id is removed from getAllOptionIds() / getListedOptionIds().
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, the pool's live fee is recorded as getOptionOriginalFee() (the fee restored when the option ends), and the pool gets a financial lock. The live pool is checked again first, since listings do not lock it.

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. Reverts on: "Option not available", "Cannot buy your own option", "Pool not active", "Writer no longer pool provider", "Pool already has active bond or rental", "Pool already under another financial product", or "Insufficient balance for premium". Event decoding: OptionBought.startTime is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
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 fee recorded at purchase when the option unwinds. Reverts on: "Option not active", "Only option buyer", "Option expired" (now >= endTime), or saturnpools' "Fee too low, min: <n>" / "Fee too high, max: <n>" if the admin has since narrowed the protocol range.
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
Reverts on: "Option not active", "Only option buyer", or (exercised options only) saturnpools' "Fee too low, min: <n>" / "Fee too high, max: <n>" if the admin has since moved the protocol range past getOptionOriginalFee(). If the option had been exercised, the pool's fee is restored to getOptionOriginalFee() (the fee at purchase). The pool's financial lock decrements, status becomes 4 (released) and the id leaves getAllOptionIds(). 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 getOptionOriginalFee() (the fee at purchase). The pool's financial lock decrements, status becomes 2 (expired) and the id leaves getAllOptionIds(). Reverts on: "Option not active", "Option not expired yet" if called early — poll getOptionEndTime first — or (exercised options only) saturnpools' "Fee too low, min: <n>" / "Fee too high, max: <n>" if the admin has since moved the protocol range past getOptionOriginalFee().
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|0>_duration:<seconds>_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.duration, 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 button in the provider dashboard. expireOption() is open to any wallet once endTime has passed. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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
Read getOptionPremiumToken() for the symbol and divide by 10^decimals of that token to display it.
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. It is 0 until the option is bought; read getOptionDurationSeconds() for the window of a listing.

Parameters
NameTypeDescription
optionIdnumberThe option to inspect.
Returns
number — Unix seconds, or 0 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 the ids of options that are still open: listed (status 0) or active (status 1). Cancelling, releasing or expiring an option removes its id. Walk 1 .. getNextOptionId() − 1 to reach closed options.

Returns
number* — Iterable of option IDs.
What to expect
Combine with getOptionStatus() to split listed (0) from active (1).
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnfeeopts", "getAllOptionIds", [])
  .endScript();

getOptionPremiumToken()

READ
getOptionPremiumToken(optionId: number): string

Token the premium is paid in (chosen by the writer).

Parameters
NameTypeDescription
optionIdnumberId to inspect.
Returns
string — Token symbol.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const premiumToken = await readContract("saturnfeeopts", "getOptionPremiumToken", [optionId]);

getOptionStartTime()

READ
getOptionStartTime(optionId: number): number

Unix time the option was bought; 0 while listed.

Parameters
NameTypeDescription
optionIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const start = await readContract("saturnfeeopts", "getOptionStartTime", [optionId]);

getOptionDurationSeconds()

READ
getOptionDurationSeconds(optionId: number): number

Window length set by the writer (3,600 .. 2,592,000 s). endTime = startTime + this value.

Parameters
NameTypeDescription
optionIdnumberId to inspect.
Returns
number — Seconds.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const duration = await readContract("saturnfeeopts", "getOptionDurationSeconds", [optionId]);

getOptionExercised()

READ
getOptionExercised(optionId: number): number

1 once the buyer has exercised (the pool fee sits at the target), 0 otherwise.

Parameters
NameTypeDescription
optionIdnumberId to inspect.
Returns
number — 1 or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const exercised = await readContract("saturnfeeopts", "getOptionExercised", [optionId]);

getOptionOriginalFee()

READ
getOptionOriginalFee(optionId: number): number

Pool fee recorded when the option was bought; restored when the option is released or expired. 0 while listed.

Parameters
NameTypeDescription
optionIdnumberId to inspect.
Returns
number — Fee per 10,000.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const originalFee = await readContract("saturnfeeopts", "getOptionOriginalFee", [optionId]);

getActiveOptionIds()

READ
getActiveOptionIds(): number*

Yields the ids of options in status 1 (bought, not yet released or expired). An option past its endTime stays here until someone calls expireOption().

Returns
number* — Stream of option ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnfeeopts", "getActiveOptionIds", []);

getListedOptionIds()

READ
getListedOptionIds(): number*

Yields the ids of options in status 0 (written, not yet bought).

Returns
number* — Stream of option ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnfeeopts", "getListedOptionIds", []);

getActiveOptionsData()

READ
getActiveOptionsData(): string*

One pipe-delimited row per active option: optionId|poolId|writer|buyer|targetFee|premium|premiumToken|endTime|durationSeconds|exercised|status. premium is raw units of premiumToken.

Returns
string* — Stream of "optionId|poolId|writer|buyer|targetFee|premium|premiumToken|endTime|durationSeconds|exercised|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnfeeopts", "getActiveOptionsData", []);

getListedOptionsData()

READ
getListedOptionsData(): string*

Same row layout as getActiveOptionsData() for options in status 0; the buyer field is the text "[Null address]" and endTime is 0.

Returns
string* — Stream of "optionId|poolId|writer|buyer|targetFee|premium|premiumToken|endTime|durationSeconds|exercised|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnfeeopts", "getListedOptionsData", []);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnfeeopts-4.1.2". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnfeeopts-4.1.2".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnfeeopts", "getContractVersion", []);
// "saturnfeeopts-4.1.2"
Financial Products · Contract #12

SaturnSyndicate

saturnsyndicate saturnsyndicate-4.1.5

Pool crowdfunding. Anyone can open a syndicate that names a token pair, target amounts, the ratio contributions must follow, a minimum fill and a pool fee rate. Contributors deposit both tokens in that ratio (up to each remaining target); once at least the minimum fill is raised the creator calls 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. Members claim their proportional cut of swap fees as long as the syndicate is active. Dissolution is a member vote: any member proposes, members vote with their share weight, and after a 72-hour timelock a strict majority executes — the pool is emptied and members claim back proportional reserves with claimDissolution. During the funding phase or after cancellation, contributors can pull their tokens back with withdrawContribution.

Syndicate Lifecycle

createSyndicateV2()

WRITE
createSyndicateV2(from: address, tokenA: string, tokenB: string, targetA: number, targetB: number, feePer10k: number, ratioA: number, ratioB: number, minFillPer10k: number)

Anyone can open a new syndicate funding round for a token pair. Sets the fundraising targets, the ratio every contribution must respect (targetA : targetB must equal ratioA : ratioB), the minimum fill (per 10k of both targets) required before activation, and the eventual pool's fee rate. No tokens move at creation.

Parameters
NameTypeDescription
fromaddressCreator (witness).
tokenAstringFirst token of the pair.
tokenBstringSecond token of the pair.
targetAnumberRaw amount of tokenA to raise.
targetBnumberRaw amount of tokenB to raise.
feePer10knumberFee of the pool that will be created (inside the protocol range).
ratioAnumbertokenA side of the contribution ratio, in raw units: every contribution must satisfy amountA * ratioB == amountB * ratioA. For 2 SOUL : 5 KCAL (8 and 10 decimals) use 2 : 500.
ratioBnumbertokenB side of the contribution ratio, in raw units. targetA * ratioB must equal targetB * ratioA.
minFillPer10knumberMinimum fill of both targets before activateSyndicate() is allowed, 1000 (10%) .. 10000 (100%).
What to expect
Reverts on: "Not authorized", "Token does not exist: <symbol>", "Same token", "Target A must be > 0" / "Target B must be > 0", "Ratio A must be > 0" / "Ratio B must be > 0", "Targets must match declared ratio" (targetA * ratioB != targetB * ratioA), "minFillPer10k must be between 1000 and 10000", "Fee too low" or "Fee too high" (saturnadmin range, 30..3000 per 10,000 live). A new syndicateId is created in status 0 (funding). The method returns nothing: read the id from SyndicateCreated (syndicateId). Emits SyndicateCreated.
Example
// Raise 1,000 SOUL (8 decimals) + 2,500 KCAL (10 decimals): 2 SOUL : 5 KCAL,
// which is 2 : 500 in raw units. Activation allowed at 80% fill.
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "createSyndicateV2", [from, "SOUL", "KCAL",
    100000000000,     // targetA: 1,000 SOUL
    25000000000000,   // targetB: 2,500 KCAL
    30,               // feePer10k: 0.3% (live range 30..3000)
    2, 500,           // ratioA, ratioB (raw units)
    8000])            // minFillPer10k: 80%
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

createSyndicate()

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

DEPRECATED — always reverts with "Deprecated in 4.1.0: use createSyndicateV2(tokenA, tokenB, targetA, targetB, feePer10k, ratioA, ratioB, minFillPer10k)". V2 adds the contribution ratio and minimum fill.

Parameters
NameTypeDescription
fromaddressIgnored.
tokenAstringIgnored.
tokenBstringIgnored.
targetAnumberIgnored.
targetBnumberIgnored.
feePer10knumberIgnored.
What to expect
Always reverts. Call createSyndicateV2() instead.

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. Reverts on: "Not authorized", "Only creator" or "Syndicate not in funding state".
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. 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. Reverts on: "Syndicate not in funding state", "Amount A must be > 0" / "Amount B must be > 0", "Contribution must match declared ratio" (amountA * ratioB != amountB * ratioA), "Exceeds target for token A/B", or "Insufficient token A/B".
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 on: "Not authorized", "Can only withdraw during funding or after cancellation" or "No contribution found".
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 the syndicate contract 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. Targets need not be fully hit — whatever has been raised becomes the initial reserves — but both sides must reach the declared minimum fill and the protocol's minimum pool size (saturnadmin.getMinScaledPoolUnits: 100 whole tokens per side on mainnet and devnet). Reverts on: "Only creator", "Syndicate not in funding state", "No token A/B raised", "Token A/B fill below minimum threshold", or "Token A/B below min pool size".
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 share of the swap fees the syndicate pool has earned. Shares are tokenA contributions (total = raisedA). MasterChef-style: each harvest adds harvested * 10^12 / raisedA to accFeePerShare, and you receive contribA * accFeePerShare / 10^12 minus what you already took (your reward debt).

Parameters
NameTypeDescription
fromaddressMust be a member of the syndicate.
syndicateIdnumberAn active syndicate (status 1).
What to expect
The syndicate first claims the pool's pending provider fees from saturnfees (both tokens, scaled down to raw units) and adds them to the accumulators. Since 4.1.5 the part the per-share figure cannot represent is carried to the next harvest instead of being stranded in the contract. You then receive contribA * accFeePerShare / 10^12 − rewardDebt in each token (raw) and your reward debt moves up. Callable repeatedly; it pays 0 when nothing new has accrued. Reverts on: "Not authorized", "Syndicate not active" or "Not a member of this syndicate". Emits RewardsClaimed.
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)

DEPRECATED — always reverts with "Deprecated in 4.1.0: dissolve now requires member vote - use proposeDissolve, voteDissolve, executeDissolve". The creator can no longer wind a syndicate down unilaterally.

Parameters
NameTypeDescription
fromaddressIgnored.
syndicateIdnumberIgnored.
What to expect
Always reverts.

proposeDissolve()

WRITE
proposeDissolve(from: address, syndicateId: number)

Any member of an active syndicate opens a dissolution proposal. The proposer's own tokenA contribution is counted as the first vote and a 72-hour timelock starts. A syndicate gets one proposal in its life: getDissolveProposed never goes back to 0, so the proposal never expires and keeps collecting votes until executeDissolve succeeds.

Parameters
NameTypeDescription
fromaddressMember (witness).
syndicateIdnumberActive syndicate.
What to expect
Reverts on: "Syndicate not active", "Proposal already open", or "Not a member of this syndicate". Emits DissolveProposed with the earliest execution time (now + 259,200 s). Event decoding: DissolveProposed.proposedAt is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "proposeDissolve", [from, syndicateId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

voteDissolve()

WRITE
voteDissolve(from: address, syndicateId: number)

A member adds their share weight (tokenA contribution) to the open dissolution proposal. Each member votes at most once per proposal; there is no vote against — members who disagree simply do not vote.

Parameters
NameTypeDescription
fromaddressMember (witness).
syndicateIdnumberSyndicate with an open proposal.
What to expect
Reverts on: "Syndicate not active", "No proposal open", "Not a member of this syndicate", or "Already voted". Emits DissolveVoted with the cumulative vote weight.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "voteDissolve", [from, syndicateId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

executeDissolve()

WRITE
executeDissolve(from: address, syndicateId: number)

Any member executes a proposal once the 72-hour timelock has elapsed and the votes represent a strict majority of the raised tokenA (votes * 2 > raisedA). Pending fees are harvested, the pool's reserves are pulled into the syndicate contract, the pool is deactivated and its financial lock released, and status becomes 2 (dissolved). Members then call claimDissolution() for their proportional share of the reserves.

Parameters
NameTypeDescription
fromaddressMember (witness).
syndicateIdnumberSyndicate with a passed proposal.
What to expect
Reverts on: "Syndicate not active", "No proposal open", "Timelock not elapsed (72h from proposal required)", "Caller not a member", "Vote has not passed (need strict majority of share weight)", "Pool locked by bonds/rental/feeopts - resolve first", or "Pool locked by active reward campaign - resolve first". Emits SyndicateDissolved with the recovered reserves.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnsyndicate", "executeDissolve", [from, syndicateId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

claimDissolution()

WRITE
claimDissolution(from: address, syndicateId: number)

After a syndicate has been dissolved, each member claims their proportional share of the recovered reserves in both tokens, plus any fees they had not claimed yet. 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
You receive, in each token (raw): dissolvedRes * contribA / raisedA (see getSyndicateDissolvedResA/B) plus your unclaimed fees, contribA * accFeePerShare / 10^12 − rewardDebt. Your contribA/B and reward debt are cleared, so a second call reverts. Rounding dust stays in the contract. Reverts on: "Not authorized", "Syndicate not dissolved" or "Not a member or already claimed". Emits DissolutionClaimed.
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. contribute adds 1 for a new member and withdrawContribution subtracts 1; claimDissolution does not change it, so after a dissolution it still counts members who have claimed.

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 createSyndicateV2 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();

getSyndicateRatioA()

READ
getSyndicateRatioA(syndicateId: number): number

tokenA side of the declared contribution ratio.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Ratio numerator.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const ratioA = await readContract("saturnsyndicate", "getSyndicateRatioA", [syndicateId]);

getSyndicateRatioB()

READ
getSyndicateRatioB(syndicateId: number): number

tokenB side of the declared contribution ratio. A contribution must satisfy amountA * ratioB == amountB * ratioA.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Ratio numerator.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const ratioB = await readContract("saturnsyndicate", "getSyndicateRatioB", [syndicateId]);

getSyndicateMinFillPer10k()

READ
getSyndicateMinFillPer10k(syndicateId: number): number

Minimum fill (per 10k) of both targets required before the creator may activate.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — 1000..10000.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const minFill = await readContract("saturnsyndicate", "getSyndicateMinFillPer10k", [syndicateId]);

getDissolveProposed()

READ
getDissolveProposed(syndicateId: number): number

1 once a dissolution proposal has been opened, 0 before. It is never reset: the proposal stays open until executeDissolve, and the flag stays 1 after dissolution.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — 1 or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const proposed = await readContract("saturnsyndicate", "getDissolveProposed", [syndicateId]);

getDissolveVotes()

READ
getDissolveVotes(syndicateId: number): number

Cumulative tokenA share weight that has voted for the open proposal. It passes when votes * 2 > getSyndicateRaisedA().

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Raw tokenA weight.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const votes = await readContract("saturnsyndicate", "getDissolveVotes", [syndicateId]);

getDissolveProposedAt()

READ
getDissolveProposedAt(syndicateId: number): number

Unix time the open proposal was created; 0 when none.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const proposedAt = await readContract("saturnsyndicate", "getDissolveProposedAt", [syndicateId]);

getDissolveEarliestExecute()

READ
getDissolveEarliestExecute(syndicateId: number): number

proposedAt + 259,200 s (72 h): the earliest time executeDissolve() can succeed. 0 until a proposal has been opened.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const earliest = await readContract("saturnsyndicate", "getDissolveEarliestExecute", [syndicateId]);

getMemberHasVoted()

READ
getMemberHasVoted(syndicateId: number, member: address): number

1 if the member has already voted on the current proposal (the proposer counts as having voted).

Parameters
NameTypeDescription
syndicateIdnumberSyndicate.
memberaddressMember wallet.
Returns
number — 1 or 0.
What to expect
Never reverts.
Example
const voted = await readContract("saturnsyndicate", "getMemberHasVoted", [syndicateId, memberAddress]);

getSyndicateAccFeePerShareA()

READ
getSyndicateAccFeePerShareA(syndicateId: number): number

MasterChef-style accumulator: raw tokenA fees harvested per unit of tokenA contributed, times 10^12. A member's claimable tokenA is contribA * accFeePerShareA / 10^12 − debtA. It only moves when a claimSyndicateReward or executeDissolve harvests the pool's fees.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Scaled accumulator.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const accA = await readContract("saturnsyndicate", "getSyndicateAccFeePerShareA", [syndicateId]);

getSyndicateAccFeePerShareB()

READ
getSyndicateAccFeePerShareB(syndicateId: number): number

tokenB counterpart of getSyndicateAccFeePerShareA().

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Scaled accumulator.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const accB = await readContract("saturnsyndicate", "getSyndicateAccFeePerShareB", [syndicateId]);

getSyndicateDissolvedResA()

READ
getSyndicateDissolvedResA(syndicateId: number): number

Raw tokenA recovered from the pool at executeDissolve(); members receive contribA / raisedA of it through claimDissolution(). 0 until dissolved.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Raw tokenA.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const resA = await readContract("saturnsyndicate", "getSyndicateDissolvedResA", [syndicateId]);

getSyndicateDissolvedResB()

READ
getSyndicateDissolvedResB(syndicateId: number): number

Raw tokenB recovered at dissolution. 0 until dissolved.

Parameters
NameTypeDescription
syndicateIdnumberId to inspect.
Returns
number — Raw tokenB.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const resB = await readContract("saturnsyndicate", "getSyndicateDissolvedResB", [syndicateId]);

getMemberRewardDebt()

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

The member's reward-debt checkpoints for both tokens packed as "debtA:<n>_debtB:<n>". Subtract from contribution × accumulator to reproduce the amount claimSyndicateReward() will pay.

Parameters
NameTypeDescription
syndicateIdnumberSyndicate.
memberaddressMember wallet.
Returns
string — "debtA:<n>_debtB:<n>".
What to expect
Never reverts.
Example
const debt = await readContract("saturnsyndicate", "getMemberRewardDebt", [syndicateId, memberAddress]);

getActiveSyndicateIds()

READ
getActiveSyndicateIds(): number*

Yields the ids of syndicates in status 1 (active, pool live).

Returns
number* — Stream of syndicate ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnsyndicate", "getActiveSyndicateIds", []);

getActiveSyndicatesData()

READ
getActiveSyndicatesData(): string*

One pipe-delimited row per active syndicate: syndicateId|tokenA|tokenB|targetA|targetB|raisedA|raisedB|feePer10k|poolId|status|memberCount.

Returns
string* — Stream of "syndicateId|tokenA|tokenB|targetA|targetB|raisedA|raisedB|feePer10k|poolId|status|memberCount" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnsyndicate", "getActiveSyndicatesData", []);

getUserSyndicatesData()

READ
getUserSyndicatesData(user: address): string*

Same row layout as getActiveSyndicatesData() for every syndicate (any status) in which the user has a non-zero tokenA contribution — a member's portfolio in one call.

Parameters
NameTypeDescription
useraddressWallet to look up.
Returns
string* — Stream of "syndicateId|tokenA|tokenB|targetA|targetB|raisedA|raisedB|feePer10k|poolId|status|memberCount" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnsyndicate", "getUserSyndicatesData", [userAddress]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnsyndicate-4.1.5". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnsyndicate-4.1.5".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnsyndicate", "getContractVersion", []);
// "saturnsyndicate-4.1.5"
Financial Products · Contract #17

SaturnLaunchpad

saturnlaunchpad saturnlaunchpad-4.1.5

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 sold tokenA and the raised quote seed a Saturn pool at the fee tier set by `poolFeePer10k`, so the pool opens at the sale price; unsold tokenA goes back to the creator. 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 as a whole number: raw units of tokenQuote per 1 raw unit of tokenA. Must be > 0. Price per whole token = quotePerA × 10^(decimalsA − decimalsQuote): with equal decimals 2 means 2 quote tokens per tokenA, and a price below 1 quote token per tokenA is only possible when tokenA has fewer decimals than the quote token.
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 raw tokenQuote per commit call. Must be > 0. Every commit must also be a multiple of quotePerA, so pick a multiple here too.
endTimenumberUnix timestamp when the funding window closes. Must be in the future and at most 14 days (1209600 s) from now.
What to expect
tokensForSale of tokenA moves from your wallet into escrow now. Reverts on: "Not authorized", "Token does not exist: <symbol>", "Sale token must differ from quote token", "tokensForSale must be > 0", "quotePerA must be > 0", "minCommitQuote must be > 0", "minFillPer10k must be between 1000 and 10000", "endTime must be in the future", "endTime exceeds 14-day maximum", "Pool fee too low" / "Pool fee too high" (live range 30..3000), or "Insufficient tokenA for deposit". Emits LaunchpadCreated. The method returns nothing: read the new id from LaunchpadCreated (launchpadId); a getNextLaunchpadId() read beforehand can be taken by another creator first.
Example
// Sell 1,000,000 MYTOKEN (8 decimals) for SOUL (8 decimals) at 2 SOUL each
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlaunchpad", "createLaunchpad", [
    from,
    "MYTOKEN",          // tokenA, escrowed from your wallet now
    "SOUL",             // tokenQuote
    100000000000000,    // tokensForSale: 1,000,000 MYTOKEN (raw)
    2,                  // quotePerA: 2 raw SOUL per raw MYTOKEN = 2 SOUL per MYTOKEN
    30,                 // poolFeePer10k: 0.3% (live range 30..3000)
    5000,               // minFillPer10k: 50% must sell before activation
    1000000000,         // minCommitQuote: 10 SOUL (raw, a multiple of quotePerA)
    Math.floor(Date.now() / 1000) + 86400 * 7  // endTime: 7 days (max 14)
  ])
  .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). It only works while no buyer holds a commitment (buyers who withdrew do not count), before or after endTime. With buyers in, the creator can neither cancel nor propose a dissolution: activate once the fill is reached, or after endTime anyone (the creator included) can call `dissolveViaTimeout`, after which everyone uses `claimFundingRefund`.

Parameters
NameTypeDescription
fromaddressCreator's address; must be the transaction witness.
launchpadIdnumberID of the launchpad to cancel.
What to expect
Reverts on: "Not authorized", "Only creator", "Launchpad not in funding state", or "Buyers already committed - use dissolve flow". Emits LaunchpadCancelled. After this call `getLaunchpadStatus` returns 3 and `getLaunchpadCreatorReclaimed` returns 1.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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
amountQuote of tokenQuote moves into escrow and amountQuote / quotePerA raw tokenA is reserved for you. Reverts on: "Not authorized", "Launchpad not in funding state", "Launchpad has ended", "Creator cannot commit as a buyer", "Below minimum commitment", "Commit amount must be a multiple of quotePerA", "Commit buys zero A", "Exceeds remaining sale bucket", or "Insufficient quote token". Emits CommitMade with the updated total raisedQuote.
Example
// Buy 500 MYTOKEN at 2 SOUL each: 1,000 SOUL, a multiple of quotePerA (2)
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlaunchpad", "commit", [from, launchpadId, 100000000000]) // 1,000 SOUL raw
  .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), so buyers can back out before activation. The contract also accepts status 3 (cancelled), but a launchpad can only be cancelled while no buyer holds a commitment, so there is nothing to withdraw then. No time limit: it also works after endTime while the launchpad is still in funding. If you voted on an open dissolution proposal, the weight your vote was counted with is removed from the tally (4.1.5: the weight recorded at vote time, not your current commitment), and your vote flag and reward debt are 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
Your full committed quote comes back; raisedQuote, soldA and the buyer count drop. Reverts on: "Not authorized", "Can only withdraw during funding or after cancellation", or "No commitment found". Emits CommitWithdrawn.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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
Works before or after endTime while the status is 0. The pool is registered with soldA and raisedQuote as reserves, so it opens at the sale price. Reverts on: "Not authorized", "Only creator", "Launchpad not in funding state", "No buyers - use cancel", "Fill below minimum threshold - activate later or wait for sellout", "Sold A below min pool size" or "Raised quote below min pool size" (saturnadmin.getMinScaledPoolUnits: 100 whole tokens per side live). Emits LaunchpadActivated with poolId, soldA, raisedQuote, and unsoldReturned. After success `getLaunchpadStatus` returns 1 and `getLaunchpadPoolId` returns the new pool ID.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(creatorAddress, null, gasPrice, gasLimit)
  .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 on: "Not authorized", "Launchpad not in funding state", or "Launchpad has not reached endTime". Emits LaunchpadDissolved with reason 2. After this call participants use `claimFundingRefund` to recover funds.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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. A launchpad gets one proposal in its life: it never expires or closes and keeps collecting votes until executed, and a proposal opened during funding stays open after activation.

Parameters
NameTypeDescription
fromaddressA buyer's address; must be the transaction witness. Creator is excluded.
launchpadIdnumberID of the launchpad to target.
What to expect
Reverts on: "Not authorized", "Launchpad not in funding or active state", "Proposal already open", "Creator cannot propose dissolution", or "Not a buyer in this launchpad". Your vote weight is recorded at this moment (4.1.5). Emits LaunchpadDissolveProposed with the earliest execution timestamp (proposedAt + 259200). Event decoding: LaunchpadDissolveProposed.proposedAt is copied straight from Time.now(), so it arrives as a VM Timestamp (type 5), while fields computed from it arrive as a Number (type 3). Decode both.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(buyerAddress, null, gasPrice, gasLimit)
  .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 at the moment of voting; quote committed afterwards does not add to the vote. 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 on: "Not authorized", "Launchpad not in funding or active state", "No proposal open", "Creator cannot vote", "Not a buyer in this launchpad", or "Already voted". Emits LaunchpadDissolveVoted with the new cumulative vote total.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(buyerAddress, null, gasPrice, gasLimit)
  .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 on: "Not authorized", "Launchpad not in funding or active state", "No proposal open", "Timelock not elapsed (72h from proposal required)", "Creator cannot execute", "Caller not a buyer", or "Vote has not passed (need strict majority of buyer stake)" (votes * 2 must exceed getLaunchpadRaisedQuote). If the launchpad is active it also reverts on "Pool locked by bonds/rental/feeopts - resolve first" or "Pool locked by active reward campaign - resolve first". Emits LaunchpadDissolved with reason 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 = ScriptBuilder
  .begin()
  .allowGas(buyerAddress, null, gasPrice, gasLimit)
  .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
Pays weight * accFeePerShare / 10^12 − rewardDebt in tokenA and in tokenQuote (raw). Each harvest adds harvested * 10^12 / (raisedQuote * 2) to accFeePerShare; since 4.1.5 the part that rounds away is carried to the next harvest instead of being stranded in the contract. Reverts on: "Not authorized", "Launchpad not active", or "Not a member of this launchpad". Emits LaunchpadRewardsClaimed. Pending amounts may be 0 if no new fees have accrued since the last claim.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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
Payout per token (raw): dissolvedRes * weight / (raisedQuote * 2) plus your unclaimed fees (weight * accFeePerShare / 10^12 − rewardDebt). A buyer's commitment is zeroed; the creator's claim is recorded in a separate flag, so the creator's weight stays and buyers can still claim after the creator. Reverts on: "Not authorized", "Launchpad not dissolved", "Launchpad dissolved pre-activation - use claimFundingRefund", "Not a member or already claimed", or "Creator already claimed dissolution". Emits LaunchpadDissolutionClaimed with payoutA and payoutB amounts.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .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
The creator gets the full original tokensForSale of tokenA back; a buyer gets their full committed quote. Reverts on: "Not authorized", "Launchpad not dissolved", "Launchpad was activated - use claimDissolution", "Creator already reclaimed", or "Nothing to refund (not a buyer or already refunded)". Emits LaunchpadDissolutionClaimed (payoutA = tokenA, payoutB = quote).
Example
// Creator reclaims their tokenA:
const creatorTx = ScriptBuilder
  .begin()
  .allowGas(creatorAddress, null, gasPrice, gasLimit)
  .callContract("saturnlaunchpad", "claimFundingRefund", [creatorAddress, launchpadId])
  .spendGas(creatorAddress)
  .endScript();

// Buyer reclaims their tokenQuote:
const buyerTx = ScriptBuilder
  .begin()
  .allowGas(buyerAddress, null, gasPrice, gasLimit)
  .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 — Build tag. Mainnet and devnet report "saturnlaunchpad-4.1.5".
What to expect
Never reverts. Pure view.
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:100000000000000_price:2_sold:60000000000000_raised:120000000000000_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.
What to expect
Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.

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 buyers. commit adds 1 for a new buyer and withdrawCommit subtracts 1; claimDissolution and claimFundingRefund do not change it, so after a dissolution it still counts buyers who have claimed.

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 never changed after. It also selects the dissolution path: 0 on a dissolved launchpad means it dissolved before activation (claimFundingRefund), > 0 means claimDissolution. The creator's dissolution claim is tracked in a separate flag with no getter.

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", [launchpadId, buyerAddress]);

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 once a dissolution proposal has been opened, 0 before. It is never reset: the proposal stays open until executed and the flag stays 1 after dissolution.

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. withdrawCommit resets it to 0 (and removes the vote from the tally); claimDissolution and claimFundingRefund leave it at 1.

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, buyerAddress]);

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|100000000000000|2|100000000000000|200000000000000|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", [userAddress]);
Agent Automation · Contract #13

SaturnArbitrage

saturnarb saturnarb-4.4.0

Atomic cross-pool arbitrage for agents. No flash loans needed — the executor provides the starting capital in tokenStart, the contract routes it through two pools that share the same pair (buy cheap on one, sell expensive on the other), and the executor keeps 100% of the net gain. 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 — a failed attempt costs only gas (the swaps unwind too), and a successful one has already paid both pools' normal swap fees inside the swap outputs; there is no flash fee. Agents monitor on-chain prices, compute opportunities off-chain, and fire executeArbitrage when they find a profitable path. For arbitrage with borrowed capital see saturnflash and saturnstakearb.

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 (where the intermediate token is cheapest in tokenStart), then swaps the intermediate back to tokenStart on poolIdSell (where it is dearest). The intermediate token is inferred from poolIdBuy — whichever side isn't tokenStart. Both pools must contain the exact same pair. On success the whole finalAmount (your capital plus the entire profit) is returned to you; the protocol takes nothing beyond the normal swap fees inside each pool.

Parameters
NameTypeDescription
fromaddressExecutor address — must be a witness and must hold amountIn of tokenStart.
poolIdBuynumberPool where the intermediate token is cheap: tokenStart buys the most of it here (the highest saturnrouter.getPoolPrice(poolId, tokenStart)). The first hop swaps tokenStart into the intermediate token here.
poolIdSellnumberPool where the intermediate token is dear: tokenStart buys the least of it here (the lowest getPoolPrice(poolId, tokenStart)). 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, <= your wallet balance, and large enough that each leg clears saturnrouter.getMinRawForSwap() for its input token.
minProfitnumberMinimum acceptable profit (finalAmount − amountIn) in raw tokenStart units, checked as profit >= minProfit; the transaction reverts with 'Profit below minimum' if not met. Gas is paid in KCAL, so convert your gas cost into tokenStart if minProfit should cover it.
What to expect
amountIn tokenStart is moved from your wallet into liquidity custody, then routed through two swaps. If finalAmount > amountIn and profit >= minProfit, finalAmount tokenStart is sent back to you and the executor statistics are updated and ArbExecuted (poolIdBuy, poolIdSell, tokenStart, tokenMid, amountIn, midAmount, finalAmount, profit) is emitted. The method returns nothing, so read the profit from that event or from your balance change. Reverts on: "Not authorized" (from did not sign), "Amount must be > 0", "Same pool", "Insufficient balance", "tokenStart not in poolBuy", "Pool sell missing tokenStart", "Pool sell missing tokenMid", any saturnswap revert on either leg ("Pool not active", "Below minimum swap", "Output rounds to zero", "Admin fee rounds to zero", "Cannot drain pool"), "No arbitrage profit" or "Profit below minimum". A nested call for the same from fails with "Reentrancy detected". On revert nothing moves; only gas is spent.
Example
// Off-chain: read both pools (saturnrouter.getPoolFullInfo), simulate both legs
// with each pool's fee, and check your wallet holds amountIn of tokenStart.
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnarb", "executeArbitrage", [
    from,
    cheapPoolId,   // leg 1: SOUL -> tokenMid here (tokenMid is cheap in SOUL)
    richPoolId,    // leg 2: tokenMid -> SOUL here (same pair)
    "SOUL",        // tokenStart, taken from and returned to your wallet
    10000000000,   // amountIn: 100 SOUL (8 decimals)
    1000000        // minProfit: 0.01 SOUL, else the whole tx reverts
  ])
  .spendGas(from)
  .endScript();

updateExecutorShare()

WRITE
updateExecutorShare(newShare: number)

DEPRECATED — always reverts with "Deprecated in 4.1.0: executor keeps 100% of arb profit - no configurable split". Kept in the ABI for compatibility; there is no profit split to change.

Parameters
NameTypeDescription
newSharenumberIgnored.
What to expect
Always reverts. The executor always keeps 100% of the profit; getExecutorSharePer100() returns 100.

Arbitrage Stats

getExecutorSharePer100()

READ
getExecutorSharePer100(): number

Always returns 100. Since 4.1.0 the executor keeps the entire arbitrage profit; the method remains so older integrations that computed executorProfit = rawProfit * share / 100 keep working.

Returns
number — Always 100.
What to expect
Never reverts. Frozen at 100.
Example
const share = await readContract("saturnarb", "getExecutorSharePer100", []);
// 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 profit (finalAmount − amountIn, after both pools' swap fees) of every successful executeArbitrage, summed across all tokens in raw units. There is no split: all of it went to the executors.

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 (finalAmount − amountIn, before gas) earned by a specific executor. The executor keeps all of it; there is no split.

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]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnarb-4.4.0". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnarb-4.4.0".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnarb", "getContractVersion", []);
// "saturnarb-4.4.0"
Agent Automation · Contract #14

SaturnLimit

saturnlimit saturnlimit-4.1.4

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), an optional maximum output ceiling, 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 against that pool, the owner receives the output minus the bounty and the executor keeps the bounty: bountyPer10k (at most 500 = 5%) of the surplus above minAmountOut, so the owner never gets less than their limit and an order filled exactly at its limit pays no bounty. Owners can cancelOrder at any time for a full refund, and once an order's expiry has passed anyone can call expireOrder to send the deposit back to the owner (the caller earns nothing).

Order Lifecycle

placeOrderV2()

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

Deposit tokenIn and create a limit order against one specific pool. The swap fires when an agent calls executeOrder and the pool's current output for amountIn is at least minAmountOut (the constant-product slippage check enforces this). maxAmountOut is an optional ceiling: when non-zero, an execution that would deliver more than it is refused, which stops an order from being filled at an absurd price through a manipulated or nearly empty pool. Both tokens must belong to the pool's pair. amountIn must be at least saturnrouter.getMinRawForSwap(tokenIn), the swap engine's minimum, or the order could never execute. bountyPer10k must be between 0 and 500.

Parameters
NameTypeDescription
fromaddressOrder owner (witness).
poolIdnumberPool the order executes against (must be active).
tokenInstringToken deposited / sold.
tokenOutstringToken wanted.
amountInnumberRaw amount of tokenIn to escrow.
minAmountOutnumberMinimum raw tokenOut you accept (the limit price); must be > 0.
maxAmountOutnumberOptional ceiling on the fill; 0 = no ceiling, otherwise must exceed minAmountOut.
bountyPer10knumberExecutor reward: this share (per 10,000) of the output above minAmountOut, 0..500 (5%); a negative value is refused. Paid in tokenOut.
expiryTimenumberUnix time after which the order can be expired; 0 = never.
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 the active list; totalOrdersPlaced increments. Reverts on: "Not authorized", "Amount must be > 0", "Min output must be > 0", "Same token", "Bounty cannot be negative", "Max bounty: 5% (500 per 10k)", "Max output must exceed min output", "Pool not active", "tokenIn not in pool pair", "tokenOut not in pool pair", "Amount is below the minimum swap", "Insufficient balance", or "Expiry must be in the future". Emits OrderPlaced.
Example
// Sell 100 SOUL for at least 2,500 KCAL (25 KCAL per SOUL; mainnet pool 12 paid about 21.7 on 2026-09-28), 0.5% bounty, no ceiling, no expiry
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnlimit", "placeOrderV2", [from, poolId, "SOUL", "KCAL",
    10000000000,    // amountIn: 100 SOUL (8 decimals)
    25000000000000, // minAmountOut: 2,500 KCAL (10 decimals), above today's price
    0,              // maxAmountOut: no ceiling
    50,             // bountyPer10k: 0.5% of whatever the fill pays above 2,500 KCAL
    0])             // expiryTime: never
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

placeOrder()

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

DEPRECATED — always reverts with "Deprecated in 4.1.0: use placeOrderV2 (adds maxAmountOut ceiling and pool-pair validation)".

Parameters
NameTypeDescription
fromaddressIgnored.
poolIdnumberIgnored.
tokenInstringIgnored.
tokenOutstringIgnored.
amountInnumberIgnored.
minAmountOutnumberIgnored.
bountyPer10knumberIgnored.
expiryTimenumberIgnored.
What to expect
Always reverts. Call placeOrderV2() instead.

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), the order leaves the active list and your full amountIn of tokenIn is transferred back to you in a single Token.transfer. Reverts on "Not authorized", "Only order owner" or "Order not active". Emits OrderCancelled.
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)

Anyone executes an active, unexpired order and earns its bounty. The order is marked executed and taken off the active list first; then the escrowed tokenIn is sent to saturnliquidity and swapped through saturnswap.swapFromContract on the order's pool with minAmountOut as the slippage floor. If the pool pays less than minAmountOut, or more than a non-zero maxAmountOut, the whole transaction reverts and only gas is spent. The bounty comes from the surplus: bounty = (amountOut − minAmountOut) × bountyPer10k / 10000, paid in tokenOut to the executor; the owner receives amountOut − bounty, never less than minAmountOut. An order filled exactly at its limit pays no bounty. The owner may execute their own order.

Parameters
NameTypeDescription
fromaddressExecutor — any signer (witness). Does not need to own the order; receives the bounty.
orderIdnumberAn active order whose expiryTime has not passed.
What to expect
amountIn of tokenIn moves from the escrow to saturnliquidity and swapFromContract returns amountOut. bounty = (amountOut − minAmountOut) × bountyPer10k / 10000 goes to the executor (from) in tokenOut; ownerPayout = amountOut − bounty goes to the owner. Status becomes 1 (executed), the order leaves the active list and totalOrdersExecuted increments. Emits OrderExecuted(orderId, amountOut, ownerPayout, bounty). Reverts on "Not authorized" (from must sign), "Order not active" (already executed, cancelled or expired), "Order expired" (now >= expiryTime), "Pool not active", "Below minimum swap", "Slippage exceeded" (the pool pays less than minAmountOut) or "Output exceeds ceiling".
Example
// Off-chain agent loop:
//   - read getActiveOrdersData() (one row per live order)
//   - skip rows whose expiry is set and has passed (executeOrder needs now < expiry)
//   - quote amountIn on poolId with the swap engine's math: need out >= minOut and (maxOut == 0 or out <= maxOut)
//   - bounty = (out - minOut) * bountyPer10k / 10000 raw tokenOut; fire only when it beats the gas
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 signer: must be a witness but need not own the order. Earns nothing.
orderIdnumberAn active order with expiry > 0 and now >= expiry.
What to expect
Status flips to 3 (expired), the order leaves the active list and the owner's full tokenIn deposit is returned in one transfer. The caller receives nothing. Reverts on "Not authorized", "Order not active", "Order has no expiry" (expiryTime 0) or "Order not expired yet" (now < expiryTime). Emits OrderExpired under the owner's address.
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>_maxOut:<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. Reverts for an id that was never created: the address slot is empty, so the read faults with "Invalid cast", while every number and string getter of the same id returns 0 or "". Check the id against the contract's id list or next-id first.
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. An id that was never assigned also reads 0, so check orderId < getNextOrderId() or use the active-order views.

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 placeOrderV2 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 the id of every live (status 0) order. executeOrder, cancelOrder and expireOrder remove an order from the list, so no status filter is needed; an order whose expiry has passed stays listed (still status 0) until someone calls expireOrder.

Returns
number* — Iterable of order IDs.
What to expect
Keepers usually prefer getActiveOrdersData(), which returns every field in one call: skip rows whose expiry has passed, quote the swap and fire executeOrder when the bounty beats gas. For one wallet's orders use getActiveOrderIdsByUser.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnlimit", "getAllActiveOrderIds", [])
  .endScript();

getOrderMaxAmountOut()

READ
getOrderMaxAmountOut(orderId: number): number

Fill ceiling in raw tokenOut; 0 means no ceiling. executeOrder() reverts with "Output exceeds ceiling" when the pool would pay more.

Parameters
NameTypeDescription
orderIdnumberId to inspect.
Returns
number — Raw tokenOut or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const maxOut = await readContract("saturnlimit", "getOrderMaxAmountOut", [orderId]);

getOrderBountyPer10k()

READ
getOrderBountyPer10k(orderId: number): number

Executor bounty in units of 1/10,000 of the output above minAmountOut (0..500). Bounty = (amountOut − minAmountOut) × this / 10000.

Parameters
NameTypeDescription
orderIdnumberId to inspect.
Returns
number — Bounty per 10,000.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const bounty = await readContract("saturnlimit", "getOrderBountyPer10k", [orderId]);

getOrderExpiry()

READ
getOrderExpiry(orderId: number): number

Unix time after which anyone may call expireOrder(); 0 = never expires.

Parameters
NameTypeDescription
orderIdnumberId to inspect.
Returns
number — Unix seconds or 0.
What to expect
Never reverts; 0 / empty for unknown ids.
Example
const expiry = await readContract("saturnlimit", "getOrderExpiry", [orderId]);

getActiveOrderCount()

READ
getActiveOrderCount(): number

Number of orders currently in the active list — the upper bound of the scan an executor bot has to do.

Returns
number — Count of active orders.
What to expect
Never reverts.
Example
const n = await readContract("saturnlimit", "getActiveOrderCount", []);

getActiveOrderIdsByPool()

READ
getActiveOrderIdsByPool(poolId: number): number*

Yields the active order ids that target one pool — the natural query for a bot watching a single market.

Parameters
NameTypeDescription
poolIdnumberPool to filter on.
Returns
number* — Stream of order ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnlimit", "getActiveOrderIdsByPool", [poolId]);

getActiveOrderIdsByUser()

READ
getActiveOrderIdsByUser(user: address): number*

Yields the active order ids owned by a wallet.

Parameters
NameTypeDescription
useraddressOrder owner.
Returns
number* — Stream of order ids.
What to expect
Never reverts.
Example
const ids = await readContract("saturnlimit", "getActiveOrderIdsByUser", [userAddress]);

getActiveOrdersData()

READ
getActiveOrdersData(): string*

One pipe-delimited row per active order: orderId|poolId|owner|tokenIn|tokenOut|amountIn|minAmountOut|maxAmountOut|bountyPer10k|expiry|status. Everything an executor needs to evaluate the whole book in one call.

Returns
string* — Stream of "orderId|poolId|owner|tokenIn|tokenOut|amountIn|minAmountOut|maxAmountOut|bountyPer10k|expiry|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnlimit", "getActiveOrdersData", []);
for (const r of rows) { const [id, pool, owner, tIn, tOut, amtIn, minOut, maxOut, bounty, expiry, status] = r.split("|"); /* quote pool, compare */ }

getActiveOrdersDataByUser()

READ
getActiveOrdersDataByUser(user: address): string*

Same row layout as getActiveOrdersData() filtered to one owner — a user's open-orders panel in one call.

Parameters
NameTypeDescription
useraddressOrder owner.
Returns
string* — Stream of "orderId|poolId|owner|tokenIn|tokenOut|amountIn|minAmountOut|maxAmountOut|bountyPer10k|expiry|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnlimit", "getActiveOrdersDataByUser", [userAddress]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnlimit-4.1.4". A deployment that still reports "saturnlimit-4.1.3" accepts a negative bountyPer10k and does not check the swap minimum at placement. Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnlimit-4.1.4".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnlimit", "getContractVersion", []);
// "saturnlimit-4.1.4"
Agent Automation · Contract #15

SaturnPredict

saturnpredict saturnpredict-4.1.8

Bet on whether a pool's cumulative provider fees will grow by at least a threshold before a deadline. At market creation the contract snapshots the pool's lifetime provider fees (both tokens, via saturnfees.getProviderLifetimeFees). Users bet OVER or UNDER with the market's bet token. After the endTime, the first caller of claimWinnings triggers self-resolution — the contract re-reads the metric, compares the delta with the 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 has no bets when the market ends, nobody can win: every bettor gets a full, fee-free refund through claimRefund or claimWinnings (status 4). The metric is read at the first claim after endTime, not at endTime itself, so fees earned in between still count. Since 4.1.7 a market stops taking bets on both sides once its fees have already grown by the threshold, and the creator picks the length down to the admin floor getMinDuration() (0 on mainnet). Since 4.1.8 the fee metric is summed in 8-decimal scaled units as stored (fee basis 2). Metric types 2 and 3 (reserve product, price) still exist in the ABI but are refused at creation because live reserves can be manipulated at settlement.

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 v4 pool's provider fees. metricType must be 1 — the only metric that cannot be pushed around at settlement, because lifetime fees never decrease. The metric is saturnfees.getProviderLifetimeFees(poolId, tokenA) + getProviderLifetimeFees(poolId, tokenB), both already in 8-decimal scaled units (fee basis 2 since 4.1.8). Its current value is snapshotted so the first claim after endTime can compute the delta. Since 4.1.7 there is no fixed one-hour minimum: the market only has to last getMinDuration() seconds (0 on mainnet, so any future endTime works).

Parameters
NameTypeDescription
fromaddressMarket creator — must be a witness. Has permission to cancelMarket before bets are placed.
poolIdnumberThe active pool whose metric is being forecast.
metricTypenumberMust be 1: the pool's lifetime provider fees, tokenA + tokenB, in 8-decimal scaled units. 2 (k = resA * resB) and 3 (price) are refused at creation.
thresholdnumberHow much the metric must INCREASE over the snapshot for OVER to win (delta >= threshold). Must be > 0. Units: 8-decimal scaled fees with tokenA and tokenB added together, so 100000000 = 1 whole token of fees.
endTimenumberUnix seconds when betting closes and claims open. Must be in the future and at least getMinDuration() seconds from now (0 on mainnet: no minimum).
betTokenstringToken symbol used for all bets and payouts. Must exist on chain, else "Token does not exist: <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 and totalMarketsCreated increments; the fee basis is recorded as 2. Reverts on: "Not authorized", "Pool not active", "Metric type must be 1 (fees), 2 (k=resA*resB), or 3 (priceA/B)", "Only metric type 1 (cumulative pool fees) is permitted: types 2 and 3 read live reserves and are manipulable at settlement", "Threshold must be > 0", "End time must be in the future", "Market is shorter than the minimum duration", "Min bet must be > 0", or "Token does not exist: <symbol>". The method returns nothing: read the new id from MarketCreated (marketId, poolId, metricType, threshold, endTime, betToken, minBet, snapshotValue).
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnpredict", "createMarket", [
    from,
    poolId,
    1,                     // metricType: only 1 (lifetime pool fees) is accepted
    500000000,             // threshold: fees (tokenA + tokenB) must grow by 5.0, 8-decimal scaled
    Math.floor(Date.now()/1000) + 86400, // endTime: 24h
    "SOUL",                // betToken
    100000000              // minBet: 1 SOUL (8 decimals)
  ])
  .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; emits BetPlaced (side 1). Reverts on: "Not authorized", "Market not open", "Betting period ended" (now >= endTime), "Below minimum bet", "Outcome already decided: the pool's fees already beat the threshold" (4.1.7: a lifetime-fee market takes no more bets once its fees have grown by the threshold), or "Insufficient balance".
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; emits BetPlaced (side 2). Same reverts as betOver, including "Outcome already decided: the pool's fees already beat the threshold": once OVER has already won, UNDER bets are refused too.
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 resolves it. If either side has no bets, the market becomes a refund market (status 4). Otherwise the metric is read live, delta = max(0, current − snapshot), and status becomes 1 (OVER won, delta >= threshold) or 2 (UNDER won). The metric is read at that first claim, not at endTime, so fees earned after endTime still count until someone claims. Every caller (first or later) then receives their payout once.

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. Refund market (status 4): your over + under bets come back in full, no fee (RefundClaimed). If you bet on the losing side the call still completes — it just pays nothing. If you won: grossPayout = yourBet * (totalOver + totalUnder) / winningSideTotal, fee = grossPayout * getProtocolFeePer10k() / 10000 (200 = 2% live) goes to the saturnadmin admin address, and the net reaches your wallet (WinningsClaimed). All amounts are raw betToken. Reverts on: "Not authorized", "Market not ended yet", "Already claimed" or "Market cancelled".
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. Works before or after endTime. Reverts on: "Not authorized", "Only market creator", "Market not open", "Bets already placed (over)" or "Bets already placed (under)".
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, no fee. hasClaimed is flipped to prevent double-refund, and the first refund marks the market status 4. Reverts on: "Not authorized", "Market not ended yet", "Already claimed", "Market already resolved with both sides", or "Both sides have bets - use claimWinnings". claimWinnings pays the same refund on a one-sided market.
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|3>_threshold:<n>_end:<unix>_snapshot:<n>_over:<raw>_under:<raw>_status:<0-4>_token:<betToken>_decimals:<n>_minBet:<raw>_feeBasis:<0|1|2>
What to expect
Split on '_' and then on ':' to decode. Status: 0=open, 1=resolved_over, 2=resolved_under, 3=cancelled, 4=refund (one side had no bets). over, under and minBet are raw units of token; decimals is that token's decimals. feeBasis says how threshold and snapshot count fees: 2 = lifetime fees in 8-decimal scaled units (every market created on 4.1.8), 1 = lifetime fees scaled a second time, 0 = pending claimable fees (older markets). 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 (pool provider fees) for every market createMarket() accepts today. Markets created before metric 1 became the only choice can still show 2 (k = resA * resB) or 3 (price) and resolve on that metric; getOpenMarketsData on devnet still lists open metric-2 markets.
What to expect
Label the bet question as '... will pool fees grow by at least X before <endTime>?'.
Example
const kind = await readContract("saturnpredict", "getMarketMetricType", [marketId]);

getMarketThreshold()

READ
getMarketThreshold(marketId: number): number

Returns the required delta for OVER to win, in metric units (fee basis 2: 8-decimal scaled fees, tokenA + tokenB added).

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 metric value captured at market creation, in the same units as the threshold (see getMarketFeeBasis).

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, every bettor gets a full refund via claimRefund or claimWinnings.
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, 4=refund (one side had no bets).
What to expect
0 means bets still accepted (before endTime, and until a lifetime-fee market's fees beat the threshold); after endTime 0 means nobody has claimed yet. 1 / 2 mean claim winnings now. 4 means every bettor takes a full refund with claimWinnings or claimRefund.
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, per 10,000 (200 = 2% on mainnet and devnet; the admin can set 0..1000 with updateProtocolFee). Refunds pay no fee. The fee is read at claim time.

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]);

getOpenMarketsData()

READ
getOpenMarketsData(): string*

One pipe-delimited row per market still in status 0 (open — not yet resolved, whether or not endTime has passed): marketId|poolId|metricType|threshold|endTime|snapshotValue|totalOver|totalUnder|status. Scans ids 1..nextMarketId-1.

Returns
string* — Stream of "marketId|poolId|metricType|threshold|endTime|snapshotValue|totalOver|totalUnder|status" rows.
What to expect
Never reverts. Markets past endTime remain listed until the first claimWinnings() or claimRefund() resolves them.
Example
const rows = await readContract("saturnpredict", "getOpenMarketsData", []);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag of this contract. The mainnet and devnet deployments documented here report "saturnpredict-4.1.8". Compare it against the value you developed against before trusting method semantics after an upgrade.

Returns
string — Build tag, e.g. "saturnpredict-4.1.8".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnpredict", "getContractVersion", []);
// "saturnpredict-4.1.8"

getMarketBetToken()

READ
getMarketBetToken(marketId: number): string

Returns the token symbol this market takes bets in and pays out in. Added in 4.1.6.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
string — Bet token symbol; empty string for an unknown id.
What to expect
Never reverts. Every amount of the market (bets, totals, minBet, payouts) is in raw units of this token.
Example
const token = await readContract("saturnpredict", "getMarketBetToken", [marketId]);

getMarketMinBet()

READ
getMarketMinBet(marketId: number): number

Returns the smallest amount one betOver or betUnder call may stake, in raw bet-token units. Added in 4.1.6.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Minimum bet, raw bet-token units; 0 for an unknown id.
What to expect
Never reverts. A smaller bet reverts with "Below minimum bet".
Example
const minBet = await readContract("saturnpredict", "getMarketMinBet", [marketId]);

getMarketBetDecimals()

READ
getMarketBetDecimals(marketId: number): number

Returns the decimals of the market's bet token, read live with Token.getDecimals. Added in 4.1.6. Divide raw amounts by 10^decimals for display.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — Bet token decimals; 0 when the market does not exist.
What to expect
Never reverts; an unknown id returns 0.
Example
const dec = await readContract("saturnpredict", "getMarketBetDecimals", [marketId]);
const over = await readContract("saturnpredict", "getMarketTotalOver", [marketId]);
const overWhole = Number(over) / 10 ** Number(dec);

getMarketFeeBasis()

READ
getMarketFeeBasis(marketId: number): number

Returns how metric 1 counts this market's fees (added in 4.1.8). 2 = lifetime provider fees summed as stored, already in 8-decimal scaled units (every market created on 4.1.8). 1 = lifetime fees scaled a second time (markets from 4.1.x before 4.1.8). 0 = pending claimable fees scaled again (the oldest markets). A market's snapshot, threshold and settlement always use its own basis.

Parameters
NameTypeDescription
marketIdnumberThe market to inspect.
Returns
number — 0, 1 or 2.
What to expect
Never reverts; 0 for an unknown id. Only basis 1 and 2 markets stop taking bets early once their fees beat the threshold.
Example
const basis = await readContract("saturnpredict", "getMarketFeeBasis", [marketId]);

getMinDuration()

READ
getMinDuration(): number

Returns the admin's floor on market length in seconds (added in 4.1.7). createMarket requires endTime − now >= this value. 0 means no minimum: any future endTime works.

Returns
number — Seconds; 0 on mainnet, 3 on devnet.
What to expect
Never reverts.
Example
const minDur = await readContract("saturnpredict", "getMinDuration", []);
const endTime = Math.floor(Date.now() / 1000) + Math.max(Number(minDur) + 60, 86400);

Admin & Internal

updateProtocolFee()

WRITE
updateProtocolFee(newFee: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must sign. Sets the fee taken from winning payouts, per 10,000, from 0 to 1000 (10%). The fee is read at claim time, so it applies to every later winning claim, including markets already resolved. Refunds never pay a fee.

Parameters
NameTypeDescription
newFeenumberNew fee per 10,000, 0..1000. Live value is 200 (2%).
What to expect
Reverts on: "Only admin", "Fee cannot be negative" or "Max fee: 10%". Takes and releases the saturnadmin reentrancy guard for the admin address. Emits no event.

setMinDuration()

WRITE
setMinDuration(seconds: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must sign. Sets the minimum market length in seconds that createMarket enforces. 0 removes the floor. Existing markets are not affected.

Parameters
NameTypeDescription
secondsnumberMinimum market length in seconds, 0..31536000 (1 year). Live: 0 on mainnet, 3 on devnet.
What to expect
Reverts on: "Only admin", "Min duration cannot be negative" or "Min duration: at most 1 year". Emits no event.
Agent Automation · Contract #16

SaturnVaults

saturnvaults saturnvaults-4.2.0

Profit-only strategy vaults run by an agent (a bot, an AI or any wallet). The agent opens a vault in one base token with a name, a performance fee (1–30%), a minimum deposit and an optional hold time; anyone can deposit that token for shares. Only the vault's agent can trade its money, and only with agentArb (two pools) or agentArb3 (three pools): each trade starts and ends in the base token and must end with more of it than it started, measured on the contract's own base balance rather than on swap return values, or the whole transaction reverts. A trade may use at most the vault's own NAV (every vault's deposits share one contract balance), and a trade lock blocks deposits, withdrawals and nested trades while it runs. The agent's fee is taken from each trade's profit and paid at once; the rest stays in the vault, so the share price (8 decimals, starts at 1.0) only goes up. Depositors exit with withdrawV2 once the vault's hold has run from their latest deposit; the hold can only be lowered, and a closed vault holds nobody. The fee and minimum deposit are fixed for the life of the vault. Any token that exists on the chain can be a base token, so depositors should trust the token as well as the agent. 4.2.0 retires agentRoundTrip and the high-water-mark fee; the legacy views keep their layout, and getVaultSharePrice / getVaultStats / getAllVaultsStats carry the 4.2.0 data.

Vault Lifecycle

createVault()

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

Legacy creator, kept for ABI compatibility: opens a vault with no name and no hold time (depositors can withdraw at any time). Prefer createVaultV2, which sets both. from becomes the vault's agent, the only address that can trade, rename or close the vault or lower its hold; the agent cannot be changed later.

Parameters
NameTypeDescription
fromaddressAgent address (witness). Becomes the vault's permanent agent.
baseTokenstringSymbol of the vault's only token: deposits, withdrawals, trades, profit and share price are all in it. Any token that exists on the chain is accepted (saturnpools.validateTokenSymbol only checks that it exists).
perfFeePer10knumberAgent's cut of each trade's profit, per 10,000: 100 (1%) to 3000 (30%). Fixed for the life of the vault.
minDepositnumberMinimum raw amount per deposit() call; must be > 0. Fixed for the life of the vault.
What to expect
Assigns vaultId = getNextVaultId() in status 0 (active) and emits VaultCreated(vaultId, baseToken, perfFeePer10k, minDeposit). The method returns nothing, so read the new id from that event. getVaultName reads "" and getVaultMinHold 0 for such a vault; setVaultName can add a name later, but the hold can never be raised. Reverts: "Not authorized", "Token does not exist: <symbol>", "Min performance fee: 1%", "Max performance fee: 30%", "Min deposit must be > 0".
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "createVault", [
    from,
    "SOUL",
    1500,            // 15% of each trade's profit to the agent
    10_00000000      // min deposit 10 SOUL (raw, 8 decimals)
  ])
  .spendGas(from)
  .endScript();

deposit()

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

Deposits the vault's base token and mints shares at the current share price. The first deposit into an empty vault mints amount × 10,000 shares (share price 1.0); later deposits mint amount × totalShares / totalDeposits. Every deposit restarts the depositor's hold time for the whole position, so a top-up locks the older shares again too. The agent may deposit into its own vault like anyone else.

Parameters
NameTypeDescription
fromaddressDepositor (witness) holding at least amount of the base token.
vaultIdnumberAn active vault (status 0).
amountnumberRaw base token to deposit; must be >= getVaultMinDeposit(vaultId).
What to expect
Pulls amount into the contract, credits the new shares, adds amount to the vault's totalDeposits and to your lifetime getUserDepositAmount, sets your unlock time to now + the hold in force (getUserUnlockAt), counts you once in getVaultDepositorCount, and emits VaultDeposit(vaultId, amount, sharesMinted, newTotalDeposits, newTotalShares). Reverts: "Not authorized", "Vault not active", "Below minimum deposit", "Not during a vault trade", "Insufficient balance", "Deposit too small for shares". Measured on devnet: about 0.024 KCAL.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "deposit", [
    from,
    vaultId,
    25_00000000      // 25 SOUL (raw, 8 decimals)
  ])
  .spendGas(from)
  .endScript();

withdraw()

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

RETIRED: always reverts with "Deprecated in 4.1.0: use withdrawV2 (adds minAmountOut slippage guard)".

Parameters
NameTypeDescription
fromaddressIgnored.
vaultIdnumberIgnored.
sharesToRedeemnumberIgnored.
What to expect
Always reverts. Call withdrawV2 instead.

withdrawV2()

WRITE
withdrawV2(from: address, vaultId: number, sharesToRedeem: number, minAmountOut: number)

Burns shares for the matching slice of the vault's base token: payout = sharesToRedeem × totalDeposits / totalShares. Works in any status. In an active vault it reverts until the vault's hold has run from your latest deposit (see getUserUnlockAt); a closed vault holds nobody. Since 4.2.0 there is no fee to settle here: the agent is paid per trade.

Parameters
NameTypeDescription
fromaddressDepositor (witness).
vaultIdnumberVault to exit.
sharesToRedeemnumberShares to burn; > 0 and <= getUserShares(vaultId, from).
minAmountOutnumberMinimum raw payout you accept; 0 disables the check. The share price never falls, so a fresh quote (getUserValue for a full exit) is a safe floor.
What to expect
Transfers the payout, lowers totalShares and totalDeposits, takes you off getVaultDepositorCount when your shares reach 0, and emits VaultWithdraw(vaultId, sharesRedeemed, amountOut, newTotalDeposits, newTotalShares). When the last share leaves, the share price reads 1.0 again (the history stays in the trade log). Reverts: "Not authorized", "Must redeem > 0 shares", "Not during a vault trade", "Insufficient shares", "Hold time not over: this vault keeps each deposit for its hold time", "Payout rounds to zero", "Slippage exceeded". Measured on devnet: about 0.024 KCAL.
Example
// Full exit: quote first; the share price never falls, so the quote is a safe floor
const shares = await readContract("saturnvaults", "getUserShares", [vaultId, from]);
const minOut = await readContract("saturnvaults", "getUserValue", [vaultId, from]);
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "withdrawV2", [from, vaultId, shares, minOut])
  .spendGas(from)
  .endScript();

agentSwap()

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

RETIRED: always reverts with "Deprecated in 4.1.0: use agentArb or agentArb3 (agentRoundTrip was retired in 4.2.0)". A one-way swap would leave a vault holding a token it cannot price.

Parameters
NameTypeDescription
fromaddressIgnored.
vaultIdnumberIgnored.
poolIdnumberIgnored.
amountInnumberIgnored.
tokenInstringIgnored.
tokenOutstringIgnored.
minAmountOutnumberIgnored.
What to expect
Always reverts. Call agentArb or agentArb3 instead.

agentRoundTrip()

WRITE
agentRoundTrip(from: address, vaultId: number, poolId: number, baseIn: number, riskToken: string, minRiskOut: number, minBaseBack: number)

RETIRED in 4.2.0: always reverts with "Retired in 4.2.0: use agentArb (two pools) or agentArb3 (three pools); a trade must end in profit". A round trip through one pool always returns less than it takes (two swap fees), so it could only cost depositors money.

Parameters
NameTypeDescription
fromaddressIgnored.
vaultIdnumberIgnored.
poolIdnumberIgnored.
baseInnumberIgnored.
riskTokenstringIgnored.
minRiskOutnumberIgnored.
minBaseBacknumberIgnored.
What to expect
Always reverts. Call agentArb or agentArb3 instead.

closeVault()

WRITE
closeVault(from: address, vaultId: number)

The agent closes its vault for good (there is no reopen). Closing stops deposits and trades and releases every depositor from the hold: withdrawV2 works at once, at the current share price.

Parameters
NameTypeDescription
fromaddressThe vault's agent (witness).
vaultIdnumberAn active vault.
What to expect
Status becomes 1 and VaultClosed(vaultId) is emitted. The money stays in the vault until each depositor calls withdrawV2; nothing is paid out automatically. The protocol admin can also close any vault (forceCloseVault, admin-only) with the same effect. Reverts: "Not authorized", "Only vault agent", "Vault not active". Measured on devnet: about 0.017 KCAL.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "closeVault", [from, vaultId])
  .spendGas(from)
  .endScript();

Vault Views

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag. The deployments documented here report "saturnvaults-4.2.0". A deployment that still reports "saturnvaults-4.1.3" has none of the methods added in 4.2.0 (createVaultV2 onward) and still runs agentRoundTrip and the high-water-mark fee, so check this before relying on them.

Returns
string — Build tag, e.g. "saturnvaults-4.2.0".
What to expect
Never reverts. Pure view.
Example
const v = await readContract("saturnvaults", "getContractVersion", []);
// "saturnvaults-4.2.0"

getVaultInfo()

READ
getVaultInfo(vaultId: number): string

Legacy one-string snapshot, layout unchanged since 4.1.x. The hwm field is no longer used (it stays at 10000). Use getVaultStats for the full 4.2.0 picture.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
string — base:<symbol>_deposits:<raw>_shares:<total>_hwm:<legacy, unused>_perfFee:<per10k>_status:<0|1>
What to expect
Split on '_' and then on ':'. Never reverts: an id that was never created reads base: (empty) and status 0, so check vaultId < getNextVaultId() first.
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 vault's agent: its creator, the only address that can call agentArb, agentArb3, setVaultName, setVaultMinHold and closeVault. It cannot be changed.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
address — The vault's agent.
What to expect
Reverts for an id that was never created.
Example
const agent = await readContract("saturnvaults", "getVaultAgent", [vaultId]);

getVaultBaseToken()

READ
getVaultBaseToken(vaultId: number): string

Returns the symbol of the vault's base token. Deposits, withdrawals, trades, profit and the share price are all in this token.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
string — Base token symbol; "" for an id that was never created.
What to expect
Any existing token can be a base token. Check the token itself before depositing.
Example
const base = await readContract("saturnvaults", "getVaultBaseToken", [vaultId]);

getVaultTotalDeposits()

READ
getVaultTotalDeposits(vaultId: number): number

The vault's NAV in raw base token as booked by the contract: deposits − withdrawals + the depositors' part of every trade's profit. Agent fees are already paid out and not included. It is also the most one trade may put in (baseIn <= this).

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw NAV in base token.
What to expect
All vaults' money sits in one contract balance; this number is this vault's claim on it. Between calls a vault never holds any other token, so this is its full value.
Example
const nav = await readContract("saturnvaults", "getVaultTotalDeposits", [vaultId]);

getVaultTotalShares()

READ
getVaultTotalShares(vaultId: number): number

Total shares outstanding. The first deposit mints amount × 10,000 shares, so share amounts carry 4 more digits than the base token's raw units.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Total share supply.
What to expect
A depositor's fraction of the vault = getUserShares / getVaultTotalShares.
Example
const total = await readContract("saturnvaults", "getVaultTotalShares", [vaultId]);

getVaultHighWaterMark()

READ
getVaultHighWaterMark(vaultId: number): number

Legacy: the 4.1.x high-water mark. Unused since 4.2.0 (the agent's fee is taken per trade); it stays at 10000.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Legacy value, normally 10000.
What to expect
Kept for ABI compatibility. Do not use it for fee or price math.
Example
const hwm = await readContract("saturnvaults", "getVaultHighWaterMark", [vaultId]);

getVaultPerfFeePer10k()

READ
getVaultPerfFeePer10k(vaultId: number): number

The agent's cut of each trade's profit, per 10,000 (100–3000 = 1–30%). Fixed when the vault is created.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Performance fee, per 10,000.
What to expect
Show as (fee / 100) + '%'. On each trade the agent receives floor(profit × fee / 10000) at once; the rest raises the share price.
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
Deposits and trades need status 0; withdrawV2 works in both. An id that was never created also reads 0, so check vaultId < getNextVaultId().
Example
const status = await readContract("saturnvaults", "getVaultStatus", [vaultId]);

getNextVaultId()

READ
getNextVaultId(): number

Returns the id the next createVault / createVaultV2 call will get. Ids start at 1.

Returns
number — Next vault id.
What to expect
Existing ids are 1 … getNextVaultId() − 1. The create methods return nothing, so take a new vault's id from its VaultCreated event; reading this before sending races other creators.
Example
const next = await readContract("saturnvaults", "getNextVaultId", []);

getTotalVaultsCreated()

READ
getTotalVaultsCreated(): number

Returns the number of vaults ever created, closed ones included.

Returns
number — Cumulative vault count.
What to expect
Equals getNextVaultId() − 1.
Example
const n = await readContract("saturnvaults", "getTotalVaultsCreated", []);

getVaultNavPerShare()

READ
getVaultNavPerShare(vaultId: number): number

Legacy: totalDeposits × 10,000 / totalShares as a whole number. Because the first deposit mints amount × 10,000 shares, it reads the share price rounded down to an integer (1 for a new vault). Use getVaultSharePrice (8 decimals) instead.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Legacy value; 10000 when the vault has no shares.
What to expect
Never reverts. Kept for ABI compatibility only.
Example
const legacy = await readContract("saturnvaults", "getVaultNavPerShare", [vaultId]);

getUserShares()

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

Returns a depositor'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 the full balance to withdrawV2 for a full exit. Raw base value = shares × totalDeposits / totalShares (getUserValue computes it).
Example
const mine = await readContract("saturnvaults", "getUserShares", [vaultId, me]);

getUserDepositAmount()

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

Returns the total base token the user has ever deposited into the vault. It is never reduced on withdrawal, so after a partial exit it is not a cost basis.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
useraddressThe depositor address to look up.
Returns
number — Raw cumulative deposit amount.
What to expect
Display only. For realised PnL, track withdrawals (VaultWithdraw events) yourself.
Example
const invested = await readContract("saturnvaults", "getUserDepositAmount", [vaultId, me]);

getAllVaultIds()

READ
getAllVaultIds(): number*

Generator yielding every vault id ever created, in creation order.

Returns
number* — Iterable of vault ids.
What to expect
Includes closed vaults. getAllVaultsStats returns the ids together with all their data in one call.
Example
const script = ScriptBuilder
  .begin()
  .callContract("saturnvaults", "getAllVaultIds", [])
  .endScript();

getAllVaultsData()

READ
getAllVaultsData(): string*

Legacy batch view, layout unchanged: one row per vault, vaultId|agent|baseToken|totalDeposits|totalShares|highWaterMark|perfFeePer10k|status. highWaterMark is unused since 4.2.0. getAllVaultsStats returns 22 fields per vault, including the share price, hold time and trade counters.

Returns
string* — Stream of "vaultId|agent|baseToken|totalDeposits|totalShares|highWaterMark|perfFeePer10k|status" rows.
What to expect
Never reverts.
Example
const rows = await readContract("saturnvaults", "getAllVaultsData", []);

getUserVaultsData()

READ
getUserVaultsData(user: address): string*

Same legacy row layout as getAllVaultsData, for the vaults in which user holds shares: a depositor's portfolio in one call.

Parameters
NameTypeDescription
useraddressDepositor wallet.
Returns
string* — Stream of "vaultId|agent|baseToken|totalDeposits|totalShares|highWaterMark|perfFeePer10k|status" rows.
What to expect
Never reverts. Pair each vaultId with getUserValue and getUserUnlockAt for the 4.2.0 figures.
Example
const rows = await readContract("saturnvaults", "getUserVaultsData", [userAddress]);

Market Vaults & Profit-Only Trades (4.2.0)

createVaultV2()

WRITE
createVaultV2(from: address, baseToken: string, perfFeePer10k: number, minDeposit: number, name: string, minHoldSeconds: number)

Opens a vault for the market, with a name and a hold time. from becomes the vault's agent: the only address that can trade it (agentArb, agentArb3), rename it, lower its hold or close it; it cannot be handed over. The fee and minimum deposit are fixed for good and the hold can later only be lowered, so a vault's terms only get better for depositors.

Parameters
NameTypeDescription
fromaddressAgent address (witness), usually the bot's own key.
baseTokenstringSymbol of the vault's only token: deposits, withdrawals, trades, profit and share price are all in it. Any token that exists on the chain is accepted, so depositors should trust the token as well as the agent.
perfFeePer10knumberAgent's cut of each trade's profit, per 10,000: 100 (1%) to 3000 (30%).
minDepositnumberMinimum raw amount per deposit; must be > 0.
namestring1–40 characters of printable ASCII. The chain counts UTF-8 bytes (40 at most) and phantasma-sdk-ts sends one byte per character, so non-ASCII characters are stored garbled.
minHoldSecondsnumberHow long each deposit must stay before it can be withdrawn: 0 (any time) to 2592000 (30 days). Stops money jumping in just before a trade and out just after.
What to expect
Assigns vaultId = getNextVaultId() in status 0 and emits VaultCreated(vaultId, baseToken, perfFeePer10k, minDeposit). The method returns nothing, so read the new id from that event. Reverts: "Not authorized", "Name required", "Name: 40 characters at most", "Hold time cannot be negative", "Hold time: 30 days at most", "Token does not exist: <symbol>", "Min performance fee: 1%", "Max performance fee: 30%", "Min deposit must be > 0". Measured on devnet: about 0.021 KCAL.
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "createVaultV2", [
    from,
    "SOUL",
    1000,            // 10% of each trade's profit to the agent
    10_00000000,     // min deposit 10 SOUL (raw, 8 decimals)
    "SOUL gap bot",  // printable ASCII, 1-40 characters
    3600             // each deposit stays at least 1 hour
  ])
  .spendGas(from)
  .endScript();
// the new vaultId is in this transaction's VaultCreated event

setVaultName()

WRITE
setVaultName(from: address, vaultId: number, name: string)

The agent renames its vault, or names one made with createVault. Same rule as createVaultV2: 1–40 characters of printable ASCII.

Parameters
NameTypeDescription
fromaddressThe vault's agent (witness).
vaultIdnumberA vault the agent runs, open or closed.
namestringNew name, 1–40 printable ASCII characters.
What to expect
No event; read it back with getVaultName. Reverts: "Not authorized", "Only vault agent", "Name required", "Name: 40 characters at most".
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "setVaultName", [from, vaultId, "SOUL gap bot v2"])
  .spendGas(from)
  .endScript();

setVaultMinHold()

WRITE
setVaultMinHold(from: address, vaultId: number, minHoldSeconds: number)

The agent lowers its vault's hold time. The hold can only go down, and the lower value applies only once the hold in force now has run from this call: an agent cannot drop it to 0 and jump in and out of its own vault around a trade, and depositors see the change coming. A second lowering replaces a waiting one and waits again.

Parameters
NameTypeDescription
fromaddressThe vault's agent (witness).
vaultIdnumberA vault the agent runs.
minHoldSecondsnumberNew hold in seconds; >= 0 and below the hold in force now (getVaultMinHold).
What to expect
getVaultNextHold / getVaultNextHoldAt show the waiting value and when it applies (now + the current hold); getVaultMinHold switches to it at that time. A vault whose hold is 0 cannot be changed. The hold always runs from each depositor's latest deposit. No event. Reverts: "Not authorized", "Only vault agent", "Hold time cannot be negative", "Hold time can only go down".
Example
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "setVaultMinHold", [
    from,
    vaultId,
    600              // 10 minutes, applies once the current hold has run
  ])
  .spendGas(from)
  .endScript();

agentArb()

WRITE
agentArb(from: address, vaultId: number, baseIn: number, riskToken: string, poolBuy: number, poolSell: number, minProfit: number): number

Two-pool arbitrage with the vault's money. Swaps baseIn of the base token to riskToken on poolBuy, then all of it back to the base token on poolSell, in one transaction. The trade must end with more base token than it started (and at least minProfit more), measured as the change in the contract's own base balance rather than from swap return values, and the contract's riskToken balance may not end lower; otherwise the whole transaction reverts. The agent's fee (perfFeePer10k of the profit) is paid to it at once and the rest is added to the vault's NAV, so the share price only rises. Only the vault's agent can call it.

Parameters
NameTypeDescription
fromaddressThe vault's agent (witness).
vaultIdnumberAn active vault.
baseInnumberRaw base token to trade; > 0 and <= getVaultTotalDeposits(vaultId). A vault can never trade with other vaults' money, although they share the contract's balance.
riskTokenstringThe other token; both pools must hold exactly {baseToken, riskToken}.
poolBuynumberPool for leg 1 (base → riskToken), where riskToken is cheap.
poolSellnumberPool for leg 2 (riskToken → base), where riskToken is dear. Must differ from poolBuy.
minProfitnumberMinimum gross profit in raw base token, before the agent fee; 0 accepts any profit above 0.
Returns
number — The depositors' part of the profit (profit − agent fee), raw base token.
What to expect
Each leg is an ordinary swap in that pool and pays its swap fee. Legs run with no per-leg slippage floor (minAmountOut 1), so the profit check is the guard. A trade lock is held for the whole call: deposit, withdrawV2 and any nested trade revert while it runs. On success getVaultTotalDeposits grows by the depositors' part, a row is appended to the trade log (getVaultTrades), the counters in getVaultStats update, and VaultRoundTrip(vaultId, poolId = poolBuy, riskToken, baseIn, riskReceived, baseOut, netBasePnl = gross profit, isProfit = true) is emitted, plus VaultPerfFee(vaultId, feeAmount, share price before, share price after) when the fee is above 0. Reverts: "Not authorized", "A vault trade is already running", "Only vault agent", "Vault not active", "Amount must be > 0", "Trade exceeds vault NAV", "Risk token must differ from base", "Use two different pools", a swap-engine revert such as "Pool not active", "Token pair mismatch", "Below minimum swap", "Output rounds to zero" or "Admin fee rounds to zero", "Trade left less of the risk token than it found", "No arbitrage profit: a vault trade must end with more than it started", "Profit below minProfit", "Profit too small to share with depositors". A reverted trade moves no funds, but the agent still pays its gas. Measured on devnet: about 0.11 KCAL; a refused losing trade about 0.044 KCAL.
Example
// Buy KCAL with 50 SOUL where it is cheap (poolBuy), sell it where it is dear (poolSell)
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "agentArb", [
    from,            // the vault's agent
    vaultId,
    50_00000000,     // baseIn: 50 SOUL (raw, 8 decimals), <= getVaultTotalDeposits(vaultId)
    "KCAL",          // riskToken: both pools are SOUL/KCAL
    poolBuy,         // leg 1: SOUL -> KCAL
    poolSell,        // leg 2: KCAL -> SOUL
    10000000         // minProfit: 0.1 SOUL (raw), gross, before the agent fee
  ])
  .spendGas(from)
  .endScript();
// returns the depositors' part of the profit, raw SOUL

agentArb3()

WRITE
agentArb3(from: address, vaultId: number, baseIn: number, tokenX: string, tokenY: string, pool1: number, pool2: number, pool3: number, minProfit: number): number

Three-pool (triangle) arbitrage with the vault's money: base → tokenX on pool1, tokenX → tokenY on pool2, tokenY → base on pool3, in one transaction. Same rules as agentArb: the vault must end with more base token than it started (and at least minProfit more), measured on the contract's own balance, and neither the tokenX nor the tokenY balance may end lower, or the whole transaction reverts. Only the vault's agent can call it.

Parameters
NameTypeDescription
fromaddressThe vault's agent (witness).
vaultIdnumberAn active vault.
baseInnumberRaw base token to trade; > 0 and <= getVaultTotalDeposits(vaultId).
tokenXstringFirst intermediate token; must differ from the base token and from tokenY.
tokenYstringSecond intermediate token; must differ from the base token.
pool1numberPool holding exactly {baseToken, tokenX}.
pool2numberPool holding exactly {tokenX, tokenY}.
pool3numberPool holding exactly {tokenY, baseToken}.
minProfitnumberMinimum gross profit in raw base token, before the agent fee; 0 accepts any profit above 0.
Returns
number — The depositors' part of the profit (profit − agent fee), raw base token.
What to expect
Settles exactly like agentArb. The trade log row has pools "pool1,pool2,pool3" and route "tokenX,tokenY"; VaultRoundTrip carries poolId = pool1, riskToken = "tokenX,tokenY" and riskReceived = the tokenY sold into pool3. Reverts as agentArb, with "tokenX must differ from base", "tokenY must differ from base", "tokenX and tokenY must differ", "Trade left less of tokenX than it found" and "Trade left less of tokenY than it found" in place of the two-pool checks. Measured on devnet: about 0.15 KCAL; a refused losing trade about 0.058 KCAL.
Example
// SOUL -> KCAL -> TAZ -> SOUL around three pools (mainnet has all three pairs)
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnvaults", "agentArb3", [
    from,            // the vault's agent
    vaultId,
    20_00000000,     // baseIn: 20 SOUL (raw, 8 decimals)
    "KCAL",          // tokenX
    "TAZ",           // tokenY
    pool1,           // SOUL/KCAL
    pool2,           // KCAL/TAZ
    pool3,           // TAZ/SOUL
    5000000          // minProfit: 0.05 SOUL (raw), gross; your simulated profit minus a margin
  ])
  .spendGas(from)
  .endScript();

Market Views (4.2.0)

getVaultSharePrice()

READ
getVaultSharePrice(vaultId: number): number

Value of one share in base token with 8 decimals (100000000 = 1.0): totalDeposits × 10^12 / totalShares. It starts at 1.0 and never falls: trades only add to it, and deposits and withdrawals leave it unchanged (rounding can only nudge it up).

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Share price, 8 decimals; 100000000 when the vault has no shares.
What to expect
Raw base value of a position = shares × sharePrice / 10^12 (getUserValue computes it exactly). A vault emptied by withdrawals reads 1.0 again; its history stays in the trade log.
Example
const p = await readContract("saturnvaults", "getVaultSharePrice", [vaultId]);
const price = Number(p) / 1e8;   // 1.69918288 = +69.9% since the first deposit

getVaultName()

READ
getVaultName(vaultId: number): string

Returns the vault's name.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
string — Name; "" for a vault made with createVault and never renamed.
What to expect
Never reverts.
Example
const name = await readContract("saturnvaults", "getVaultName", [vaultId]);

getVaultMinHold()

READ
getVaultMinHold(vaultId: number): number

Returns the hold time in force now, in seconds (0 = withdraw any time). A lowered hold counts once its wait is over.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Seconds each deposit must stay.
What to expect
A closed vault holds nobody, whatever this reads.
Example
const hold = await readContract("saturnvaults", "getVaultMinHold", [vaultId]);

getVaultMinDeposit()

READ
getVaultMinDeposit(vaultId: number): number

Returns the minimum raw amount per deposit.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw base token.
What to expect
Fixed when the vault is created.
Example
const min = await readContract("saturnvaults", "getVaultMinDeposit", [vaultId]);

getVaultCreatedAt()

READ
getVaultCreatedAt(vaultId: number): number

Returns when the vault was created.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Unix seconds.
What to expect
Never reverts; 0 for an id that was never created.
Example
const t = await readContract("saturnvaults", "getVaultCreatedAt", [vaultId]);

getVaultTradeCount()

READ
getVaultTradeCount(vaultId: number): number

Returns the number of trades (agentArb / agentArb3) booked for the vault. Round trips made before 4.2.0 are not counted.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Trade count; also the index of the next trade log row.
What to expect
Rows 0 … count − 1 are readable with getVaultTrade / getVaultTrades.
Example
const n = await readContract("saturnvaults", "getVaultTradeCount", [vaultId]);

getVaultVolume()

READ
getVaultVolume(vaultId: number): number

Returns the sum of baseIn over the vault's trades.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw base token.
What to expect
Counts only what the vault put into its trades.
Example
const vol = await readContract("saturnvaults", "getVaultVolume", [vaultId]);

getVaultProfitToDepositors()

READ
getVaultProfitToDepositors(vaultId: number): number

Returns the total profit trades have added to the vault's NAV, after the agent fee. It includes the part earned by the agent's own shares; see getVaultProfitToOthers.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw base token.
What to expect
This + getVaultAgentFees = the vault's gross trading profit.
Example
const p = await readContract("saturnvaults", "getVaultProfitToDepositors", [vaultId]);

getVaultAgentFees()

READ
getVaultAgentFees(vaultId: number): number

Returns the total fees paid to the agent from trade profits.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw base token.
What to expect
Paid at each trade; nothing is owed to the agent at any time.
Example
const fees = await readContract("saturnvaults", "getVaultAgentFees", [vaultId]);

getVaultLastTradeAt()

READ
getVaultLastTradeAt(vaultId: number): number

Returns the time of the vault's last trade.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Unix seconds; 0 if the vault has not traded.
What to expect
Use it to show whether an agent is still active.
Example
const last = await readContract("saturnvaults", "getVaultLastTradeAt", [vaultId]);

getVaultDepositorCount()

READ
getVaultDepositorCount(vaultId: number): number

Returns the number of addresses holding shares in the vault.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Depositor count.
What to expect
A depositor from before the 4.2.0 upgrade is counted only once they deposit again.
Example
const d = await readContract("saturnvaults", "getVaultDepositorCount", [vaultId]);

getTotalArbTrades()

READ
getTotalArbTrades(): number

Returns the number of trades across all vaults.

Returns
number — Trade count, all vaults.
What to expect
Never reverts.
Example
const all = await readContract("saturnvaults", "getTotalArbTrades", []);

getUserUnlockAt()

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

Returns when user may withdraw from the vault: their latest deposit time + the hold in force now.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
useraddressThe depositor address to look up.
Returns
number — Unix seconds; 0 when the user holds no shares or the vault is closed. A time in the past means now.
What to expect
A waiting lower hold (getVaultNextHoldAt) brings it forward once it applies; read it again then.
Example
const at = await readContract("saturnvaults", "getUserUnlockAt", [vaultId, me]);
const canWithdraw = Number(at) <= Math.floor(Date.now() / 1000);

getUserValue()

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

Returns what user would receive for all their shares now: shares × totalDeposits / totalShares.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
useraddressThe depositor address to look up.
Returns
number — Raw base token; 0 without shares.
What to expect
Exactly what withdrawV2 pays for the full balance, so it works as minAmountOut for a full exit.
Example
const value = await readContract("saturnvaults", "getUserValue", [vaultId, me]);

getVaultTrade()

READ
getVaultTrade(vaultId: number, index: number): string

Returns one row of the vault's on-chain trade log (index is 0-based): time|pools|route|baseIn|toDepositors|agentFee|sharePriceAfter|agentSharePer10k.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
indexnumberTrade index, 0 … getVaultTradeCount − 1.
Returns
string — time = unix seconds; pools = "buy,sell" or "pool1,pool2,pool3"; route = the risk token or "tokenX,tokenY"; baseIn, toDepositors, agentFee = raw base token; sharePriceAfter = 8 decimals; agentSharePer10k = the agent's own share of the vault at the trade, per 10,000.
What to expect
"" for an index that does not exist.
Example
const row = await readContract("saturnvaults", "getVaultTrade", [vaultId, 0]);
// "1790553189|224,227|RA|3000000000|4403625768|489291752|144036257|2000"
const [time, pools, route, baseIn, toDepositors, agentFee, priceAfter, agentPer10k] = row.split("|");

getVaultTrades()

READ
getVaultTrades(vaultId: number, start: number, count: number): string*

Generator yielding up to count trade log rows from start (0-based), oldest first, in the getVaultTrade format.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
startnumberFirst index; a negative value reads from 0.
countnumberMost rows to return.
Returns
string* — Stream of "time|pools|route|baseIn|toDepositors|agentFee|sharePriceAfter|agentSharePer10k" rows.
What to expect
Past the end it yields nothing. Page with start += count up to getVaultTradeCount.
Example
const rows = await readContract("saturnvaults", "getVaultTrades", [vaultId, 0, 50]);

getVaultStats()

READ
getVaultStats(vaultId: number): string

Everything about one vault in one call, 22 pipe-separated fields: vaultId|agent|baseToken|totalDeposits|totalShares|sharePrice|perfFee|status|minDeposit|minHold|createdAt|trades|volume|profitToDepositors|agentFees|lastTradeAt|depositors|agentShares|profitToOthers|nextHold|nextHoldAt|name.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
string — sharePrice has 8 decimals; perfFee is per 10,000; status 0 = active, 1 = closed; minHold is the hold in force now and nextHold / nextHoldAt a lower one still waiting (nextHoldAt 0 or past: none); agentShares is the agent's own share balance; amounts are raw base token; times are unix seconds.
What to expect
The name is last so a "|" inside it cannot shift the other fields: split on "|" and join fields 21 onward back together (JavaScript's split with a limit drops the rest). Reverts for an id that was never created.
Example
const row = await readContract("saturnvaults", "getVaultStats", [vaultId]);
const f = row.split("|");
const stats = {
  vaultId: +f[0], agent: f[1], baseToken: f[2], totalDeposits: f[3], totalShares: f[4],
  sharePrice: Number(f[5]) / 1e8, feePct: Number(f[6]) / 100, status: +f[7],
  minDeposit: f[8], minHold: +f[9], createdAt: +f[10], trades: +f[11], volume: f[12],
  profitToDepositors: f[13], agentFees: f[14], lastTradeAt: +f[15], depositors: +f[16],
  agentShares: f[17], profitToOthers: f[18], nextHold: +f[19], nextHoldAt: +f[20],
  name: f.slice(21).join("|")
};

getAllVaultsStats()

READ
getAllVaultsStats(): string*

Generator yielding one getVaultStats row per vault ever created, in id order, closed vaults included.

Returns
string* — Stream of 22-field getVaultStats rows.
What to expect
The one call a vault list, leaderboard or bot scanner needs. Never reverts.
Example
const rows = await readContract("saturnvaults", "getAllVaultsStats", []);
const open = rows.map(r => r.split("|")).filter(f => f[7] === "0");

getVaultProfitToOthers()

READ
getVaultProfitToOthers(vaultId: number): number

Returns the profit added for depositors other than the agent: each trade's depositors' part times the other depositors' fraction of the shares. An agent that manufactures a profit with its own money (moving a pool it provides, say) gets its fee and its own share of the rest back, so a record built that way shows as next to nothing here.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Raw base token.
What to expect
Compare with getVaultProfitToDepositors to see how much of a track record came from outside money.
Example
const others = await readContract("saturnvaults", "getVaultProfitToOthers", [vaultId]);

getVaultNextHold()

READ
getVaultNextHold(vaultId: number): number

Returns the lower hold set by setVaultMinHold, in seconds.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Seconds.
What to expect
Only meaningful while getVaultNextHoldAt is in the future; after that getVaultMinHold already returns it.
Example
const nextHold = await readContract("saturnvaults", "getVaultNextHold", [vaultId]);

getVaultNextHoldAt()

READ
getVaultNextHoldAt(vaultId: number): number

Returns when the lower hold set by setVaultMinHold applies.

Parameters
NameTypeDescription
vaultIdnumberThe vault to inspect.
Returns
number — Unix seconds; 0 = none set.
What to expect
Once past, getVaultMinHold returns the new value and getUserUnlockAt uses it.
Example
const at = await readContract("saturnvaults", "getVaultNextHoldAt", [vaultId]);

Admin & Internal

forceCloseVault()

WRITE
forceCloseVault(vaultId: number)

Admin only. The saturnadmin owner (saturnadmin.getAdmin()) must sign. Closes any vault, as closeVault does for its agent: status becomes 1, so deposit and the agent's trades are refused. Depositors can still withdraw with withdrawV2, and the hold time no longer applies. Emits VaultClosed. It does not check that the vault exists or is still open.

Parameters
NameTypeDescription
vaultIdnumberVault to close.
What to expect
Reverts on: "Only admin", "Reentrancy detected" (the admin's guard is already held).
Agent Automation · Contract #22

SaturnStakeArb

saturnstakearb saturnstakearb-4.4.0

Permissionless arbitrage on borrowed staked capital. executeArb borrows amountIn of tokenSymbol from the idle stake in saturnholders (at most getTotalStaked(tokenSymbol)), swaps it tokenSymbol → riskToken on poolBuy and back on poolSell (two different active saturnpools v4 pools of the same pair), requires more tokenSymbol back than it borrowed, returns the full principal to saturnholders and splits the profit (back − amountIn, already net of both pools' swap fees): the bot gets floor(profit / 2), the stakers of tokenSymbol get the rest, credited by saturnholders.settleArbLoan, which also re-checks that saturnholders holds at least the total staked before the transaction can commit. The bot needs no capital and pays no flash or protocol fee, only gas (about 0.11–0.15 KCAL measured on devnet). If the round trip is not profitable, or anything else fails, the whole transaction reverts and only gas is spent; stakers cannot lose principal to it. Leg 1 sells tokenSymbol, so its stakers also get the holder slice of that leg's swap fee. Choosing a source: saturnflash borrows pool liquidity for a fee of 5 per 10,000 of amountIn (default) and the bot keeps the rest, so it pays the bot better when the profit exceeds about 0.1% of amountIn; below that, stake-arb leaves the bot more. With your own tokens and no split, use saturnarb.

Arbitrage Execution

executeArb()

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

Runs one stake arbitrage in a single atomic transaction: (1) saturnholders.flashLendStake sends amountIn of tokenSymbol from the stake vault to this contract; (2) leg 1 swaps it tokenSymbol → riskToken on poolBuy and (3) leg 2 swaps all of that riskToken → tokenSymbol on poolSell, both through saturnswap.swapFromContract with minAmountOut 1 (no per-leg slippage limit; the only floor is step 4); (4) requires back > amountIn, else "No arbitrage profit"; (5) with profit = back − amountIn, sends amountIn back to saturnholders, holderShare = profit − floor(profit / 2) to saturnliquidity and botShare = floor(profit / 2) to from; (6) saturnholders.settleArbLoan requires saturnholders to hold at least getTotalStaked(tokenSymbol) again and credits holderShare to every staker of tokenSymbol pro-rata (claimed with saturnholders.claim, like swap fees). Each leg pays its pool's normal swap fee before the profit check. There is no minProfit parameter: simulate both legs off-chain and send only when botShare covers your gas.

Parameters
NameTypeDescription
fromaddressBot address. Must sign (witness) and needs KCAL for gas and a little SOUL for the chain's data fee (a first call creates a storage key), but no tokenSymbol. Receives botShare = floor((back − amountIn) / 2) in tokenSymbol.
tokenSymbolstringSymbol of the token to borrow and denominate profit in; must be staked in saturnholders.
amountInnumberRaw amount of tokenSymbol to borrow. Must be > 0, ≤ saturnholders.getTotalStaked(tokenSymbol) and ≥ saturnrouter.getMinRawForSwap(tokenSymbol); the riskToken received on leg 1 must also clear getMinRawForSwap(riskToken).
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 — holderShare: the stakers' part of the profit, (back − amountIn) − floor((back − amountIn) / 2), in raw tokenSymbol. Your own share is floor((back − amountIn) / 2), i.e. holderShare or holderShare − 1, and is in the StakeArbExecuted event (tokenSymbol, amountIn, backAmount, botShare, holderShare).
What to expect
On success emits StakeArbExecuted(from, {tokenSymbol, amountIn, backAmount, botShare, holderShare}) and increments getTotalArbs, getTotalHolderProfit(tokenSymbol) and getExecutorArbCount(from). Reverts on: "Reentrancy detected" (from is already inside another Saturn call), "Not authorized" (from did not sign), "Amount must be > 0", "Same pool", "Risk token must differ", "poolBuy inactive", "poolSell inactive", "first token not in pool" / "second token not in pool" (a pool is not the tokenSymbol/riskToken pair), "Exceeds staked capital" (amountIn > getTotalStaked), any saturnswap revert on either leg ("Below minimum swap", "Zero reserve in", "Zero reserve out", "Output rounds to zero", "Admin fee rounds to zero", "Reinvest rounds to zero", "Cannot drain pool"), "No arbitrage profit" (back ≤ amountIn), or saturnholders' safety checks ("Loan already open", "Principal not restored"), which a correct call never reaches. On revert nothing moves: the stake is untouched and only gas is spent.
Example
// Off-chain: list the SOUL/KCAL pools (saturnrouter.getPoolCountForPair,
// getPoolIdForPairAtIndex, getPoolFullInfo), simulate both legs with each
// pool's fee, cap amountIn at saturnholders.getTotalStaked("SOUL") and send
// only if floor(profit / 2) covers your gas. The bot needs KCAL and a little SOUL, none of the token.
const script = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnstakearb", "executeArb", [
    from,
    "SOUL",        // tokenSymbol: borrowed from SOUL stakers, profit paid in SOUL
    10000000000,   // amountIn: 100 SOUL (8 decimals), <= getTotalStaked("SOUL")
    "KCAL",        // riskToken: the other token of both pools
    cheapPoolId,   // poolBuy: leg 1 SOUL -> KCAL here (KCAL is cheap in SOUL)
    richPoolId     // poolSell: leg 2 KCAL -> SOUL here (same pair)
  ])
  .spendGas(from)
  .endScript();
// returns holderShare; you receive floor((back - amountIn) / 2) SOUL, or the tx reverts

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

Seconds a payment may come after its installment's due time and still count as on time, and seconds after the loan's final due date (saturnloans.getLoanDueDate) before the lender can call saturnloans.triggerDefault. A late installment only costs credit score; it never defaults the loan. Default is 259,200 (3 days).

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 collateral value (8-decimal scaled units; default 100,000,000 = 1 RA) that the protocol intends to require before a loan can be created. Note for integrators: in the current release no contract enforces this value — saturnmarket and saturnloans accept any positive collateral valuation — so treat it as a UI hint for your loan form, not as an on-chain guarantee.

Returns
number — Minimum collateral value in RA-anchor scaled units (default: 100000000).
What to expect
Positive; changed only by the admin. Not enforced on-chain in the current release.
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. Only the loan's lender can act on it: saturnloans.flagLiquidation needs the spot LTV above it, and triggerLiquidation, 6-24 h later on mainnet, needs the time-weighted LTV above it. 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 since the borrower was registered in saturncredit (postLoanRequest registers a new borrower). Default is 1 point/day, 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 for each payment made after its installment's due time plus the grace period. A payment inside the grace period counts as on time. 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 for repaying a loan in full before its due date (counted once, at the final payment). Default is 15; saturncredit caps the total at 50.

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

Stored liquidation penalty in basis points (per 10,000). Default is 500 (5%). No contract applies it: a liquidation or default hands the lender the whole pledged pool, whatever the debt.

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 the interest part of each repayment (bps per 10,000) sent to the saturnlendcfg admin instead of the lender. Default is 1,000 (10%). The origination fee (getOriginationFeeBps) is separate.

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. Only saturnauto uses it, and auto-lending is disabled in v1.0. saturnmarket and saturnloans never check it: a P2P loan has no LTV cap at origination, so a lender must size its own quote against the pool's value.

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]);
// 1991 → 19.91% APR for a score of 600 (2000 − 600 × 15 / 1000)

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. Informational only: no contract charges it, and there is no liquidator income, because only the loan's lender can liquidate.

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

accrueStorageFee()

WRITE
accrueStorageFee(feesFromSoul: number)

Does nothing. Anyone may call it; it takes no SOUL and changes no state. Gen3 storage is funded by the transaction's SOUL data escrow (maxData on the payer), not by SOUL collected through this contract. Kept for ABI compatibility.

Parameters
NameTypeDescription
feesFromSoulnumberIgnored.
What to expect
Never reverts; changes nothing.
Example
// Nothing to call: this method is a no-op.
// Storage is paid from the transaction's data escrow (maxData on the payer).

increaseStorage()

WRITE
increaseStorage(from: address, stakeAmount: number, soultoken: string)

Removed in v4-03x: it always reverts, for every caller, and moves no SOUL. Contract self-staking is gone; Gen3 storage is paid from the transaction's SOUL data escrow (maxData on the payer). Kept for ABI compatibility.

Parameters
NameTypeDescription
fromaddressIgnored.
stakeAmountnumberIgnored.
soultokenstringIgnored.
What to expect
Always reverts with "Storage staking removed: Gen3 storage is funded by tx data escrow, not by staking or SOUL charged here".
Example
// Do not call: it always reverts.
// Pay for storage through the transaction's data escrow (maxData) instead.

Admin & Internal

updateAdmin()

WRITE
updateAdmin(newAdmin: address)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Hands the lending admin role to newAdmin. saturnloans, saturnmarket, saturnvault, saturndexadapt, saturncredit and saturnauto all read the admin from here (saturntaz has its own _owner): it receives the interest cut of each repayment and the origination fees, and it signs contract upgrades, setLiquidationWindow, toggleMarket, setReferencePool and setAnchorToken. Today it is the same wallet as the saturnadmin owner, on mainnet and devnet.

Parameters
NameTypeDescription
newAdminaddressNew admin address.
What to expect
Reverts with "Only admin" or "Invalid admin" (null address).

updateCreditParams()

WRITE
updateCreditParams(newBase: number, newMax: number, newTimeBonusPerDay: number, newTimeCap: number, newOnTimeBonus: number, newOnTimeCap: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the credit score scale saturncredit uses: the starting score (default 200), the maximum (1,000), the points per day since registration (1) and their cap (150), and the points per on-time repayment (50) and their cap (350). Only base and max are checked; the other four are stored as given. The LTV tier boundaries (200, 400, 600, 800 in getMaxLtvForScore) are fixed and do not follow newMax.

Parameters
NameTypeDescription
newBasenumberStarting score, 0 to newMax.
newMaxnumberMaximum score, 100 to 10,000.
newTimeBonusPerDaynumberPoints per day since registration.
newTimeCapnumberCap on the time bonus.
newOnTimeBonusnumberPoints per on-time repayment.
newOnTimeCapnumberCap on the on-time bonus.
What to expect
Reverts with "Only admin", "Base must be >= 0", "Max must be >= 100", "Max too high" or "Base cannot exceed max".

updatePenaltyParams()

WRITE
updatePenaltyParams(newLatePenalty: number, newDefaultPenalty: number, newStreakBonus: number, newStreakCap: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the credit points a late repayment costs (default 30) and a default costs (150), and the bonus per repayment in the borrower's best on-time streak (10) and its cap (100). No value is range-checked. partialRepayBonus (15) has no setter.

Parameters
NameTypeDescription
newLatePenaltynumberPoints lost per late repayment.
newDefaultPenaltynumberPoints lost per default or liquidation.
newStreakBonusnumberPoints per repayment in the borrower's best on-time streak.
newStreakCapnumberCap on the streak bonus.
What to expect
Reverts with "Only admin".

updateLtvTiers()

WRITE
updateLtvTiers(t1: number, t2: number, t3: number, t4: number, t5: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the maximum LTV per credit tier, per 10,000 (defaults 2,500 / 3,500 / 5,000 / 6,500 / 8,000 for scores 0-199, 200-399, 400-599, 600-799 and 800+). The tiers must rise strictly, t1 at least 1,000 (10%) and t5 at most 9,000 (90%). Keep t5 below getLiquidationThreshold(); this method does not compare them.

Parameters
NameTypeDescription
t1numberScore 0-199, per 10,000, at least 1,000.
t2numberScore 200-399, above t1.
t3numberScore 400-599, above t2.
t4numberScore 600-799, above t3.
t5numberScore 800 and up, above t4 and at most 9,000.
What to expect
Reverts with "Only admin", "Tier 1 too low (min 10%)", "Tier 5 too high (max 90%)" or "Tiers must be ascending".

updateInterestParams()

WRITE
updateInterestParams(newBase: number, newDiscount: number, newMin: number, newMax: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the rate curve of getInterestRateForScore, all annual per 10,000: the base rate (default 2,000 = 20%), the discount (15; score × newDiscount / 1,000 comes off the base), the floor (300) and the ceiling (3,000). saturnmarket quotes carry their own rate and are not bounded by these.

Parameters
NameTypeDescription
newBasenumberBase annual rate per 10,000, at least 100.
newDiscountnumberDiscount per 1,000 score points, per 10,000.
newMinnumberFloor, at least 100 and below newMax.
newMaxnumberCeiling, at most 5,000.
What to expect
Reverts with "Only admin", "Base rate too low" (under 100), "Max rate too high (50%)" (over 5,000), "Min rate too low (1%)" (under 100) or "Min must be < max".

updateDurationParams()

WRITE
updateDurationParams(newMin: number, newMax: number, newAutoBase: number, newAutoExt: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the loan term limits in seconds: the minimum (604,800 = 7 days on mainnet and devnet) and maximum (31,536,000 = 365 days), which saturnmarket.submitQuote enforces on every quote, and the auto-lending base term (default 2,592,000) and maximum extension (12,960,000) used by getAutoDurationForScore. The auto values are not checked.

Parameters
NameTypeDescription
newMinnumberShortest term in seconds, at least 86,400 (1 day).
newMaxnumberLongest term in seconds, at most 63,072,000 (2 years), above newMin.
newAutoBasenumberAuto-lending base term in seconds.
newAutoExtnumberAuto-lending maximum extension in seconds.
What to expect
Reverts with "Only admin", "Min duration >= 1 day", "Max duration <= 2 years" or "Min must be < max".

updateGracePeriod()

WRITE
updateGracePeriod(newGrace: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the grace period in seconds, 0 to 604,800 (7 days); 259,200 (3 days) on mainnet and devnet. saturnloans reads it at each call: it decides whether a repayment counts as late for the credit score, and how long after the due date the lender must wait for triggerDefault. A change applies to open loans at once.

Parameters
NameTypeDescription
newGracenumberGrace period in seconds, 0 to 604,800.
What to expect
Reverts with "Only admin", "Grace cannot be negative" or "Grace max 7 days".

updateProtocolFees()

WRITE
updateProtocolFees(newOrigination: number, newLiquidation: number, newProtocolShare: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets three rates per 10,000: the origination fee taken from each disbursement (100 = 1% today, at most 500), the liquidation penalty (500 today, at most 2,000; only the getLiquidationPenaltyBps and getLiquidationPenalty views read it; no lending contract charges it), and the admin's share of the interest in each repayment (1,000 = 10% today, at most 3,000). The origination fee is fixed when a loan opens; the interest share is read at every makePayment, so it applies to open loans too.

Parameters
NameTypeDescription
newOriginationnumberOrigination fee per 10,000, at most 500.
newLiquidationnumberLiquidation penalty per 10,000, at most 2,000.
newProtocolSharenumberAdmin's share of the interest per 10,000, at most 3,000.
What to expect
Reverts with "Only admin", "Origination max 5%", "Liquidation max 20%" or "Protocol share max 30%".

updateCollateralParams()

WRITE
updateCollateralParams(newMinValue: number, newLiqThreshold: number, newMaxLoans: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Sets the minimum collateral value (100,000,000 today; not range-checked, and no lending contract reads it), the liquidation threshold LTV per 10,000 (9,000 = 90% today), and the most loans a borrower may have open at once (5 today; saturnmarket.acceptQuote checks it). The threshold applies to open loans at once: flagLiquidation and triggerLiquidation read it when they run.

Parameters
NameTypeDescription
newMinValuenumberMinimum collateral value (8-decimal scaled).
newLiqThresholdnumberLiquidation threshold per 10,000: above 5,000, at most 9,800.
newMaxLoansnumberOpen loans per borrower, 1 to 20.
What to expect
Reverts with "Only admin", "Liq threshold must be > 50%", "Liq threshold must be <= 98%", "Must allow at least 1 loan" or "Max 20 concurrent loans".

updateSOULfee()

WRITE
updateSOULfee(newFee: number)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Even then it always reverts: the SOUL loan fee is gone, and getSOULfeeLoan() always returns 0. Kept for ABI compatibility.

Parameters
NameTypeDescription
newFeenumberIgnored.
What to expect
Reverts with "Only admin" for any other signer, and otherwise always with "SOUL loan fee removed: lending entrypoints no longer charge SOUL".

acquireGuard()

WRITE
acquireGuard(user: address)

Internal: only saturncredit, saturnvault, saturnloans, saturnauto or saturnmarket can call this. It takes the lending reentrancy lock for user and records the calling contract as its owner (getGuardLocked = 1, getGuardOwner = that contract). Lending writes such as makePayment, withdrawLenderBalance, postLoanRequest, submitQuote and acceptQuote take it on entry and release it on exit, so a nested lending write for the same user reverts.

Parameters
NameTypeDescription
useraddressThe wallet the lending write runs for.
What to expect
Reverts with "Only lending contracts" or "Reentrancy detected" (the lock is already held for this user).

releaseGuard()

WRITE
releaseGuard(user: address)

Internal: only saturncredit, saturnvault, saturnloans, saturnauto or saturnmarket can call this, and only the contract that took the lock. It clears the lock for user (getGuardLocked = 0, getGuardOwner = "").

Parameters
NameTypeDescription
useraddressThe wallet whose lock is released.
What to expect
Reverts with "Only lending contracts" or "Only the acquiring contract can release" (also when no lock is held).

clearReentrancy()

WRITE
clearReentrancy(user: address)

Admin only. The saturnlendcfg admin (getAdmin(), the contract's _owner) must sign. Clears the lending lock for user (getGuardLocked back to 0, getGuardOwner to ""). A reverted transaction rolls the lock back by itself, so use this only if getGuardLocked(user) reads 1 outside any transaction.

Parameters
NameTypeDescription
useraddressWallet whose lock is cleared.
What to expect
Reverts with "Only admin".
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. Loan events update the counters, not the score: the stored (cached) score changes only when computeScore runs in a transaction, so read computeScore through invokeRawScript for the live value. With the live saturnlendcfg config the highest reachable score is 850 (200 base + 150 time + 350 on-time + 100 streak + 50 early payoff). 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. saturnmarket.postLoanRequest also auto-registers the borrower (ensureRegistered), but explicit registration lets users build tenure earlier.

Parameters
NameTypeDescription
fromaddressThe borrower's wallet address. Must be the transaction signer.
What to expect
Reverts with "Reentrancy detected" (saturnlendcfg guard held for `from`), "Not authorized" (`from` did not sign) or "Already registered". Initializes all counters to 0 and sets the cached score to saturnlendcfg.getBaseScore() (200 live).
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturncredit", "registerUser", [from])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

Score Computation

computeScore()

WRITE
computeScore(user: address): number

Recomputes the user's score, stores it as the cached score with the current timestamp, and returns it. Anyone can call it for any registered user. Formula (saturnlendcfg values, live value in brackets): baseScore [200] + min(floor(secondsSinceRegistration / secondsPerDay [86,400]) × timeBonusPerDay [1], timeBonusCap [150]) + min(onTimeRepays × onTimeRepayBonus [50], onTimeRepayCap [350]) + min(bestStreak × streakBonus [10], streakBonusCap [100]) + min(partialRepays × partialRepayBonus [15], 50) − lateRepays × lateRepayPenalty [30] − defaults × defaultPenalty [150]. A result below 0 becomes 0, and the result is capped at maxScore [1000]; with the live config the highest reachable score is 850. Called through invokeRawScript it returns the live score for free without storing it.

Parameters
NameTypeDescription
useraddressThe borrower whose score to recompute.
Returns
number — Credit score, 0 to maxScore (1000); 850 is the highest reachable with the live config.
What to expect
Reverts with "User not registered". Each bonus is capped on its own before summing; penalties are uncapped.
Example
// Free: the live score, nothing stored
const live = await readContract("saturncredit", "computeScore", [userAddr]);

// Store it on-chain (refreshes getUserCachedScore and getCreditReport)
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturncredit", "computeScore", [userAddr])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

Credit Score & Status Reads

getUserCachedScore()

READ
getUserCachedScore(user: address): number

Returns the stored credit score for this user. It is set to the base score at registration and rewritten only when computeScore runs in a transaction; loan events (origination, repayment, default) do not refresh it. It can therefore lag the live score (mainnet: a borrower with an open loan still shows 200 while computeScore returns 207). For the live value, read computeScore through invokeRawScript.

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 (Number(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 payments made on time: saturnloans counts a payment as on time when it arrives no later than the installment due date plus saturnlendcfg.getGracePeriod() (259,200 s = 3 days live). Each contributes onTimeRepayBonus points (50 live) to the credit score, capped at onTimeRepayCap (350 live).

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 (saturnloans records one on a lender-triggered default and on a liquidation). Each costs defaultPenalty points (150 live), the heaviest penalty in the formula, and resets the current streak (getUserConsecutiveOnTime) to 0; the best streak is kept.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cumulative default count.

getUserPartialRepays()

READ
getUserPartialRepays(user: address): number

Count of loans fully repaid before their final due date (saturnloans calls markPartialRepay when a loan closes early; installment payments do not count). Each adds partialRepayBonus points (15 live), capped at 50 in total.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Cumulative early/partial repayment count.

getUserTotalRepaid()

READ
getUserTotalRepaid(user: address): number

Total amount repaid by this user across all loans (principal plus interest, as credited by saturnloans), in 8-decimal scaled units. Useful for displaying lifetime repayment volume on a borrower dashboard.

Parameters
NameTypeDescription
useraddressThe borrower's address.
Returns
number — Sum of all repayments in 8-decimal scaled units.

getUserLastActivity()

READ
getUserLastActivity(user: address): number

Unix timestamp of the most recent registration, loan creation, repayment or 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 loans. Increments on loan creation; decrements when a loan is fully repaid, defaulted or liquidated. saturnmarket.acceptQuote reverts with "Maximum concurrent loans reached" once this reaches saturnlendcfg.getMaxLoansPerUser() (5 live).

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}`);

Admin & Internal

markLoanCreated()

WRITE
markLoanCreated(user: address): void

Internal: only saturnloans, saturnauto or saturnmarket can call this. saturnloans calls it when it creates a loan. Adds 1 to the user's total and active loans, sets last activity to now and adds 1 to getTotalLoansIssued. Does not refresh the cached score.

Parameters
NameTypeDescription
useraddressBorrower.
What to expect
Reverts with "Only lending contracts" for any other caller, or "User not registered".

markTimelyRepay()

WRITE
markTimelyRepay(user: address, amountRepaid: number): void

Internal: only saturnloans, saturnauto or saturnmarket can call this. saturnloans calls it for a payment made no later than the installment due date plus the grace period. Adds 1 to on-time repays, adds `amountRepaid` to total repaid, extends the current streak (and the best streak if it is longer) and sets last activity. Does not refresh the cached score.

Parameters
NameTypeDescription
useraddressBorrower.
amountRepaidnumberPayment credited, 8-decimal scaled units.
What to expect
Reverts with "Only lending contracts" for any other caller. Does not check registration.

markLateRepay()

WRITE
markLateRepay(user: address, amountRepaid: number): void

Internal: only saturnloans, saturnauto or saturnmarket can call this. saturnloans calls it for a payment made after the installment due date plus the grace period. Adds 1 to late repays, adds `amountRepaid` to total repaid, resets the current streak to 0 and sets last activity.

Parameters
NameTypeDescription
useraddressBorrower.
amountRepaidnumberPayment credited, 8-decimal scaled units.
What to expect
Reverts with "Only lending contracts" for any other caller.

markDefault()

WRITE
markDefault(user: address): void

Internal: only saturnloans, saturnauto or saturnmarket can call this. saturnloans calls it (followed by markLoanClosed) on a lender-triggered default and on a liquidation. Adds 1 to the user's defaults and to getTotalDefaults, resets the current streak to 0 and sets last activity.

Parameters
NameTypeDescription
useraddressBorrower.
What to expect
Reverts with "Only lending contracts" for any other caller.

markLoanClosed()

WRITE
markLoanClosed(user: address): void

Internal: only saturnloans, saturnauto or saturnmarket can call this. saturnloans calls it when a loan is fully repaid, defaulted or liquidated. Subtracts 1 from active loans (never below 0).

Parameters
NameTypeDescription
useraddressBorrower.
What to expect
Reverts with "Only lending contracts" for any other caller.

markPartialRepay()

WRITE
markPartialRepay(user: address): void

Internal: only saturnloans, saturnauto or saturnmarket can call this. saturnloans calls it when a loan is fully repaid before its final due date. Adds 1 to getUserPartialRepays.

Parameters
NameTypeDescription
useraddressBorrower.
What to expect
Reverts with "Only lending contracts" for any other caller.

ensureRegistered()

WRITE
ensureRegistered(user: address): void

Internal: only saturnauto or saturnmarket can call this. saturnmarket.postLoanRequest calls it. If `user` is not registered, registers it exactly like registerUser (counters 0, cached score = base score, registration time = now) and adds 1 to getTotalRegisteredUsers; otherwise does nothing.

Parameters
NameTypeDescription
useraddressBorrower to register.
What to expect
Reverts with "Only lending contracts" for any other caller (saturnloans included).
Lending Protocol · Contract #3

SaturnVault

saturnvault saturnvault-1.1.0

Custodian for all loan collateral on the Saturn lending protocol. Since 1.1.0 it accepts one collateral type: a v4 RA/TAZ pool, pledged in place in saturnpools (getPoolPawned = 1, financial lock exactly 1) while the vault holds its SATURN certificate; swaps and the borrower's provider fees continue during the loan. Full repayment releases the pledge and returns the certificate; a liquidation or default makes the lender the pool's provider and sends the lender the certificate. Single-token and v3 LP NFT collateral are disabled. 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. A v4 pool (every live position): "type:v4pool_dex:2_status:1_loan:1_poolId:35_pair:RA_TAZ_certId:<SATURN certificate id>" (mainnet collateral 1). The pair itself contains an underscore.
Example
const summary = await readContract("saturnvault", "getCollateralSummary", [colId]);
// do not split on "_": the pair (RA_TAZ) holds one
const m = summary.match(/_poolId:([0-9]+)_pair:([A-Z]+_[A-Z]+)_certId:([0-9]+)$/);
const [poolId, pair, certId] = m.slice(1);

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", []);

getCollateralTokenSymbol()

READ
getCollateralTokenSymbol(colId: number): string

Token symbol of a type-1 (single-token) collateral position. Single-token collateral is disabled in v1.0, so every live position is type 2 or 3 and this returns an empty string; it is exposed for forward compatibility and for indexers that decode all fields uniformly.

Parameters
NameTypeDescription
colIdnumberCollateral position id.
Returns
string — Token symbol, or "" for LP-backed positions.
What to expect
Never reverts.
Example
const sym = await readContract("saturnvault", "getCollateralTokenSymbol", [colId]);

getCollateralTokenAmount()

READ
getCollateralTokenAmount(colId: number): number

Raw amount held for a type-1 (single-token) collateral position. Always 0 in v1.0 because token collateral is disabled — LP positions report their size through the V4 pool / V3 NFT field getters instead.

Parameters
NameTypeDescription
colIdnumberCollateral position id.
Returns
number — Raw token amount, 0 for LP-backed positions.
What to expect
Never reverts.
Example
const amount = await readContract("saturnvault", "getCollateralTokenAmount", [colId]);

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag. Mainnet and devnet report "saturnvault-1.1.0". 1.1.0 is the first vault with this method. saturndexadapt.v4LockPool calls it before it pledges a pool, so an older vault, which would leave the certificate with the borrower, cannot take a pledge.

Returns
string — Build tag, e.g. "saturnvault-1.1.0".
What to expect
Never reverts.
Example
const v = await readContract("saturnvault", "getContractVersion", []);
// "saturnvault-1.1.0"

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 of the pledged pool, in the pool's own order: always "RA" or "TAZ", since only RA/TAZ pools can back a loan.

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 of the pledged pool, in the pool's own order: always "RA" or "TAZ", since only RA/TAZ pools can back a loan.

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).

getCollateralCertificateId()

READ
getCollateralCertificateId(colId: number): number

The SATURN certificate NFT ID the vault took into custody for a type-2 (v4 pool) record, 0 when it took none: another collateral type, or a record made before 1.1.0. A legacy record with 0 cannot be released or liquidated ("legacy collateral: the vault holds no certificate for this record"). The ID is not cleared when the record is released or liquidated, so check getCollateralStatus: only status 1 (locked) means the vault still holds the certificate. getCollateralSummary shows the same value as certId.

Parameters
NameTypeDescription
colIdnumberCollateral position ID.
Returns
number — SATURN NFT ID (a large integer), or 0.
What to expect
Never reverts.
Example
const certId = await readContract("saturnvault", "getCollateralCertificateId", [colId]);
const status = await readContract("saturnvault", "getCollateralStatus", [colId]);
// certId > 0 and status 1: the vault holds this SATURN certificate now

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. For a v4 pool (every live position) pass TAZ with any baseDex, or another RA-paired token with baseDex 2; any other token with baseDex 1 reverts with 'v4 pool collateral is valued in TAZ or through DEX v4 only (baseDex 2)'. saturnloans.getCurrentLtv values it in TAZ. 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, and with 'v4 pool collateral is valued in TAZ or through DEX v4 only (baseDex 2)' when baseToken is not TAZ and baseDex is not 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". Offer a v4 RA/TAZ pool through saturnmarket instead (depositV4PoolCollateral); v3 LP NFT collateral is disabled too (depositV3LpNftCollateral always reverts). 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.

Admin & Internal

depositV4PoolCollateral()

WRITE
depositV4PoolCollateral(from: address, poolId: number): number

Internal: only saturnmarket can call this (acceptQuote, signed by the borrower). It checks that the pool pairs RA with TAZ (saturndexadapt.anchoredPair), has both reserves and has a SATURN certificate. Then it pledges the pool through saturndexadapt.v4LockPool, which re-checks that from is the provider and holds the certificate, and that the pool has no fee redirect, financial or campaign lock, burn or time lock. It writes a type-2 record (status 1, dexVersion 2, the reserves at lock, the certificate ID). Last, it moves the certificate from the borrower into the vault with NFT.transfer. Swaps, fee accrual, the borrower's provider-fee claims and addLiquidity continue during the loan.

Parameters
NameTypeDescription
fromaddressBorrower: the pool's provider and certificate holder, who signed the transaction.
poolIdnumberv4 RA/TAZ pool ID.
Returns
number — The new collateral ID.
What to expect
Reverts with "Only saturnmarket", "V4 pool must be RA-paired", "Pool has no reserves" or "Pool has no certificate", or with a pledge refusal from saturndexadapt.v4LockPool: "Pool not active", "Only pool provider", "Pool has active fee redirect", "Pool already under another financial product", "Pool is enrolled in a reward campaign", "Pool liquidity is burned or time-locked - it cannot back a loan" or "You do not hold this pool's SATURN certificate".

depositV3LpNftCollateral()

WRITE
depositV3LpNftCollateral(from: address, nftId: number): number

Internal: only saturnauto, saturnmarket or saturnloans can call this. Disabled since 1.1.0: it always reverts, for every caller, with "v3 LP NFT collateral disabled". SATRN's onSend trigger refuses a send from a contract, so an NFT deposited here could never be returned or liquidated. saturnmarket refuses type-3 requests and quotes before this point.

Parameters
NameTypeDescription
fromaddressBorrower holding the SATRN LP NFT.
nftIdnumberSATRN LP NFT ID.
Returns
number — Never returns: the call always reverts.
What to expect
Always reverts with "v3 LP NFT collateral disabled".

linkToLoan()

WRITE
linkToLoan(colId: number, loanId: number)

Internal: only saturnloans can call this (createLoan). Sets the loan ID on a locked record that has none yet. A record is linked to one loan, once.

Parameters
NameTypeDescription
colIdnumberCollateral ID, status 1 (locked).
loanIdnumberThe new loan's ID.
What to expect
Reverts with "Only saturnloans", "Collateral not locked" or "Already linked to a loan".

releaseCollateral()

WRITE
releaseCollateral(colId: number)

Internal: only saturnloans can call this (makePayment, once the loan is fully repaid). It sets the status to 2 (released) first, then returns the collateral to the record's owner. For a v4 pool it releases the pledge (saturndexadapt.v4UnlockPool) and sends the SATURN certificate back with NFT.transfer. For token collateral (type 1, disabled) it sends the raw tokens back; for a v3 LP NFT (type 3, disabled) it sends the NFT back.

Parameters
NameTypeDescription
colIdnumberCollateral ID, status 1 (locked).
What to expect
Reverts with "Only saturnloans" or "Collateral not locked". A v4 pool record made before 1.1.0 reverts with "legacy collateral: the vault holds no certificate for this record".

liquidateTokenCollateral()

WRITE
liquidateTokenCollateral(colId: number, recipient: address)

Internal: only saturnloans can call this (a default or liquidation of a type-1 record). It sets the status to 3 (liquidated), then sends the deposited raw token amount to recipient, the lender. Token collateral is disabled in v1.0, so no such record is expected.

Parameters
NameTypeDescription
colIdnumberCollateral ID: type 1, status 1.
recipientaddressLender.
What to expect
Reverts with "Only saturnloans", "Collateral not locked" or "Not token collateral".

liquidateV4PoolCollateral()

WRITE
liquidateV4PoolCollateral(colId: number)

Internal: saturnloans called this up to 1.0.2. Retired in 1.1.0: it always reverts, for every caller, with "needs saturnloans 1.0.3 (liquidateV4PoolCollateralTo)". It only marked the record and left the pool frozen with the borrower. saturnloans 1.0.3 calls liquidateV4PoolCollateralTo(colId, lender) instead.

Parameters
NameTypeDescription
colIdnumberCollateral ID.
What to expect
Always reverts with "needs saturnloans 1.0.3 (liquidateV4PoolCollateralTo)".

liquidateV3LpNftCollateral()

WRITE
liquidateV3LpNftCollateral(colId: number, recipient: address)

Internal: only saturnloans can call this (a default or liquidation of a type-3 record). It sets the status to 3 (liquidated), then sends the SATRN LP NFT to recipient, the lender, who can burn it through SATRN.removeLiquidity. v3 LP NFT collateral is disabled since 1.1.0, so no such record is expected.

Parameters
NameTypeDescription
colIdnumberCollateral ID: type 3, status 1.
recipientaddressLender.
What to expect
Reverts with "Only saturnloans", "Collateral not locked" or "Not v3 LP NFT collateral".

adminFinalizeV4PoolLiquidation()

WRITE
adminFinalizeV4PoolLiquidation(colId: number)

Admin only, and retired: since 1.1.0 it always reverts, for every caller (the admin included), with "removed in saturnvault 1.1.0: a v4 liquidation hands the pool to the lender". It used to unlock a liquidated pool back to the borrower and could be repeated. Now the lender receives the pool inside the liquidation transaction (liquidateV4PoolCollateralTo), and the admin has no path over pools.

Parameters
NameTypeDescription
colIdnumberCollateral ID.
What to expect
Always reverts with "removed in saturnvault 1.1.0: a v4 liquidation hands the pool to the lender".

liquidateV4PoolCollateralTo()

WRITE
liquidateV4PoolCollateralTo(colId: number, recipient: address)

Internal: only saturnloans can call this (triggerDefault or triggerLiquidation on a v4 pool record, in the lender's transaction). It sets the status to 3 (liquidated) first. Then saturndexadapt.v4HandOverPool makes recipient the pool's provider and clears the pledge and the lock (saturnpools.handOverPledgedPool), and the SATURN certificate goes to recipient with NFT.transfer. The lender gets the whole pool, including liquidity the borrower added during the loan. It runs once per record.

Parameters
NameTypeDescription
colIdnumberCollateral ID: type 2, status 1, linked to a loan.
recipientaddressLender, the pool's new provider.
What to expect
Reverts with "Only saturnloans", "Collateral not locked", "Not v4 pool collateral", "Collateral not linked to a loan", "legacy collateral: the vault holds no certificate for this record" or "Invalid recipient".
Lending Protocol · Contract #4

SaturnLoans

saturnloans saturnloans-1.0.3

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. Only the loan's lender can default a loan past its due date plus the grace period (triggerDefault) or liquidate it in two steps: flagLiquidation() on the spot LTV, then triggerLiquidation() getLiquidationWindowMin() to getLiquidationWindowMax() seconds later (6 h to 24 h on mainnet) on the reference RA/TAZ pool's time-weighted price since the flag, so a price pushed inside one transaction cannot liquidate a position. On a liquidation or default the lender becomes the pledged pool's provider and receives its SATURN certificate. The lender's share of every payment is escrowed here (withdrawLenderBalance), and the TAZ reward of a repaid loan is claimed separately (claimRepaymentReward). 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) — 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 (raw units of the loan token, TAZ) is converted to the ledger's 8-decimal scaled units and split by its interest share: that share × saturnlendcfg.getProtocolFeeShare() (1,000 = 10% today) goes to the protocol admin, the rest is credited to the lender's escrow balance in this contract (the lender takes it with withdrawLenderBalance). Installment tracking and the next-due timestamp are advanced automatically. If the payment clears the balance (totalRepaid >= totalOwed) the loan moves to 2 (repaid), the pool pledge is released and its SATURN certificate returned to the borrower, and the credit score is updated; the TAZ reward is then claimable once through claimRepaymentReward(loanId) within one reward day. The borrower can pay while the loan is active, flagged or past due, until the lender defaults or liquidates it. An overpayment is cut to the remaining balance, but the wallet must hold the whole paymentAmount passed: the balance check runs before the cut. Interest is fixed for the whole term, so paying early does not lower it.

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 with "Loan not active", "Only borrower can repay", "Insufficient balance for payment", "Payment must be > 0", "Payment rounds to zero - increase amount" (under one scaled unit: 10 raw TAZ), "Not authorized" or "Reentrancy detected".
Example
// Repay everything left on loan #1 (the ledger is 8-decimal scaled, the payment is raw TAZ)
const remaining = await readContract("saturnloans", "getLoanRemaining", [1]);   // scaled
const raw = await readContract("saturnpools", "scaleDown", [remaining, "TAZ"]); // raw TAZ (9 decimals)
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnloans", "makePayment", [from, 1, raw]) // an overpayment is clamped
  .spendGas(from)
  .endScript();
// sign with the borrower's wallet and send

Default & Liquidation Enforcement

triggerDefault()

WRITE
triggerDefault(loanId: number)

The lender defaults a loan once its due date plus the grace period (saturnlendcfg.getGracePeriod, 259,200 s = 3 days today) has passed. Only the loan's lender can call it: the lender must be a witness (since 1.0.2 nobody else can). Moves status 1 (active) → 3 → 4 (liquidated) in the same call, hands the collateral to the lender (for a v4 pool the lender becomes its provider and receives its SATURN certificate), records the trigger time, and marks a default on the borrower's credit profile. Until the lender calls it the loan stays active and the borrower can still repay, late.

Parameters
NameTypeDescription
loanIdnumberID of the active loan to default.
What to expect
Reverts with "Loan not active", "Loan not yet in default" (not past dueDate + gracePeriod) or "Only the lender can flag, liquidate or default this loan".
Example
// The lender, after dueDate + grace period
const tx = ScriptBuilder
  .begin()
  .allowGas(lender, null, gasPrice, gasLimit)
  .callContract("saturnloans", "triggerDefault", [loanId])
  .spendGas(lender)
  .endScript();
// sign with the lender's wallet (the lender must be the witness)

flagLiquidation()

WRITE
flagLiquidation(loanId: number)

First step of an LTV liquidation, lender only (the lender must be a witness). Allowed once the spot getCurrentLtv(loanId) exceeds saturnlendcfg.getLiquidationThreshold() (9,000 = 90% today). It records the flag time, the reference RA/TAZ pool and that pool's accumulated time-weighted price; nothing moves. triggerLiquidation() then needs the flag to be getLiquidationWindowMin() to getLiquidationWindowMax() seconds old (21,600 to 86,400 s on mainnet) and decides on the average price since the flag. Every new flag replaces the last one (time and snapshot) and restarts the wait.

Parameters
NameTypeDescription
loanIdnumberActive loan whose LTV is above the liquidation threshold.
What to expect
Reverts on: "Loan not active", "LTV not above liquidation threshold", "Only the lender can flag, liquidate or default this loan", or a saturndexadapt refusal when no TWAP-tracked reference pool is pinned. No event: read getLoanLiquidationFlaggedAt(loanId) and schedule the trigger getLiquidationWindowMin() seconds later.
Example
// Step 1 of 2 — the lender signs (there is no from parameter, but the lender must be the witness)
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnloans", "flagLiquidation", [loanId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

triggerLiquidation()

WRITE
triggerLiquidation(loanId: number)

Second step of an LTV liquidation, lender only. Requires a flag getLiquidationWindowMin() to getLiquidationWindowMax() seconds old (21,600 to 86,400 s on mainnet; devnet is set to 120 to 1,200 s for testing) and the loan's LTV at the reference pool's time-weighted average price since the flag (getLiquidationTwapLtv) above the threshold; an atomic swap in and out of the reference adds nothing to that average. Moves status to 4 (liquidated), makes the lender the pool's provider and sends the lender its SATURN certificate, records the average price, the time-weighted LTV and the time (getLoanLiquidationTriggerPrice / getLoanLiquidationTriggerLtv / getLoanLiquidationTriggeredAt), marks a default on the borrower's credit profile and notifies saturntaz so no reward is paid.

Parameters
NameTypeDescription
loanIdnumberFlagged loan to liquidate.
What to expect
Reverts on: "Loan not active", "Liquidation not flagged - call flagLiquidation first", "Liquidation flag too recent - wait getLiquidationWindowMin() seconds after flagging", "Liquidation flag expired - re-flag", "Flag has no price snapshot (set before 1.0.2) - re-flag", "Reference pool changed since the flag - re-flag", "Reference TWAP restarted since the flag - re-flag", "Time-weighted LTV not above liquidation threshold" (the position recovered) or "Only the lender can flag, liquidate or default this loan". Threshold is per 10,000 (9,000 = 90% LTV today).
Example
// Step 2 of 2 — the lender, 6 to 24 hours after flagLiquidation() on mainnet
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnloans", "triggerLiquidation", [loanId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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 collateral value in TAZ (saturnvault.getCollateralValue): a pledged RA/TAZ pool is valued at its fair-LP value 2 × √(k × P), with P the spot TAZ-per-RA price of the reference pool the admin pins (saturndexadapt.getReferencePool). If the collateral value is zero it returns 10,000. This spot figure only gates flagLiquidation(); triggerLiquidation() uses the time-weighted LTV (getLiquidationTwapLtv).

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; a flag needs more than getLiquidationThreshold() (9000 today)

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 ("TAZ" for every saturnmarket loan in v1.0).

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]);
// mainnet loan 1: "status:1_token:TAZ_principal:5000000000_owed:5002876712_repaid:0_rate:300_origin:2_installments:0/1"
// principal, owed and repaid are 8-decimal scaled: 50 TAZ at 3% APR for 7 days

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. installmentOverdue() returns 1 once the current time is past this timestamp plus the grace period. A late installment payment only costs credit score; the lender can default the loan only after getLoanDueDate() plus the grace period. For a loan shorter than 30 days this timestamp falls after the due date.

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.

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag. Mainnet and devnet report "saturnloans-1.0.3".

Returns
string — Build tag, e.g. "saturnloans-1.0.3".
What to expect
Never reverts.
Example
const v = await readContract("saturnloans", "getContractVersion", []);
// "saturnloans-1.0.3"

Lender Escrow & TAZ Reward Claim

withdrawLenderBalance()

WRITE
withdrawLenderBalance(from: address, tokenSymbol: string)

The lender takes out everything escrowed for them in tokenSymbol. Since 1.0.3 the lender's share of every makePayment is credited to this balance instead of being sent to the lender, so a lender whose wallet refuses the token cannot block repayments. The balance is zeroed before the transfer.

Parameters
NameTypeDescription
fromaddressLender (must be the transaction witness).
tokenSymbolstringLoan token, "TAZ" in v1.0.
What to expect
Reverts with "Nothing to withdraw", "Not authorized" or "Reentrancy detected".
Example
const due = await readContract("saturnloans", "getLenderBalance", [from, "TAZ"]); // raw TAZ
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnloans", "withdrawLenderBalance", [from, "TAZ"])
  .spendGas(from)
  .endScript();
// sign with the lender's wallet and send

getLenderBalance()

READ
getLenderBalance(lender: address, tokenSymbol: string): number

What the lender can withdraw now with withdrawLenderBalance: the lender's shares of repayments, in raw token units.

Parameters
NameTypeDescription
lenderaddressLender address.
tokenSymbolstringLoan token, "TAZ" in v1.0.
Returns
number — Raw token amount (TAZ has 9 decimals).
What to expect
Never reverts. 0 when nothing is owed.
Example
const due = await readContract("saturnloans", "getLenderBalance", [lenderAddr, "TAZ"]);

claimRepaymentReward()

WRITE
claimRepaymentReward(loanId: number)

Pays the saturntaz TAZ reward of a loan fully repaid under 1.0.3: the borrower's and lender's shares are sent to them, the pledgers' shares are credited to their claimable balances (saturntaz.claimPledgeRewards). Anyone may call it, once, until getLoanRewardClaimDeadline(loanId): one reward day after the repayment (saturntaz getRewardParam("daySeconds"), 0 = 86,400 s). The reward may be 0 (see saturntaz.previewRewardForLoan); a wallet refusing TAZ only makes this call revert, the repayment stands.

Parameters
NameTypeDescription
loanIdnumberA loan with status 2 (repaid).
What to expect
Reverts with "Loan not repaid", "Loan not repaid under saturnloans 1.0.3: its reward was settled at repayment", "Reward claim window closed" or "Reward already claimed".
Example
// Anyone can send it; the borrower usually does right after the final payment
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnloans", "claimRepaymentReward", [loanId])
  .spendGas(from)
  .endScript();
// sign with any wallet and send

getLoanRewardClaimDeadline()

READ
getLoanRewardClaimDeadline(loanId: number): number

Last unix second claimRepaymentReward accepts the loan; 0 when it never will (not repaid, or repaid before 1.0.3, when the reward was settled at repayment). saturntaz.getLoanRewardPaid(loanId) tells whether the reward went out.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Unix seconds, or 0.
What to expect
Never reverts.
Example
const deadline = await readContract("saturnloans", "getLoanRewardClaimDeadline", [loanId]);

Liquidation Window & Time-Weighted LTV

getLiquidationWindowMin()

READ
getLiquidationWindowMin(): number

Minimum age in seconds of a liquidation flag before triggerLiquidation accepts it. 21,600 (6 h) on mainnet; devnet is set to 120 for testing.

Returns
number — Seconds.
What to expect
Never reverts.
Example
const minAge = await readContract("saturnloans", "getLiquidationWindowMin", []);

getLiquidationWindowMax()

READ
getLiquidationWindowMax(): number

Maximum age in seconds of a liquidation flag; an older flag must be replaced with a new flagLiquidation. 86,400 (24 h) on mainnet; devnet 1,200.

Returns
number — Seconds.
What to expect
Never reverts.
Example
const maxAge = await readContract("saturnloans", "getLiquidationWindowMax", []);

getLoanLiquidationFlaggedAt()

READ
getLoanLiquidationFlaggedAt(loanId: number): number

Unix time of the lender's last flagLiquidation on this loan, 0 if never flagged.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Unix seconds, or 0.
What to expect
Never reverts.
Example
const flaggedAt = await readContract("saturnloans", "getLoanLiquidationFlaggedAt", [loanId]);

getLoanLiquidationFlagReference()

READ
getLoanLiquidationFlagReference(loanId: number): number

The reference pool id stored with the flag. 0 means a flag from before 1.0.2, which triggerLiquidation refuses (re-flag).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Pool ID, or 0.
What to expect
Never reverts.

getLoanLiquidationFlagCumulative()

READ
getLoanLiquidationFlagCumulative(loanId: number): number

The reference pool's accumulated TAZ-per-RA price at the flag (price × 10^18 × seconds), the starting point of the average triggerLiquidation uses.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Cumulative price at the flag.
What to expect
Never reverts.

getLiquidationAveragePrice()

READ
getLiquidationAveragePrice(loanId: number): number

The reference pool's average TAZ-per-RA price from the flag to now, scaled by 10^18: the price triggerLiquidation would use now.

Parameters
NameTypeDescription
loanIdnumberA flagged loan.
Returns
number — Average TAZ per RA × 10^18.
What to expect
Reverts with "Liquidation not flagged", "Flag too recent to average" (same second), or the trigger's refusals (no snapshot, reference changed, TWAP restarted).
Example
const avg = await readContract("saturnloans", "getLiquidationAveragePrice", [loanId]);
const tazPerRa = Number(BigInt(avg) / 10n ** 10n) / 1e8;

getLiquidationTwapLtv()

READ
getLiquidationTwapLtv(loanId: number): number

The loan's LTV (per 10,000) at that average price. triggerLiquidation needs it above getLiquidationThreshold() while the flag is inside the window. Poll it: the lender to decide when to trigger, the borrower to decide when to repay.

Parameters
NameTypeDescription
loanIdnumberA flagged loan.
Returns
number — Time-weighted LTV per 10,000.
What to expect
Same reverts as getLiquidationAveragePrice.
Example
const twapLtv = await readContract("saturnloans", "getLiquidationTwapLtv", [loanId]);

getLoanLiquidationTriggerPrice()

READ
getLoanLiquidationTriggerPrice(loanId: number): number

The average TAZ-per-RA price (× 10^18) triggerLiquidation decided on; 0 for a loan it never liquidated (and for a default).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Price × 10^18, or 0.
What to expect
Never reverts.

getLoanLiquidationTriggerLtv()

READ
getLoanLiquidationTriggerLtv(loanId: number): number

The time-weighted LTV (per 10,000) triggerLiquidation decided on; 0 for a loan it never liquidated (and for a default).

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — LTV per 10,000, or 0.
What to expect
Never reverts.

getLoanLiquidationTriggeredAt()

READ
getLoanLiquidationTriggeredAt(loanId: number): number

When the loan was liquidated or defaulted (unix seconds); 0 otherwise.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
Returns
number — Unix seconds, or 0.
What to expect
Never reverts.

Admin & Internal

createLoan()

WRITE
createLoan(borrower: address, lender: address, loanTokenSymbol: string, loanTokenDexVersion: number, principal: number, interestRate: number, duration: number, collateralId: number, origin: number): number

Internal: only saturnmarket or saturnauto can call this. saturnmarket.acceptQuote calls it to open a loan (saturnauto's writes are disabled in v1.0). It computes the interest (saturnlendcfg.calculateInterest), the installment count and amount, and the origination fee, then writes the loan with status 1 (active), due date now + duration and the first installment due one installment interval from now. It links the collateral (saturnvault.linkToLoan), counts the loan in saturncredit (markLoanCreated), registers it in saturntaz (onLoanCreated, with the principal's RA value from saturndexadapt.priceInAnchor and duration / 86,400 whole days), and indexes it under the borrower and the lender. Principal and the owed amounts are 8-decimal scaled. It moves no tokens: saturnmarket pays out after it returns.

Parameters
NameTypeDescription
borroweraddressBorrower.
lenderaddressLender (the saturnauto contract for an auto loan).
loanTokenSymbolstringLoan token, "TAZ" in v1.0.
loanTokenDexVersionnumberDEX that prices the loan token against RA: 1 = v3, 2 = v4.
principalnumberPrincipal, 8-decimal scaled (saturnmarket passes saturndexadapt.scaleUp(loanAmount)).
interestRatenumberAnnual rate per 10,000 (1,000 = 10%).
durationnumberTerm in seconds.
collateralIdnumbersaturnvault collateral ID: locked (status 1) and not linked to a loan yet.
originnumber1 = auto (saturnauto), 2 = P2P market (saturnmarket).
Returns
number — The new loan ID.
What to expect
Reverts with "Only lending contracts" for any other caller and "Invalid loan token dex version" unless loanTokenDexVersion is 1 or 2. The calls it makes can revert too: saturnvault.linkToLoan with "Collateral not locked" or "Already linked to a loan", saturntaz.onLoanCreated with "Loan already registered", and saturndexadapt.priceInAnchor with "No reference RA/TAZ pool: the admin pins a burned or time-locked one" while no reference pool qualifies.

setLiquidationWindow()

WRITE
setLiquidationWindow(minSeconds: number, maxSeconds: number)

Admin only. The saturnlendcfg admin (saturnlendcfg.getAdmin()) must sign. Sets how old a liquidation flag must be before triggerLiquidation accepts it (minSeconds) and when it expires (maxSeconds). If it was never set, the window is 21,600 s (6 h) to 86,400 s (24 h). Mainnet reads those defaults today; devnet is set to 120 to 1,200 s. The new window applies to every flag, live ones included, from the next trigger on. Read it back with getLiquidationWindowMin() and getLiquidationWindowMax().

Parameters
NameTypeDescription
minSecondsnumberMinimum flag age in seconds, at least 60.
maxSecondsnumberMaximum flag age in seconds: at least minSeconds + 60 and at most 604,800 (7 days).
What to expect
Reverts with "Only admin", "Minimum window must be at least 60 s", "Maximum window must be at least 60 s above the minimum" or "Maximum window must be at most 604800 s (7 days)".
Lending Protocol · Contract #5

SaturnAuto

saturnauto

Protocol-managed lending reserve with algorithmic loan origination. It was designed so lenders deposit tokens into a shared pool and borrowers receive auto-approved loans sized and priced by credit score: LTV 25–80% by credit tier. With the live saturnlendcfg config the rate is about 20% a year (2000 − score × 15 / 1000 per 10,000) and the term is 30 + score × 150 / 1000 days (60 days at the 200 base score, about 157 days at 850, the highest reachable score); 3–30% and 7–365 days are only the clamps. LP collateral would get 5% off the rate (rate − rate/20) and 10% more time. No token is approved and every reserve reads 0 on mainnet and devnet. 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. The rate and duration shown are the plain terms (no LP bonus). loanAmount is raw; the amounts returned are 8-decimal scaled, so 10 KCAL (100000000000 raw, 10 decimals) and 10 SOUL (1000000000 raw) give the same numbers.

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 with "User not registered" if `user` never registered with saturncredit (registerUser, or saturnmarket.postLoanRequest). An unknown token symbol fails in the chain's token lookup. The saturnauto approval list is not checked. maxLTV and rate are per 10,000 (3500 = 35%, 1997 = 19.97% a year). Duration in seconds. interest, totalOwed, perInstallment and origFee are 8-decimal scaled units of the loan token.
Example
const terms = await readContract("saturnauto", "previewLoanTerms",
  [borrower, "KCAL", 100000000000]);   // 10 KCAL (10 decimals, raw)
// mainnet, borrower with score 207:
// "score:207_maxLTV:3500_rate:1997_duration:5274720_interest:33401876_totalOwed:1033401876_installments:3_perInstallment:344467292_origFee:10000000"
const t = Object.fromEntries(terms.split("_").map(p => p.split(":")));

Reserve Views

getReserveBalance()

READ
getReserveBalance(tokenSymbol: string): number

Returns the current available (unlent) scaled balance of a given token in the protocol reserve. Deposits are disabled in v1.0; it reads 0 for every token on mainnet and devnet.

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. The registry is empty on mainnet and devnet (getApprovedTokenCount() = 0), so this returns 0.

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", []);
// [] on mainnet and devnet (no token approved)

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. It reads 1 on mainnet and devnet even though every write reverts, so do not use it alone as a status indicator.

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. To lend now, answer a borrower's request with saturnmarket.submitQuote() (there is no postLoanOffer).

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 would be pledged in saturnpools with saturnvault holding its SATURN certificate, and provider fees keep accruing to the borrower. saturnvault 1.1.0 accepts v4 pool collateral from saturnmarket only; 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). v3 LP NFT collateral is also disabled in saturnvault 1.1.0 ("v3 LP NFT collateral disabled") and saturnmarket.postLoanRequest refuses collateral type 3, so there is no v3 LP loan path; pledge a v4 pool through saturnmarket instead.

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 one Saturn v4 pool of RA and TAZ that the borrower provides (single-token and v3 LP NFT collateral are refused); lenders browse requests, submit binding quotes with escrowed funds, and the borrower atomically accepts a quote to open the loan. The pool is pledged in saturnpools and its SATURN certificate held 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: a v4 pool of RA and TAZ that the borrower provides, whose SATURN certificate the borrower holds, and that is free of pledges, financial locks, campaigns, burn, time lock and fee redirect (saturndexadapt.v4PoolPledgeable(from, poolId) = 1). Types 1 (single token) and 3 (v3 LP NFT) revert. 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 is registered in saturncredit automatically. The request takes quotes 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 RA/TAZ pool (pledged). 1 (single token) and 3 (v3 LP NFT) revert.
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: an active RA/TAZ pool the borrower provides and whose SATURN certificate the borrower holds, with no pledge, financial or campaign lock, burn, time lock or fee redirect.
collateralNftIdnumberUnused (v3 LP NFT collateral is disabled). Pass 0.
preferredDurationnumberPreferred loan duration in seconds. Advisory only — lenders may quote different durations.
maxInterestRatenumberHighest annual rate the borrower will accept, per 10,000 (500 = 5% APR). Advisory: lenders can still quote higher, so filter quotes before accepting.
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', loan token has no RA pool on the chosen DEX, collateralType 3 ("v3 LP NFT collateral disabled - offer a v4 LP pool (2)"). For type 2: "Pool cannot be pledged: ..." (not active, not provided by from, certificate not held, already pledged, locked, in a campaign, burned, time-locked or fee-redirected) or "V4 pool must be RA-paired" (the pool is not RA/TAZ).
Example
// Borrow 50 TAZ against an RA/TAZ pool you provide (you hold its certificate), 5% APR max, open 7 days
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnmarket", "postLoanRequest", [
    from,          // from (borrower)
    "TAZ",         // loanTokenSymbol: TAZ only
    2,             // loanDexVersion: v4
    50000000000,   // loanAmount: 50 TAZ raw (9 decimals)
    2,             // collateralType: v4 pool
    "",            // collateralTokenSymbol (unused)
    0,             // collateralTokenAmount (unused)
    0,             // collateralDexVersion (unused)
    poolId,        // collateralPoolId: saturndexadapt.v4PoolPledgeable(from, poolId) must be 1
    0,             // collateralNftId (unused)
    604800,        // preferredDuration: 7 days (advisory)
    500,           // maxInterestRate: 5% APR, per 10,000 (advisory)
    "",            // message
    604800         // expiresInSeconds: 7 days (1 to 30 days)
  ])
  .spendGas(from)
  .endScript();
// sign with the borrower's wallet and send

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, null, gasPrice, gasLimit)
  .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 and colPoolId must be the pool the request offers (getRequestCollateralPoolId), still pledgeable by the borrower; types 1 and 3 revert. Interest is simple and fixed when the quote is accepted: principal × interestRate × duration / (10,000 × 31,536,000). 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.
interestRatenumberAnnual rate per 10,000 (300 = 3% APR), prorated over duration. Must be > 0; no upper bound is enforced.
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.
colTypenumberMust be 2 (the request's v4 pool). 1 and 3 revert.
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.
colPoolIdnumberMust equal the request's collateralPoolId (getRequestCollateralPoolId); a quote cannot ask for a different pool.
colNftIdnumberUnused (v3 LP NFT collateral is disabled). Pass 0.
messagestringOptional note to the borrower (terms, conditions). May be empty.
expiresInSecondsnumberSeconds the borrower has to accept. Not bounded by the contract. The escrow stays in saturnmarket after expiry until you call withdrawQuote.
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 (604,800 to 31,536,000 s today), lender's balance < offeredLoanAmount, colType 3. For colType 2: "Quote must name the pool the borrower offered in the request", "Requested pool not active", or the pool is no longer pledgeable or not RA/TAZ.
Example
// Quote 50 TAZ at 3% APR for 7 days; the collateral is the pool the request offers
const poolId = await readContract("saturnmarket", "getRequestCollateralPoolId", [requestId]);
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturnmarket", "submitQuote", [
    from,          // from (lender)
    requestId,
    300,           // interestRate: 3% APR, per 10,000
    604800,        // duration: 7 days (604800 to 31536000)
    50000000000,   // offeredLoanAmount: 50 TAZ raw (9 decimals), escrowed now
    2,             // colType: the request's v4 pool
    "",            // colTokenSymbol (unused)
    0,             // colTokenAmount (unused)
    0,             // colDexVersion (unused)
    poolId,        // colPoolId: must be the request's pool
    0,             // colNftId (unused)
    "",            // message
    172800         // expiresInSeconds: 2 days to accept
  ])
  .spendGas(from)
  .endScript();
// sign with the lender's wallet and send

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, expired or accepted with another quote, your quote expired, or you need your funds back: nothing is refunded automatically.

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, null, gasPrice, gasLimit)
  .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 pledged: saturnpools marks the pool pledged (getPoolPawned = 1, financial lock 1) and its SATURN certificate moves into saturnvault 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. Other quotes on the request stay pending with their escrow until their lenders call withdrawQuote. The quote's expiry is checked here, the request's is not.

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: "Quote must name the pool the borrower offered in the request", "You do not own this pool", or a pledge refusal (certificate not held, pool no longer free). No loan event: the lending contracts emit none (saturnpools logs only a PoolLockChanged pledge, plus the token transfers), so read getRequestLoanId(requestId) after the transaction.
Example
// Borrower accepts quote #7 — collateral locked, TAZ disbursed in the same tx
const tx = sb.begin()
  .allowGas(borrower, null, gasPrice, gasLimit)
  .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_..."). The score is saturncredit's cached score, which the lending flow never refreshes; simulate saturncredit.computeScore(borrower) for the live one. 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_onTime:5_late:0_defaults:0_bestStreak:5_totalLoans:5_active:0"

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. It is always 2 (a v4 RA/TAZ pool): types 1 and 3 are refused 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. The contract never writes 4 (expired): a request past getRequestExpiresAt() still reads 1 (and still counts in getTotalOpenRequests) but refuses new quotes, so compare the expiry yourself.

Parameters
NameTypeDescription
reqIdnumberLoan request ID.
Returns
number — 1 open | 2 accepted | 3 cancelled (4 is reserved and never set).
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 annual interest rate offered by the lender, per 10,000 (300 = 3% APR), prorated over the quote's duration.

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. It is always 2 (the request's own v4 pool): types 1 and 3 are refused at quote time.

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. A quote stays 1 after it expires and after the borrower accepts another quote; its escrow comes back only through withdrawQuote.

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])
  )
);

Admin & Internal

toggleMarket()

WRITE
toggleMarket(enabled: number)

Admin only. The saturnlendcfg admin (saturnlendcfg.getAdmin()) must sign. Opens (1) or pauses (0) the market. While it is paused, postLoanRequest, submitQuote and acceptQuote revert with "Market is disabled". cancelRequest and withdrawQuote still work, so borrowers can close requests and lenders can take back their escrow. Open loans are not affected: repayment, liquidation and default run in saturnloans. The market is open (1) on mainnet and devnet today; read it with getMarketEnabled().

Parameters
NameTypeDescription
enablednumber1 = open, 0 = paused.
What to expect
Reverts with "Only admin" or "Must be 0 or 1".
Lending Protocol · Contract #7

SaturnDexAdapt

saturndexadapt saturndexadapt-1.3.0

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 only when the pair is the anchor token (RA) and TAZ, in either order, and 0 for every other pair, other RA pairs included. Since 1.1.0 this is the only pair the market and the vault accept as collateral.

Parameters
NameTypeDescription
tokenAstringFirst token symbol.
tokenBstringSecond token symbol.
Returns
number — 1 for RA/TAZ in either order, 0 otherwise.
What to expect
Pure computation, no on-chain reads. Never reverts.
Example
const ok = await readContract("saturndexadapt", "anchoredPair", ["RA", "TAZ"]);
// 1 (["MKST", "RA"] gives 0)

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. On v4 the adapter walks every pool registered for the token/RA pair and returns 1 if any of them is active with both reserves above 0 (pricing then uses the deepest one). For TAZ it returns 1 only while the reference pool qualifies (getReferencePool() > 0), whatever dexVersion is. A token whose value is 1 can be priced via priceInAnchor. It says nothing about collateral: only an RA/TAZ pool can back a loan (anchoredPair).

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]);
// 0 on mainnet (MKST does not exist there). A 1 only means the token can be priced; only an RA/TAZ pool can back a loan.

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, null, gasPrice, gasLimit)
  .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 be removed and its provider cannot change its fee; swaps, addLiquidity and provider fee accrual continue. For a loan pledge specifically, read v4PoolPledged.

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)

Internal: only saturnvault can call this (depositV4PoolCollateral, from saturnmarket.acceptQuote). Pledges a v4 pool to a loan (since 1.3.0): it reads saturnvault.getContractVersion, so a vault older than 1.1.0 cannot pledge; checks that the pool is active, that from is the provider and holds the SATURN certificate, and that the pool is free (no fee redirect, financial or campaign lock, burn or time lock); then calls saturnpools.pledgePool, which sets getPoolPawned = 1 and the financial lock to exactly 1. Swaps, fee accrual, the borrower's provider-fee claims and addLiquidity continue (added liquidity becomes collateral); removal, provider fee changes, burn / time lock, new products and campaigns are blocked.

Parameters
NameTypeDescription
fromaddressPool provider address; must be the pool's registered provider.
poolIdnumberID of the v4 pool to lock.
What to expect
Reverts with "Only vault", "Pool not active", "Only pool provider", "Pool has active fee redirect", "Pool already under another financial product" (financial lock count above 0), "Pool is enrolled in a reward campaign", "Pool liquidity is burned or time-locked - it cannot back a loan" or "You do not hold this pool's SATURN certificate". saturnpools.pledgePool re-checks the same conditions (and that from signed) and can refuse too.
Example
// Not called directly — invoked by saturnvault during collateral deposit.
// Shown for integration reference only.

v4UnlockPool()

WRITE
v4UnlockPool(poolId: number)

Internal: only saturnvault can call this (releaseCollateral, on full repayment). Releases the pledge (saturnpools.releasePledge: getPoolPawned back to 0, the lock down by one). A liquidation or default goes through v4HandOverPool instead, which makes the lender the provider.

Parameters
NameTypeDescription
poolIdnumberID of the v4 pool to unlock.
What to expect
Reverts with "Only vault". saturnpools.releasePledge refuses with "Pool not pledged" or "Pledged pool has no financial lock".
Example
// Not called directly — invoked by saturnvault during collateral release.
// Shown for integration reference only.

v4HandOverPool()

WRITE
v4HandOverPool(poolId: number, recipient: address)

Internal: only saturnvault can call this (liquidateV4PoolCollateralTo, on a liquidation or default). It calls saturnpools.handOverPledgedPool: recipient, the lender, becomes the pool's provider, and the pledge (getPoolPawned) and the financial lock go back to 0. saturnpools logs two PoolLockChanged events ("financial" and "handover"). The vault then sends the lender the SATURN certificate.

Parameters
NameTypeDescription
poolIdnumberThe pledged v4 pool.
recipientaddressLender, the pool's new provider.
What to expect
Reverts with "Only vault". saturnpools refuses with "Pool not pledged", "Pledged pool must hold exactly one financial lock", "Pool not active", "Invalid new provider" or "New provider is already the provider".

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 reserves of the RA-paired pool on the specified DEX version — on v4, the deepest active pool of the pair (largest reserve product). TAZ is always priced at the reference pool (getReferencePool). The lending protocol uses it for the loan's RA value at creation; collateral is valued with v4PoolValueInBase instead. 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 TAZ worth? TAZ is priced at the reference pool
const scaledTaz = await readContract("saturndexadapt", "scaleUp", [50_000_000_000, "TAZ"]);
const raValue = await readContract("saturndexadapt", "priceInAnchor", ["TAZ", scaledTaz, 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 much RA are 50 TAZ worth? TAZ is priced at the reference pool
const scaledTaz = await readContract("saturndexadapt", "scaleUp", [50_000_000_000, "TAZ"]);
const raEquiv = await readContract("saturndexadapt", "priceInToken",
  ["TAZ", scaledTaz, 2, "RA", 2]);
// 8-decimal scaled RA

LP Collateral Valuation

v4PoolValueInBase()

READ
v4PoolValueInBase(poolId: number, baseToken: string, baseDex: number): number

Values an RA/TAZ v4 pool at its fair-LP value 2 × √(a × t × P) in scaled TAZ, where a and t are its RA and TAZ reserves and P the spot TAZ-per-RA price of the reference pool (getReferencePool), then converts to baseToken (identity for "TAZ"). Since 1.1.0 this replaces the sum of both reserves at spot, so swapping junk into a pool or skewing it does not raise its value. This is the collateral value saturnvault and saturnloans.getCurrentLtv use.

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: "Only RA/TAZ pools can back a loan" for any other pair, and "No reference RA/TAZ pool: the admin pins a burned or time-locked one" while no reference qualifies.
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: "Only RA/TAZ LP NFTs can back a loan" unless the NFT's pair is RA/TAZ. "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]);

Reference Price & Pledge Checks

getReferencePool()

READ
getReferencePool(): number

The RA/TAZ v4 pool every TAZ price and collateral value reads: the pool the admin pinned, while it still qualifies (active, both reserves non-zero, liquidity burned or time-locked through saturnlplock). 0 while none qualifies; then TAZ has no price and requests, loans and LTV checks refuse. Mainnet: pool 33.

Returns
number — Pool ID, or 0.
What to expect
Never reverts. getPinnedReferencePool() returns the pinned id whether or not it still qualifies.
Example
const ref = await readContract("saturndexadapt", "getReferencePool", []); // 33 on mainnet

getPinnedReferencePool()

READ
getPinnedReferencePool(): number

The pool id the admin pinned as reference, whether or not it still qualifies (a time lock can run out).

Returns
number — Pool ID, or 0 if never pinned.
What to expect
Never reverts.

getReferenceSpotPrice()

READ
getReferenceSpotPrice(): number

The reference pool's spot TAZ per RA, scaled by 10^18 (getTwapScale), to compare with a time-weighted average.

Returns
number — TAZ per RA × 10^18.
What to expect
Reverts with "No reference RA/TAZ pool: the admin pins a burned or time-locked one" while no reference qualifies.
Example
const spot = await readContract("saturndexadapt", "getReferenceSpotPrice", []);

getReferenceCumulative()

READ
getReferenceCumulative(): number

The reference pool's accumulated TAZ-per-RA price (price × 10^18 × seconds, from saturnpools' TWAP accumulators). Two readings give the average over the time between them; saturnloans stores one with every liquidation flag.

Returns
number — Cumulative price.
What to expect
Reverts when there is no reference or it is not TWAP-tracked in saturnpools.
Example
const c = await readContract("saturndexadapt", "getReferenceCumulative", []);

getReferenceTwapSince()

READ
getReferenceTwapSince(): number

When saturnpools last started tracking the reference, or last found one of its sides empty (unix seconds); 0 when there is no reference or it is not tracked. A reading taken before this time cannot be averaged with one after it.

Returns
number — Unix seconds, or 0.
What to expect
Never reverts.

getTwapScale()

READ
getTwapScale(): number

The scale of every price here: 10^18 (the same as saturnpools.getTwapScale).

Returns
number — 1000000000000000000.
What to expect
Never reverts.

v4PoolValueAtPrice()

READ
v4PoolValueAtPrice(poolId: number, priceQ: number): number

v4PoolValueInBase in TAZ at a TAZ-per-RA price you supply (scaled by 10^18) instead of the reference's spot price. saturnloans uses it with the average price since a liquidation flag.

Parameters
NameTypeDescription
poolIdnumberAn RA/TAZ v4 pool.
priceQnumberTAZ per RA × 10^18, > 0.
Returns
number — Pool value in scaled TAZ.
What to expect
Reverts with "Only RA/TAZ pools can back a loan", "Price must be > 0", or while no reference qualifies.
Example
const spot = await readContract("saturndexadapt", "getReferenceSpotPrice", []);
const value = await readContract("saturndexadapt", "v4PoolValueAtPrice", [poolId, spot]); // scaled TAZ

v4PoolPledgeable()

READ
v4PoolPledgeable(owner: address, poolId: number): number

1 when owner could pledge the pool now: active, provided by owner, not pledged, no financial or campaign lock, withdrawable (not burned, no live time lock), no fee redirect, and owner holds its SATURN certificate. saturnmarket.postLoanRequest and submitQuote require it; the pair must also be RA/TAZ (anchoredPair), which this view does not check.

Parameters
NameTypeDescription
owneraddressWould-be borrower.
poolIdnumberv4 pool ID.
Returns
number — 1 = pledgeable, 0 = not.
What to expect
Never reverts.
Example
const ok = await readContract("saturndexadapt", "v4PoolPledgeable", [from, poolId]);

v4PoolPledged()

READ
v4PoolPledged(poolId: number): number

1 while the pool is pledged to a loan (saturnpools.getPoolPawned).

Parameters
NameTypeDescription
poolIdnumberv4 pool ID.
Returns
number — 1 = pledged, 0 = not.
What to expect
Never reverts.

v4PoolNftId()

READ
v4PoolNftId(poolId: number): number

The pool's SATURN certificate id, 0 when it has none (saturnpools.getPoolNftId).

Parameters
NameTypeDescription
poolIdnumberv4 pool ID.
Returns
number — Certificate id, or 0.
What to expect
Never reverts.

getPairToken()

READ
getPairToken(): string

The only token a collateral pool may pair with the anchor: "TAZ".

Returns
string — "TAZ".
What to expect
Never reverts.

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag. Mainnet and devnet report "saturndexadapt-1.3.0".

Returns
string — Build tag.
What to expect
Never reverts.

v3LpNftValueAtPrice()

READ
v3LpNftValueAtPrice(nftId: number, priceQ: number): number

v3LpNftValueInBase in TAZ at a TAZ-per-RA price you supply (scaled by 10^18, see getTwapScale) instead of the reference's spot price. It takes the NFT's pro-rata share (NFT liquidity / pool liquidity) of the v3 RA/TAZ pool and values it at 2 × √(a × t × P). saturnloans uses it for the time-weighted LTV of a v3 LP NFT loan. v3 LP NFT collateral is disabled since saturnvault 1.1.0, so no open loan needs it today.

Parameters
NameTypeDescription
nftIdnumberSATRN LP NFT ID of an RA/TAZ position.
priceQnumberTAZ per RA × 10^18, > 0.
Returns
number — Position value in scaled TAZ (8 decimals).
What to expect
Reverts with "Only RA/TAZ LP NFTs can back a loan", "v3 pool has no liquidity", "v3 NFT has no liquidity", "Price must be > 0", or "No reference RA/TAZ pool: the admin pins a burned or time-locked one" while no reference qualifies.
Example
const spot = await readContract("saturndexadapt", "getReferenceSpotPrice", []); // TAZ per RA x 10^18
const value = await readContract("saturndexadapt", "v3LpNftValueAtPrice", [nftId, spot]); // scaled TAZ

Admin & Internal

setAnchorToken()

WRITE
setAnchorToken(newAnchor: string)

Admin only. The saturnlendcfg admin (saturnlendcfg.getAdmin()) must sign. Replaces the anchor token every price routes through ("RA" on mainnet and devnet). Everything in lending is tied to it: after a change, existing RA/TAZ collateral pools and the RA/TAZ reference pool no longer qualify, so TAZ has no price and LTV reads and liquidations revert. saturntaz reads the anchor when it returns a pledge, so open RA pledges would no longer come back as RA.

Parameters
NameTypeDescription
newAnchorstringSymbol of an existing token.
What to expect
Reverts with "Only admin", "Anchor cannot be empty" or "Anchor token does not exist".

setReferencePool()

WRITE
setReferencePool(poolId: number)

Admin only. The saturnlendcfg admin (saturnlendcfg.getAdmin()) must sign. Pins the RA/TAZ v4 pool that every TAZ price, collateral value and liquidation average reads (pool 33 on mainnet, 223 on devnet). The pool must qualify now: active, the anchor paired with TAZ, both reserves above 0, liquidity burned or time-locked (saturnpools.getPoolWithdrawable = 0), TWAP-tracked in saturnpools, provided by the DEX admin (saturnadmin.getAdmin()), and free of financial products and fee redirects. There is no unpin. Pinning a different pool makes every live liquidation flag unusable: triggerLiquidation refuses it with "Reference pool changed since the flag - re-flag", and the lender must flag again.

Parameters
NameTypeDescription
poolIdnumberv4 pool ID.
What to expect
Reverts with "Only admin", "Reference must be an active RA/TAZ pool whose liquidity is burned or time-locked", "Reference must be TWAP-tracked in saturnpools (setTwapTracked) first", "Reference must be provided by the DEX admin (saturnadmin.getAdmin)", "Reference must not be under a financial product (rental, bond, fee option)" or "Reference must not have an active fee redirect".
Lending Protocol · Contract #8

SaturnTaz

saturntaz saturntaz-1.2.3

Awards TAZ on fully repaid TAZ loans, split by default 30% to the borrower, 30% to the lender and 40% to the lender's active RA pledgers. Since 1.2 the reward counts the whole days the loan actually ran (capped at its term, zero under minDurationDays), is sized by the TAZ principal, and never exceeds feeRebateBps of the fees the protocol earned on the loan (origination fee plus its share of the interest), so a loan between one's own wallets cannot profit. It is paid when anyone calls saturnloans.claimRepaymentReward(loanId) within one reward day of the repayment. With feeRebateBps or loanCapTaz at 0 every reward is 0: that is the mainnet setting on 2026-09-28 (devnet pays). 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 with "Not authorized" (from did not sign), "Wallet not registered as treasury depositor", "Amount must be > 0", "Insufficient TAZ balance" or "Yearly deposit cap exceeded - remaining budget too small for this deposit" (getYearlyRemaining() shows what is left in the current window).
Example
const tx = sb.begin()
  .allowGas(treasury, null, gasPrice, gasLimit)
  .callContract("saturntaz", "depositTaz", [treasury, 10_000_000_000_000n])
  .spendGas(treasury)
  .endScript();

getTazBalance()

READ
getTazBalance(): number

Returns the TAZ balance held by this contract. Part of it may already be owed to pledgers (getPledgerOwedTotal); what can still pay new rewards is getAvailableTreasury().

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: 2,592,000 (30 days) on mainnet, 300 on devnet for testing. 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 (0 for an unknown key). Keys: "baseReward" (raw TAZ when every factor is at its cap), "loanCapTaz" (scaled TAZ principal where the size factor saturates; 0 = no reward), "minLoanTaz" (scaled TAZ principal floor, 0 = none), "feeRebateBps" (share of the protocol fees earned on the loan the reward may reach, 10,000 = 100%; 0 = no reward), "durationCapDays" (elapsed days where the time factor saturates), "daySeconds" (length of a counted day, 0 = 86,400), "pledgeCapRA" (scaled RA pledge where the pledge factor saturates), "minDurationDays" (elapsed-day floor), "minPledgeRA" (floor of the lender's eligible pledge, scaled RA), "minPledgeEach" (smallest single pledge, scaled RA), "maxRewardPerLoan" (raw TAZ ceiling per loan), "borrowerBps", "lenderBps", "pledgerBps" (the split). "loanCapRA" and "minLoanRA" are retired since 1.2 and no longer read.

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 pledge must be withdrawn before re-pledging" or "Existing v4 pledge must be withdrawn before switching pledge type" if an earlier pledge to this lender was not withdrawn. "Pledge amount rounds to zero" if amount is too small to survive scaleUp. "Pledge below the minimum per pledge" under getRewardParam("minPledgeEach") (100,000,000 scaled = 1 RA today). Amounts count in whole 8-decimal RA units; a ninth decimal is neither taken nor locked. "Lender pledger cap reached" if lender already has maxPledgersPerLender active pledgers.
Example
const tx = sb.begin()
  .allowGas(pledger, null, gasPrice, gasLimit)
  .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, null, gasPrice, gasLimit)
  .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 - wait until expiry" if expiry > now. "Existing v4 pledge must be withdrawn before re-pledging" or "Existing v3 pledge must be withdrawn before switching pledge type" if an earlier pledge to this lender was not withdrawn. saturnholders.lockForPledge reverts if from lacks sufficient unlocked stake. "Pledge below the minimum per pledge" under getRewardParam("minPledgeEach"). "Lender pledger cap reached" if the lender's pledger list is full.
Example
const tx = sb.begin()
  .allowGas(pledger, null, gasPrice, gasLimit)
  .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, null, gasPrice, gasLimit)
  .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 shares are not claimed here: they are sent directly when saturnloans.claimRepaymentReward(loanId) runs (anyone may call it once, within one reward day of the full repayment).

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, null, gasPrice, gasLimit)
  .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

Runs the reward formula (before the 30/30/40 split) for a hypothetical loan and the lender's current eligible pledge. Since 1.2 the first argument is the loan principal in 8-decimal scaled TAZ (the parameter keeps its old name) and durationDays the whole days the loan will have run. The fee cap (feeRebateBps) and the treasury clamp need a real loan and are not applied, so this is an upper bound; previewRewardForLoan(loanId) gives the real figure for an open loan.

Parameters
NameTypeDescription
loanValueInRAnumberSince 1.2: the loan principal in scaled (8-decimal) TAZ, e.g. 5,000,000,000 for 50 TAZ.
durationDaysnumberWhole days the loan will have run at repayment (the time factor saturates at durationCapDays).
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 the principal < minLoanTaz, durationDays < minDurationDays, eligible pledge < minPledgeRA, or loanCapTaz, durationCapDays or pledgeCapRA is 0. Never reverts.
Example
// Upper bound for a 50 TAZ loan repaid after 30 days (no fee cap, no treasury clamp)
const principal = await readContract("saturnpools", "scaleUp", [50000000000, "TAZ"]); // 5,000,000,000
const reward = await readContract("saturntaz", "previewLoanReward",
  [principal, 30, lenderAddr]);
const borrowerShare = reward * 0.30;
const lenderShare   = reward * 0.30;
const pledgerShare  = reward * 0.40;

previewRewardForLoan()

READ
previewRewardForLoan(loanId: number): number

What saturnloans.claimRepaymentReward would pay in total (raw TAZ, before the borrower / lender / pledger split) if this loan were fully repaid now: the formula on its TAZ principal and elapsed days, the fee cap and the treasury clamp all applied. 0 once paid, for an unregistered loan, or when borrower == lender.

Parameters
NameTypeDescription
loanIdnumberLoan ID in saturnloans.
Returns
number — Raw TAZ (9 decimals).
What to expect
Never reverts. Returns 0 on mainnet today because feeRebateBps and loanCapTaz are 0 there.
Example
const total = await readContract("saturntaz", "previewRewardForLoan", [loanId]);

getAvailableTreasury()

READ
getAvailableTreasury(): number

The TAZ balance less what is owed to pledgers: what can still pay new rewards. A reward larger than this is cut down to it.

Returns
number — Raw TAZ.
What to expect
Never reverts.
Example
const free = await readContract("saturntaz", "getAvailableTreasury", []);

getPledgerOwedTotal()

READ
getPledgerOwedTotal(): number

TAZ credited to pledgers and not yet claimed (tracked since 1.2.2).

Returns
number — Raw TAZ.
What to expect
Never reverts.

getPledgerOwedOf()

READ
getPledgerOwedOf(pledger: address): number

The part of a pledger's claimable balance counted in getPledgerOwedTotal. Lower than getPledgerClaimable only for a balance credited before 1.2.2; trackPledgerClaimable fixes that.

Parameters
NameTypeDescription
pledgeraddressPledger address.
Returns
number — Raw TAZ.
What to expect
Never reverts.

trackPledgerClaimable()

WRITE
trackPledgerClaimable(pledger: address)

Counts a pledger's balance credited before 1.2.2 in the owed total, so the treasury stops spending it on new rewards. Open to anyone and idempotent: it adds only what is owed and not yet counted.

Parameters
NameTypeDescription
pledgeraddressPledger whose balance to track (no signature needed from them).
What to expect
Never reverts; does nothing when the balance is already counted.
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("saturntaz", "trackPledgerClaimable", [pledgerAddr])
  .spendGas(from)
  .endScript();

getContractVersion()

READ
getContractVersion(): string

Returns the deployed build tag. Mainnet and devnet report "saturntaz-1.2.3".

Returns
string — Build tag, e.g. "saturntaz-1.2.3".
What to expect
Never reverts.
Example
const v = await readContract("saturntaz", "getContractVersion", []);

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; it never goes down, and a freed slot reads as the null address). 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 once the loan's reward claim has run (saturnloans.claimRepaymentReward), even if the reward was 0; 0 before that, and always 0 when borrower == lender. A loan is settled only 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. Informational since 1.2: the reward is sized by the TAZ principal (saturnloans.getLoanPrincipal), not by this value.

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. Since 1.2 it only caps the elapsed days the reward counts.

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 whose reward claim ran (saturnloans.claimRepaymentReward), even when the reward came out 0. It is 0 on mainnet today.

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", []);

Admin & Internal

addAuthorizedDepositor()

WRITE
addAuthorizedDepositor(wallet: address)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Lets wallet fund the reward treasury with depositTaz. Check it with getAuthorizedDepositor(wallet).

Parameters
NameTypeDescription
walletaddressTreasury wallet to allow.
What to expect
Reverts with "Only owner" or "Invalid wallet" (null address).

removeAuthorizedDepositor()

WRITE
removeAuthorizedDepositor(wallet: address)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Takes wallet off the depositor allowlist (getAuthorizedDepositor returns 0). Its next depositTaz reverts with "Wallet not registered as treasury depositor". TAZ it already deposited stays in the treasury.

Parameters
NameTypeDescription
walletaddressTreasury wallet to remove.
What to expect
Reverts with "Only owner".

withdrawTaz()

WRITE
withdrawTaz(to: address, amount: number)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Sends amount raw TAZ (9 decimals) from the treasury to `to`. Only the available treasury can leave (getAvailableTreasury(): the balance minus what pledgers are owed and have not claimed), so pledger claims stay funded. A withdrawal does not lower getCurrentYearDeposited(), so it frees no deposit budget.

Parameters
NameTypeDescription
toaddressRecipient.
amountnumberRaw TAZ (9 decimals), > 0.
What to expect
Reverts with "Only owner", "Amount must be > 0" or "Insufficient treasury TAZ (the rest is owed to pledgers)".

setTazSymbol()

WRITE
setTazSymbol(newSym: string)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Changes the token the treasury holds and pays rewards in ("TAZ" on mainnet and devnet; meant for testing). Deposits, payouts, claims and the treasury balance all switch to the new symbol at once. The old token's balance stays in the contract, and a loan whose token is not the new symbol earns no reward.

Parameters
NameTypeDescription
newSymstringSymbol of an existing token.
What to expect
Reverts with "Only owner", "Symbol cannot be empty" or "Token does not exist".

setPledgeDurationSeconds()

WRITE
setPledgeDurationSeconds(secs: number)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Sets the length of new pledges (pledgeV3 / pledgeV4), from 60 s to 365 days: 2,592,000 (30 days) on mainnet, 300 on devnet. Pledges already made keep the expiry they were given.

Parameters
NameTypeDescription
secsnumberPledge length in seconds, 60 to 31,536,000.
What to expect
Reverts with "Only owner", "Pledge duration must be at least 60 seconds" or "Pledge duration must be at most 365 days".

setMaxPledgersPerLender()

WRITE
setMaxPledgersPerLender(n: number)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Sets how many places one lender's pledger list has (30 on mainnet, 2 on devnet). distributeReward, the previews and every pledge walk this list, so keep it small. A place whose pledge has ended is reused, so only running pledges fill it. Lowering the cap removes no one already in the list; it only stops the list from growing past the new cap.

Parameters
NameTypeDescription
nnumberPlaces per lender, > 0.
What to expect
Reverts with "Only owner" or "Cap must be > 0".

setYearlyDepositCap()

WRITE
setYearlyDepositCap(cap: number)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Sets the most raw TAZ all authorized depositors together may deposit per yearly window: 40,000,000,000,000 (40,000 TAZ) on mainnet and devnet. No bounds are checked, and 0 stops all deposits. It applies to the current window at once, against what was already deposited in it.

Parameters
NameTypeDescription
capnumberRaw TAZ (9 decimals) per window.
What to expect
Reverts with "Only owner".

setYearlyDurationSeconds()

WRITE
setYearlyDurationSeconds(secs: number)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Sets the length of the deposit window (31,536,000 s = 365 days on mainnet and devnet), at least 1 day. The current window keeps its start time; the new length decides when depositTaz rolls it over.

Parameters
NameTypeDescription
secsnumberWindow length in seconds, at least 86,400.
What to expect
Reverts with "Only owner" or "Yearly duration must be at least 1 day".

resetYearWindow()

WRITE
resetYearWindow()

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Starts a new deposit window now: getYearStartTime() becomes the current time and getCurrentYearDeposited() 0, so the full yearly cap is available again.

What to expect
Reverts with "Only owner".

setRewardParam()

WRITE
setRewardParam(key: string, value: number)

Admin only. The saturntaz _owner must sign: the address set when the contract was deployed, which no method reads or changes. Sets one reward parameter by key. getRewardParam lists the keys and their units (raw TAZ, scaled TAZ or RA, days, seconds or basis points). Nothing is checked: any key is stored, a misspelt key is simply never read, and borrowerBps + lenderBps + pledgerBps is not checked to add up to 10,000. With feeRebateBps, loanCapTaz or maxRewardPerLoan at 0 every reward is 0. All three are 0 on mainnet on 2026-09-28, so mainnet pays no repayment rewards; devnet has feeRebateBps 5,000, loanCapTaz 300,000,000,000 and maxRewardPerLoan 30,000,000,000 (30 TAZ). Setting feeRebateBps to 0 is the off switch. Keep daySeconds at 0 (86,400 s) on mainnet: saturnloans also uses it as the claimRepaymentReward window.

Parameters
NameTypeDescription
keystringParameter key, e.g. "feeRebateBps".
valuenumberNew value, in the key's own unit.
What to expect
Reverts with "Only owner".

onLoanCreated()

WRITE
onLoanCreated(loanId: number, lender: address, loanValueInRA: number, durationDays: number)

Internal: only saturnloans can call this (createLoan). It registers the loan for rewards and stores its lender, its RA value (8-decimal scaled, informational since 1.2; see getLoanValueRA) and its quoted term in whole days (the cap on counted days; see getLoanDurationDays).

Parameters
NameTypeDescription
loanIdnumberNew loan ID.
lenderaddressThe loan's lender.
loanValueInRAnumberPrincipal valued in RA, 8-decimal scaled.
durationDaysnumberQuoted term in whole days (duration / 86,400).
What to expect
Reverts with "Only saturnloans" or "Loan already registered".

onLoanResolved()

WRITE
onLoanResolved(loanId: number)

Internal: only saturnloans can call this (on full repayment, default and liquidation). For a registered loan it adds 1 to getTotalLoansResolved(); nothing else. Pledges run on fixed windows, so there is no lock to release.

Parameters
NameTypeDescription
loanIdnumberLoan ID.
What to expect
Reverts with "Only saturnloans".

distributeReward()

WRITE
distributeReward(loanId: number, borrower: address)

Internal: only saturnloans can call this (claimRepaymentReward, once per loan, within one reward day of the full repayment). It returns without paying when the reward was already paid, the loan was never registered, or borrower == lender. Otherwise it marks the loan paid and computes the reward from the TAZ principal, the whole days the loan ran (capped at its term) and the lender's running pledges. The reward is capped at maxRewardPerLoan, at feeRebateBps of the protocol fees earned on the loan, and at the available treasury; a shortfall lowers the payout and never reverts. The borrower's and lender's shares (borrowerBps, lenderBps) are sent to them directly. The pledgers' share (pledgerBps) is credited to their claimable balances in proportion to each running pledge, collected with claimPledgeRewards; a pledge by the borrower gets nothing and its part stays in the treasury. All amounts are raw TAZ.

Parameters
NameTypeDescription
loanIdnumberThe repaid loan.
borroweraddressThe loan's borrower (saturnloans passes getLoanBorrower(loanId)).
What to expect
Reverts with "Only saturnloans". A borrower or lender whose account script refuses TAZ makes this call, and so claimRepaymentReward, revert; the repayment itself stands.

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. The NFTs move wallet to wallet only (the onSend trigger requires the sender's signature), so they are not loan collateral: saturnvault 1.1.0 disabled v3 LP NFT collateral ("v3 LP NFT collateral disabled"). Amounts are raw token units everywhere on the live contract (build 3.9.9.2): the scaling globals were added by an upgrade and never initialised, so getTargetDecimals() reads 0, every scale factor is 1 (or 0 = not cached yet, treated as 1) and the getMinRawFor* floors are 0. 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", "TAZ").
What to expect
Returns an empty iterator when no pools exist. Each symbol appears at most once.
Example
const tokens = await readContract("SATRN", "getTokensInDEXList", []);
// mainnet: ["SOUL", "KCAL", "BNB", "RAA", "GAS", ...] (15 symbols)

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 pairs = await readContract("SATRN", "getPairsInDEXList", []);
// mainnet: ["SOUL_KCAL", "BNB_SOUL", "RAA_SOUL", ...] (20 pools)

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 read that side's reserve (raw token units on the live contract).

Returns
string* — Iterator of reserve keys, e.g. "SOUL_KCAL_SOUL" and "SOUL_KCAL_KCAL".
Example
const keys = await readContract("SATRN", "getPoolsAndReservesInDEXList", []);
// ["SOUL_KCAL_SOUL", "SOUL_KCAL_KCAL", "BNB_SOUL_BNB", ...]

getAllReserves()

READ
getAllReserves(): number*

Yields the global reserve for every token in the DEX, in the same order as getTokensInDEXList(). On the live contract the values are raw token units (native decimals): getTargetDecimals() reads 0, so every scale factor is 1 and nothing is scaled.

Returns
number* — Iterator of reserve totals in raw token units, one per token.
Example
const reserves = await readContract("SATRN", "getAllReserves", []);
// same order as getTokensInDEXList(); mainnet SOUL reserve 2983592612562 = 29,835.9 SOUL

getReserveValue()

READ
getReserveValue(tokenSymbol: string): number

Returns the global reserve total for a single token across all pools, in raw token units on the live contract (scale factor 1). Useful for quickly checking whether a token has any liquidity in the DEX.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol, e.g. "SOUL".
Returns
number — Reserve in raw token units (native decimals). 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
Faults with "invalid index" (a chain list error, not a contract message) if select >= getCountOfTokensOnList().
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. It equals the number of keys getPairsInDEXList() yields (SATRN has no index getter for pairs; use getTokenPairAndReserveKeysOnList for indexed access, two keys per pool).

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
Faults with "invalid index" (a chain list error, not a contract message) if select >= getCountOfTokenPairsAndReserveKeysOnList().
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 that side's reserve. On the live contract this is already in raw token units (every scale factor is 1); do not divide by getScaleFactor, which returns 0 for tokens not cached yet (BNB, RAA, GAS, ETH on mainnet).

Parameters
NameTypeDescription
pairKeyPlusTokenToViewBalancestringComposite key "poolKey_tokenSymbol", e.g. "SOUL_KCAL_SOUL".
Returns
number — Reserve in raw token units. Returns 0 if the key is unknown (keys are case- and orientation-sensitive: "SOUL_KCAL_SOUL", not "KCAL_SOUL_SOUL").
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 readContract("SATRN", "getAllContractBalances", []);
// raw units, same order as getTokensInDEXList()

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 readContract("SATRN", "getAllTotalLiquidityPerPool", []);
// mainnet: ["SOUL_KCAL_134366801339856", "BNB_SOUL_3027607966695", ...]
// a pool reading 1000 has only the permanently locked minimum left

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). All fee slices are taken from amountIn in tokenIn: the pool receives amountIn minus the 0.09% and 0.1% slices, and the output is priced on amountIn minus 0.4%: out = inAfterFee × reserveOut / (reserveIn + inAfterFee), integer division, raw units. The 0.09% goes to the SATRN owner (getOwner). Holders of a Series-5 FACTORY NFT pay only 0.08% total (5× discount applied to all three slices). Only a direct pool between the two tokens is used; there is no multi-hop routing. 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 with: "Reentrancy detected" (guard set for `from`); "Amount must be > 0"; "Not authorized" (`from` did not sign); "Same token"; "Invalid minAmountOut"; "Pool does not exist"; "Pool has no liquidity"; "Zero reserve in" / "Zero reserve out"; "Below minimum swap: N" (cannot fire live: getMinRawForSwap reads 0); "Insufficient balance"; "Insufficient liquidity"; "Output rounds to zero - increase amount"; "Swap amount too small: admin fee rounds to zero. Increase amount." (amountIn × 9 / 10,000 < 1: below 1,112 raw units, or 5,556 with the FACTORY discount); "Swap too small: token owner fee rounds to zero. Increase amount." (never reached: an amount that passes the admin-fee check already gives a token-owner slice of at least 1); "Slippage: <out> < <min>" (checked only when minAmountOut > 0); "Cannot drain pool".
Example
// Quote 1 SOUL -> KCAL from the SOUL_KCAL reserves, then swap with 1% slippage
const rIn  = BigInt(await readContract("SATRN", "getTokenPairAndReserveKeysOnListVALUE", ["SOUL_KCAL_SOUL"]));
const rOut = BigInt(await readContract("SATRN", "getTokenPairAndReserveKeysOnListVALUE", ["SOUL_KCAL_KCAL"]));
const amountIn = 100000000n;                         // 1 SOUL (8 decimals)
const inAfterFee = amountIn - (amountIn * 4n) / 1000n; // 0.4% fee (FACTORY S5 holders: amountIn * 4n / 5000n)
const quote = (inAfterFee * rOut) / (rIn + inAfterFee); // raw KCAL (10 decimals)
const minOut = (quote * 99n) / 100n;

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("SATRN", "swap", [from, amountIn, "SOUL", "KCAL", minOut])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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`. The pool starts with isqrt(amountToken0 × amountToken1) LP units (raw amounts); the SATRN NFT minted to `from` gets that minus 1,000, which stay locked in the pool forever. 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 with: "Reentrancy detected"; "Only wallet owner can call."; "Token does not exist: <SYMBOL>"; "amountToken0 must be > 0" / "amountToken1 must be > 0"; "Same token"; "token0 needs at least N raw units" / "token1 needs at least N raw units" (cannot fire live: the floor reads 0); "Insufficient token0" / "Insufficient token1"; "Pool already exists" (either orientation; 20 pairs exist on mainnet, SOUL_KCAL among them); "Liquidity too low." (isqrt(amountToken0 × amountToken1) ≤ 1,000).
Example
// New pair only: SOUL_KCAL and 19 other pairs already exist ("Pool already exists")
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("SATRN", "initializePool", [from,
    100000000000,   // amountToken0: 1,000 MYTOKEN (8 decimals)
    10000000000,    // amountToken1: 100 SOUL (8 decimals); sets the price 1 MYTOKEN = 0.1 SOUL
    "MYTOKEN", "SOUL"])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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. Required second = amountFirstToken × reserveSecond / reserveFirst (integer division, raw units); LP minted = min(second × poolLP / reserveSecond, amountFirstToken × poolLP / reserveFirst). 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 with: "Reentrancy detected"; "Amount must be > 0"; "Not authorized"; "Same token"; "maxSecondTokenAmount must be > 0"; "Min N raw units for <SYMBOL>" (cannot fire live: the floor reads 0); "Pool does not exist"; "Pool has no liquidity"; "Reserve too low: <SYMBOL>"; "Rounds to zero"; "Exceeds max: N" (N = the required second-token amount); "Insufficient <SYMBOL>" (second-token balance); "Insufficient liquidity minted". The first token's balance is not pre-checked; a short balance fails in the token transfer.
Example
// Add 0.5 SOUL to SOUL_KCAL; allow up to the required KCAL + 1%
const rSoul = BigInt(await readContract("SATRN", "getTokenPairAndReserveKeysOnListVALUE", ["SOUL_KCAL_SOUL"]));
const rKcal = BigInt(await readContract("SATRN", "getTokenPairAndReserveKeysOnListVALUE", ["SOUL_KCAL_KCAL"]));
const amountSoul = 50000000n;                  // 0.5 SOUL (8 decimals)
const needKcal = (amountSoul * rKcal) / rSoul; // raw KCAL (10 decimals), about 10.95 KCAL today
const maxKcal = (needKcal * 101n) / 100n;

const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("SATRN", "addLiquidity_v2", [from, amountSoul, "SOUL", "KCAL", maxKcal])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

removeLiquidity()

WRITE
removeLiquidity(from: address, NFTuniqueID: number): bool

Burns a SATRN LP NFT and returns the proportional share of pool reserves to `from`. Each side pays `nftLiquidity × poolReserveOfToken / poolTotalLiquidity` (integer division, raw units). A side that rounds to 0 reverts with "LP too small for <SYMBOL> - consolidate first" — merge small NFTs with consolidatePositions() first. The maxTruncationPercent check only matters for scaled tokens; every scale factor is 1 on the live contract, so it never fires. 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.
NFTuniqueIDnumberID of the SATRN LP NFT to burn: a 256-bit integer from the wallet's SATRN balance (getAccount -> balances[].ids). Pass it as a BigInt.
Returns
bool — Always true on success.
What to expect
Reverts with: "Reentrancy detected"; "Not authorized"; "NFT not found" (`from` does not own the NFT); "No liquidity"; "No liquidity to remove"; "LP too small for <SYMBOL> - consolidate first"; "Truncation N% on <SYMBOL> - consolidate first" (cannot fire live: getMaxTruncationPercent reads 0 but nothing is scaled); "Drainage: ..." / "Liquidity drainage" (safety checks).
Example
// NFT IDs are 256-bit integers: take them from the wallet's SATRN balance (getAccount -> balances[].ids),
// not from getUserLiquidityPools_v2 (that returns pool keys)
const nftId = BigInt(account.balances.find(b => b.symbol === "SATRN").ids[0]);
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("SATRN", "removeLiquidity", [from, nftId])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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. The new NFT holds the summed LP units. Use it when removeLiquidity reverts with "LP too small for <SYMBOL> - consolidate first". `poolKey` must be the stored orientation, as getPairsInDEXList() returns it. TACAL rewards are no longer claimed here (removed in 3.9.8.13). 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 with: "Reentrancy detected"; "Not authorized"; "Pool does not exist" (no liquidity under that exact key, e.g. the reversed orientation); "Need at least 2 positions"; "No liquidity"; "Incomplete: ..." / "Mismatch: ..." (internal safety checks).
Example
const tx = ScriptBuilder
  .begin()
  .allowGas(from, null, gasPrice, gasLimit)
  .callContract("SATRN", "consolidatePositions", [from, "SOUL_KCAL"])
  .spendGas(from)
  .endScript();
// sign with the caller's wallet and send

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. It returns pool keys only, not NFT IDs: to map IDs to pools, read the wallet's SATRN ids (getAccount -> balances[].ids, 256-bit integers) and call getNFTticketKey(id) for each. 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 poolKeys = await readContract("SATRN", "getUserLiquidityPools_v2", [from]);
// e.g. ["SOUL_GDOG"]: one key per owned SATRN NFT

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. NFT IDs are 256-bit integers (pass them as BigInt or a decimal string).

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", [nftId]); // nftId: BigInt from the wallet's SATRN ids

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", [nftId]);

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", [nftId]);

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", [nftId]);
// "_" for an unknown or burned ID

getTotalNftMinted()

READ
getTotalNftMinted(): number

Returns the contract's own counter of SATRN LP NFTs (incremented on mint, decremented on burn). It has drifted from the real token supply: mainnet reads 26 while getToken("SATRN") reports currentSupply 33 (devnet 45 vs 57). To count live positions use the token's currentSupply or the holders' SATRN balances.

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. Mainnet returns that same address today. The admin changes it with setChainTokenFeeWallet.

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). Mainnet reads 62,500,000 (0.625 SOUL), which is exactly SATRN's SOUL balance minus its SOUL reserve; the admin can take that surplus with skim(to, "SOUL").

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. The source computes 10^(targetDecimals − decimals) for tokens with fewer decimals than the target, else 1. On the live contract targetDecimals reads 0, so every cached factor is 1 whatever the token's decimals (devnet's 0-decimal NODECIMAL reads 1 too). Tokens whose pools predate scaling and have not been touched since (BNB, RAA, GAS, ETH on mainnet) return 0 (not cached), which the contract treats as 1. The factor is cached by the next initializePool, addLiquidity_v2, swap, skim or getMinRawFor* transaction that touches the token.

Parameters
NameTypeDescription
tokenSymbolstringToken symbol to query.
Returns
number — Scale factor: 1 on the live contract, or 0 if not cached yet (treat as 1).
Example
const sf = await readContract("SATRN", "getScaleFactor", ["TAZ"]); // 1 (TAZ has 9 decimals)

getTargetDecimals()

READ
getTargetDecimals(): number

Returns the decimal target the scaling code scales tokens to. The source sets 8 in the constructor, but this global was added by an upgrade and the constructor never re-ran, so the live contract returns 0 and no token is scaled.

Returns
number — Target decimals: 0 on mainnet and devnet.
Example
const dec = await readContract("SATRN", "getTargetDecimals", []);

getMaxTruncationPercent()

READ
getMaxTruncationPercent(): number

Returns the maximum scale-down rounding loss (percent) that removeLiquidity allows before it asks the user to consolidate. It reads 0 on the live contract (never initialised after the upgrade); with every scale factor at 1 there is no rounding loss, so the check never fires. The admin can set 1–50 with updateMaxTruncationPercent.

Returns
number — Max truncation percent: 0 on the live contract (settable 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. Computed as max(minScaledPoolUnits / scaleFactor, absoluteMinRaw). Those globals are unset on the live contract, so it returns 0; the effective floor for a new pool is isqrt(amountToken0 × amountToken1) > 1,000 (else "Liquidity too low."). The view caches the token's scale factor first, so for a token SATRN has not cached yet (getScaleFactor returns 0: any token new to the DEX, and BNB, RAA, GAS and ETH on mainnet) a free invokeRawScript read fails with "DataFees: FeeEscrow failure - not enough data".

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 configured minimum raw input for swapping `tokenSymbol`: max(minScaledSwapUnits / scaleFactor, absoluteMinRaw). Those globals are unset on the live contract, so it returns 0. The effective floor comes from the admin-fee check instead: amountIn × 9 / 10,000 must be at least 1 (1,112 raw units; 5,556 with the Series-5 FACTORY discount). For a token whose scale factor is not cached yet (getScaleFactor returns 0: BNB, RAA, GAS and ETH on mainnet) the view writes the cache first, so a free invokeRawScript read fails with "DataFees: FeeEscrow failure - not enough data".

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: max(minScaledAddLiqUnits / scaleFactor, absoluteMinRaw). Those globals are unset on the live contract, so it returns 0; the checks that apply are that the required second-token amount ("Rounds to zero") and the minted LP ("Insufficient liquidity minted") are above 0. For a token whose scale factor is not cached yet (BNB, RAA, GAS and ETH on mainnet) a free invokeRawScript read fails with "DataFees: FeeEscrow failure - not enough data", because the view writes the cache first.

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. Mainnet and devnet report "3.9.9.2" (SATRN has no getContractVersion). 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", []);

getOwner()

READ
getOwner(): address

Returns SATRN's owner (_owner): the admin that receives the 0.09% admin fee on every swap and the only address that can call the Admin & Internal methods. Mainnet returns P2KBPHBKq1xuoSajuKxQCd7RfCfGFyoczoHQdVxacEUc9As.

Returns
address — The SATRN admin address.
What to expect
No reverts. Changes only through updateAdmin.
Example
const admin = await readContract("SATRN", "getOwner", []);

getName()

READ
getName(): string

Token property: the SATRN token's name. Returns "Saturn DEX - LP TRACKER".

Returns
string — "Saturn DEX - LP TRACKER".
What to expect
Constant. No reverts.
Example
const name = await readContract("SATRN", "getName", []);

getSymbol()

READ
getSymbol(): string

Token property: the token symbol, "SATRN". The whole v3 DEX is this one token contract.

Returns
string — "SATRN".
What to expect
Constant. No reverts.
Example
const symbol = await readContract("SATRN", "getSymbol", []);

isTransferable()

READ
isTransferable(): bool

Token property: true. SATRN LP NFTs can be sent between wallets. The onSend trigger requires the sender's signature ("witness failed" otherwise), so a contract can never send one out.

Returns
bool — true.
What to expect
Constant. No reverts.
Example
const transferable = await readContract("SATRN", "isTransferable", []);

isFungible()

READ
isFungible(): bool

Token property: false. Every SATRN token is a non-fungible LP position with its own ID (a 256-bit integer).

Returns
bool — false.
What to expect
Constant. No reverts.
Example
const fungible = await readContract("SATRN", "isFungible", []);

isBurnable()

READ
isBurnable(): bool

Token property: true. removeLiquidity burns the position's NFT and consolidatePositions burns the merged ones (the onBurn trigger requires the owner's signature).

Returns
bool — true.
What to expect
Constant. No reverts.
Example
const burnable = await readContract("SATRN", "isBurnable", []);

getMaxSupply()

READ
getMaxSupply(): number

Token property: 0, meaning no token-wide supply cap. Each LP position is minted in its own NFT series with a max supply of 1. For the number of live positions read the token's currentSupply (RPC getToken("SATRN")); getTotalNftMinted has drifted from it.

Returns
number — 0 (uncapped).
What to expect
Constant. No reverts.
Example
const maxSupply = await readContract("SATRN", "getMaxSupply", []);

Disabled Methods (Disabled since v3.9.1)

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 has been disabled since v3.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 has been disabled since v3.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.

Admin & Internal

updateAdmin()

WRITE
updateAdmin(newAdmin: address)

Admin only. The SATRN _owner (getOwner) can call this. Replaces the owner with `newAdmin`; from then on the 0.09% admin fee on swaps goes to the new owner and only it can call the admin methods.

Parameters
NameTypeDescription
newAdminaddressNew owner address; must not be null.
What to expect
Reverts with "Only admin" unless the current _owner signs, or "Invalid admin" for a null address.

setChainTokenFeeWallet()

WRITE
setChainTokenFeeWallet(newWallet: address)

Admin only. The SATRN _owner (getOwner) can call this. Sets the wallet that receives the 0.1% token-owner slice on swaps whose output is SOUL or KCAL (chain tokens with no token owner). Stored in a map; read it with getChainTokenFeeWallet.

Parameters
NameTypeDescription
newWalletaddressFee wallet; must not be null.
What to expect
Reverts with "Only admin" or "Invalid wallet" (null address).

updateMinScaledAmounts()

WRITE
updateMinScaledAmounts(newPoolMin: number, newSwapMin: number, newAddLiqMin: number)

Admin only. The SATRN _owner (getOwner) can call this. Sets the minimums that getMinRawForPoolCreation, getMinRawForSwap and getMinRawForAddLiquidity divide by each token's scale factor. All three are unset (0) on the live contract. Because every scale factor is 1 live, the values would act as raw units for every token: the smallest allowed swap minimum, 112,000,000,000, would mean 1,120 SOUL or 11.2 KCAL per swap.

Parameters
NameTypeDescription
newPoolMinnumberScaled minimum per side for initializePool; at least 1,000,000.
newSwapMinnumberScaled minimum swap input; at least 112,000,000,000.
newAddLiqMinnumberScaled minimum first-token amount for addLiquidity_v2; at least 100,000.
What to expect
Reverts with "Only admin", "Pool min too low" (< 1,000,000), "Swap min too low - must guarantee admin fee >= 1" (< 112,000,000,000) or "AddLiq min too low" (< 100,000).

updateMaxTruncationPercent()

WRITE
updateMaxTruncationPercent(newPercent: number)

Admin only. The SATRN _owner (getOwner) can call this. Sets the scale-down rounding loss (percent) that removeLiquidity tolerates. It reads 0 live and has no effect while every scale factor is 1.

Parameters
NameTypeDescription
newPercentnumberWhole percent, 1 to 50.
What to expect
Reverts with "Only admin" or "Must be 1-50".

clearReentrancy()

WRITE
clearReentrancy(user: address)

Admin only. The SATRN _owner (getOwner) can call this. Resets the reentrancy guard for `user` to 0. Use it when getReentrancyGuardState(user) is stuck at 1 and every write from that address reverts with "Reentrancy detected".

Parameters
NameTypeDescription
useraddressAddress whose guard to clear.
What to expect
Reverts with "Only admin".

skim()

WRITE
skim(to: address, tokentoskim: string)

Admin only. The SATRN _owner (getOwner) can call this. Sends the surplus of `tokentoskim` (SATRN's balance minus the unscaled global reserve) to `to`; does nothing when there is no surplus. The surplus includes the historic SOUL storage fees (62,500,000 raw = 0.625 SOUL on mainnet) and any tokens sent to SATRN directly.

Parameters
NameTypeDescription
toaddressRecipient of the surplus.
tokentoskimstringToken symbol to skim.
What to expect
Reverts with "Only admin". Reserves are never touched: only balance − scaleDown(getReserveValue(token)) moves.

increaseStorage()

WRITE
increaseStorage(from: address, stakeAmount: number, soultoken: string)

Admin only. The SATRN _owner (getOwner) must sign. Transfers `stakeAmount` raw SOUL from `from` into SATRN. Since 3.9.8.12 nothing is staked (Gen3 storage is paid from each transaction's data escrow), so the SOUL just becomes skimmable surplus.

Parameters
NameTypeDescription
fromaddressPayer of the SOUL; must also sign the transfer.
stakeAmountnumberRaw SOUL amount (8 decimals).
soultokenstringMust be "SOUL".
What to expect
Reverts with "Only deployer" unless the _owner signs, or "Only SOUL".