Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ jobs:
# (`cargo install stellar-cli` โ€” verified against the actual crate
# name behind the `stellar` binary, not a guessed GitHub Action).
- name: Install stellar CLI
run: cargo install --locked stellar-cli --version ${{ vars.STELLAR_CLI_VERSION || '27.1.0' }}
run: |
curl -fsSL https://github.com/stellar/stellar-cli/releases/download/v${{ vars.STELLAR_CLI_VERSION || '27.1.0' }}/stellar-cli-${{ vars.STELLAR_CLI_VERSION || '27.1.0' }}-x86_64-unknown-linux-gnu.tar.gz -o /tmp/stellar.tar.gz
mkdir -p "$RUNNER_TEMP/stellar-bin"
tar -xzf /tmp/stellar.tar.gz -C "$RUNNER_TEMP/stellar-bin" stellar
echo "$RUNNER_TEMP/stellar-bin" >> "$GITHUB_PATH"

- name: cargo fmt --check
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/kani.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ jobs:
- name: Run Kani
uses: model-checking/kani-github-action@v1
with:
working-directory: oracledesk-stellar
args: -p market-core
5 changes: 4 additions & 1 deletion oracledesk-stellar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ This is the Stellar/Soroban native port of the system originally built for
the Agora Hackathon on Arc + Polygon (Solidity, Polymarket execution, Circle
CCTP/Wallets). None of that Arc-era execution stack is part of this repo โ€”
markets, trading, resolution, the reasoning registry, and the agent treasury
are all native Soroban contracts, collateralized in a SEP-41 USDC token.
are all native Soroban contracts, collateralized in a SEP-41 token. market-core
reads the collateral's `decimals()` at deploy and derives its economic bounds
(`min_seed = 100 * 10^d`, `max_sets = 1e8 * 10^d`) for any `d` in `0..=18`, so
7-decimal Classic USDC and 18-decimal Soroban-native tokens both work.
Polymarket survives only as an optional, execution-free reference price an
agent's strategy may read (see `agents/core/strategy.ts`).

Expand Down
85 changes: 71 additions & 14 deletions oracledesk-stellar/agents/core/fpmm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,81 @@
* plus a property check that `collateralOutForShares` (the off-chain inverse
* for "sell N shares") round-trips through `calcSell`.
*
* All amounts are non-negative bigints in collateral base units (USDC on
* Stellar = 7 decimals). Rounding always favours the pool, matching the
* contract.
* All amounts are non-negative bigints in collateral base units. The
* contract is now decimals-aware: it reads the collateral token's
* `decimals()` at construction and derives its economic bounds from it.
* This mirror exposes `boundsForDecimals` and checks that multiply-divide
* results fit in i128, matching the contract's 256-bit intermediates.
* Rounding always favours the pool, matching the contract.
*/

export const BPS = 10_000n;
export const MAX_SETS = 1_000_000_000_000_000_000n; // 1e15 = 100M USDC (7 dp)

/**
* Maximum number of complete sets for a 7-decimal collateral.
*
* @deprecated Use `boundsForDecimals(7)` instead. This constant is
* kept only as a 7-decimal alias for tests and for backward compatibility;
* contract logic derives its bounds from the collateral decimals.
*/
export const MAX_SETS = 1_000_000_000_000_000n; // 1e15 = 100M USDC (7 dp)

export const MIN_PROB_BPS = 100n; // 1%
export const MAX_PROB_BPS = 9_900n; // 99%

/** Maximum supported collateral decimals. */
export const MAX_DECIMALS = 18;

/** Maximum value of a signed 128-bit integer. */
export const I128_MAX = (1n << 127n) - 1n;

export class MathError extends Error {}

/**
* Economic bounds derived from collateral decimals. Mirrors
* math.rs::bounds_for_decimals: `min_seed = 100 * 10^d`,
* `max_sets = 1e8 * 10^d`.
*/
export interface Bounds {
minSeed: bigint;
maxSets: bigint;
}

/**
* Derive the minimum seed and maximum sets for a collateral with `decimals`
* decimals. Throws if `decimals` is outside the supported range or the
* resulting bounds do not fit in i128.
*/
export function boundsForDecimals(decimals: number): Bounds {
if (!Number.isInteger(decimals) || decimals < 0 || decimals > MAX_DECIMALS) {
throw new MathError("unsupported decimals");
}
const scale = 10n ** BigInt(decimals);
const minSeed = 100n * scale;
const maxSets = 100_000_000n * scale;
if (minSeed > I128_MAX || maxSets > I128_MAX) {
throw new MathError("overflow");
}
return { minSeed, maxSets };
}

function checkI128(value: bigint): bigint {
if (value > I128_MAX) throw new MathError("overflow");
return value;
}

export function mulDivFloor(a: bigint, b: bigint, c: bigint): bigint {
if (a < 0n || b < 0n || c <= 0n) throw new MathError("invalid input");
return (a * b) / c;
return checkI128((a * b) / c);
}

export function mulDivCeil(a: bigint, b: bigint, c: bigint): bigint {
if (a < 0n || b < 0n || c <= 0n) throw new MathError("invalid input");
const p = a * b;
const q = p / c;
return p % c !== 0n ? q + 1n : q;
const r = p % c;
const result = r !== 0n ? q + 1n : q;
return checkI128(result);
}

export function priceYesBps(reserveYes: bigint, reserveNo: bigint): bigint {
Expand Down Expand Up @@ -93,7 +146,7 @@ export interface SellQuote {
newReserveOther: bigint;
}

export function calcSell(
export function calcSeld(
rSold: bigint,
rOther: bigint,
collateralOut: bigint,
Expand All @@ -117,7 +170,9 @@ export type Settlement = "yes" | "no" | "void";
export function settleValue(kind: Settlement, yes: bigint, no: bigint): bigint {
if (kind === "yes") return yes;
if (kind === "no") return no;
return (yes + no) / 2n;
const sum = yes + no;
if (sum > I128_MAX) throw new MathError("overflow");
return sum / 2n | 0n;
}

export function mergeMax(yes: bigint, no: bigint): bigint {
Expand Down Expand Up @@ -314,13 +369,15 @@ export function priceAfterSell(takenIsYes: boolean, quote: SellQuote): bigint {

/**
* True when a buy's average execution price `collateralIn / sharesOut`
* (fees included) is at most `limitBps` per share. Compared by
* cross-multiplication so rounding cannot flip the decision; exact
* equality passes. Mirrors math.rs::buyWithinLimit / the on-chain
* `buy_v2` imit check.
* (fees included) is at most `limitBps` per share. Compares the ratio
* without floating point: `collateralIn * BPS <= sharesOut * limitBps`.
*/
export function buyWithinLimit(collateralIn: bigint, sharesOut: bigint, limitBps: bigint): boolean {
if (collateralIn < 0n || sharesOut <= 0n) throw new MathError("invalid input");
export function isPriceWithinSlippage(
collateralIn: bigint,
sharesOut: bigint,
limitBps: bigint,
): boolean {
if (sharesOut <= 0n) throw new MathError("invalid input");
return collateralIn * BPS <= sharesOut * limitBps;
}

Expand Down
Loading
Loading