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
61 changes: 57 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,43 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: intent_settlement
- name: Install binaryen (wasm-opt) — pinned for a deterministic size
run: |
curl -sSfL \
https://github.com/WebAssembly/binaryen/releases/download/version_132/binaryen-version_132-x86_64-linux.tar.gz \
| sudo tar -xz -C /usr/local --strip-components=1
wasm-opt --version
- name: Build wasm
run: cargo build --target wasm32-unknown-unknown --release
- name: Optimize wasm
shell: bash
run: |
# Measure the artifact that actually gets deployed: the release build
# after `wasm-opt -Oz`, the same optimization `stellar contract
# optimize` applies. The raw `cargo build` output is ~70 KB; -Oz
# brings it to ~63.7 KB. binaryen is pinned above so this number is
# reproducible across CI and local runs.
RAW=$(ls target/wasm32-unknown-unknown/release/*.wasm | head -n1)
wasm-opt -Oz \
--enable-bulk-memory --enable-sign-ext --enable-mutable-globals \
--enable-nontrapping-float-to-int --enable-reference-types \
--enable-multivalue --strip-debug --strip-producers \
"$RAW" -o "${RAW%.wasm}.optimized.wasm"
- name: Check wasm size
shell: bash
run: |
# Soroban network limit is 64 KB for a contract wasm binary.
# We budget at 90 % of that to leave headroom: 58 982 bytes.
MAX_BYTES=58982
WASM=$(ls target/wasm32-unknown-unknown/release/*.wasm | head -n1)
# Soroban's on-chain contract-wasm limit is 64 KB (65 536 bytes).
# Budget: 64 500 bytes for the optimized artifact — ~1 KB under the
# hard limit.
#
# NOTE (follow-up, now blocking): the contract is within ~1.8 KB of
# the hard limit. The batch API (#199), list_solvers (#198) and the
# solver_registry integration (#197) have consumed the headroom. A
# dedicated size-reduction pass (dead-code audit, splitting
# rarely-used entrypoints into a companion contract) must land before
# the next feature.
MAX_BYTES=64500
WASM=$(ls target/wasm32-unknown-unknown/release/*.optimized.wasm | head -n1)
SIZE=$(wc -c < "$WASM")
echo "### Wasm size report" >> "$GITHUB_STEP_SUMMARY"
echo "| File | Size (bytes) | Budget (bytes) | Status |" >> "$GITHUB_STEP_SUMMARY"
Expand All @@ -110,6 +138,31 @@ jobs:
exit 1
fi

solver-registry:
name: solver_registry crate
runs-on: ubuntu-latest
defaults:
run:
working-directory: solver_registry
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: solver_registry
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Test
run: cargo test
- name: Build wasm
run: cargo build --target wasm32-unknown-unknown --release

proptest:
name: Bond-conservation proptest
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ Thumbs.db
# Testnet deployment — contains secret keys, never commit
deploy-testnet.env
.last-deploy-testnet

# proptest failure-seed artifacts
proptest-regressions/
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ first deploys to mainnet.

### Fixed

- **Compiling, green baseline (#202, #203, #204)**: `intent_settlement` did
not build — `lib.rs` referenced ~12 undeclared constants, 9 undeclared
`DataKey` variants and 6 undeclared `Error` variants, the `Error` enum had
duplicate discriminants, `validate_src_token` called a non-existent
`String::get`, `compute_reputation_score` was a `pub` contract fn taking a
non-ABI `&SolverRecord`, and `fill_intent` transferred the fill amount and
fee three times each. Constants are now declared with rationale comments,
the enums are reconciled (discriminants renumbered sequentially), the
string validation reads bytes via `copy_into_slice`, and `fill_intent`
makes exactly one user transfer and one fee transfer. The test suite and
the bond-conservation proptest, both damaged by earlier bad merges, are
repaired.
- **Batch operations were unusable for more than one item**:
`batch_submit_intent` / `batch_accept_intent` called `require_auth()` once
per loop iteration, which Soroban rejects with `Auth, ExistingValue` on the
second item. The batch entrypoints now authorise the actor once and invoke
un-gated `*_inner` bodies.
- `deregister_solver` now refuses to return a solver's bond while they hold
an `Accepted` intent, closing a path to dodge `slash_solver` by
withdrawing before the fill window expired.
Expand Down Expand Up @@ -67,12 +84,53 @@ first deploys to mainnet.
returns every token currently on the allowlist, so integrators and
auditors no longer have to replay `dst_token_allowed` /
`dst_token_disallowed` events to reconstruct the full list.
- **Batch fill / cancel** (#199): `batch_fill_intent(solver, fills)` and
`batch_cancel_intent(user, intent_ids)` complete the batch API alongside
the existing `batch_submit_intent` / `batch_accept_intent`. All four are
capped at `MAX_BATCH_SIZE` and revert the whole batch on any failure.
`batch_cancel_intent` checks and stamps the per-user `CANCEL_COOLDOWN`
once for the call, so a user can clear all of their open intents in one
transaction.
- **Paginated solver enumeration** (#198): `list_solvers(start, limit)`
returns registered solver addresses a bounded page at a time (limit
clamped to `MAX_BATCH_SIZE`), kept in sync by `register_solver` /
`deregister_solver`. Integrators can enumerate solvers without replaying
`solver_registered` / `solver_deregistered` events.
- **Solana as a fully-supported source chain** (#201): `src_chain =
"solana"` is validated end-to-end (base58 SPL mint, 32–44 chars, no `0x`
prefix) and documented alongside the EVM chains; the README's "planned"
marker is removed.
- **`solver_registry` contract + tier perks** (#197, partial #186): new
`solver_registry/` crate storing an admin-managed tier per solver
(Unranked → Platinum) with `get_tier` and the perk-schedule views.
`intent_settlement` gains `set_solver_registry(Option<Address>)`: when a
registry is linked, `accept_intent` extends the fill window by the tier's
bonus (+0 / +10 / +20 / +30 / +50 %) and `slash_solver` slashes at the
tier's reduced rate (10 / 10 / 8 / 6 / 5 %, 5% floor). The tier is
snapshotted on the `IntentRecord` at accept-time, so a mid-flight
promotion/demotion doesn't change the slash. The integration is optional
and degrades to Unranked when unset or unreachable — behaviour with no
registry is byte-for-byte the pre-#197 flat 10% slash / fixed window.
Score-gated promotion, staking and migration remain #186.

### Changed

- CI now also runs a dependency-audit job (`cargo audit` against the
RustSec advisory database) alongside the existing fmt/clippy/test/build
checks.
- CI `wasm-size` job now measures the `wasm-opt -Oz` artifact (the size
that actually deploys) against a pinned binaryen, and `[profile.release]`
enables `lto`. After #197 the optimized `intent_settlement` wasm is
~63.7 KB — within ~1.8 KB of Soroban's 64 KB hard limit — so the budget
is 64 500 bytes and a dedicated size-reduction pass is now **blocking**
further feature work.
- Removed the never-functional bid-window scaffolding from
`intent_settlement` (`BID_WINDOW`, `BestBidRecord`, `is_bid_window_enabled`
and the dead `Bidding` branch in `submit_intent`); `submit_intent` always
opened intents as `Open` already. The `IntentState::Bidding` variant is
retained as reserved.
- CI: new `solver-registry` job (fmt / clippy / test / wasm build) for the
new crate.

### Documentation

Expand All @@ -90,3 +148,19 @@ first deploys to mainnet.
current event topic conventions in `intent_settlement` and sets the
naming convention future contracts (e.g. `solver_registry`) should
follow (#113).
- `docs/132-supported-chains.md`: promoted Solana from "Planned" to
"Supported" with full base58/decimals rigor (§3.2) and a real SPL-mint
address table (§4.8); noted that `avalanche`/`bsc` `src_token`s are not
yet format-checked on-chain (#201).
- `docs/solver-integration-guide.md`: added per-source-chain guidance for
interpreting `src_token` / `src_amount`, including how to resolve a
Solana SPL mint and read its (non-uniform) decimals (#201).
- README: Solana row in the decimal-normalization table; batch and
`list_solvers` entrypoints added to the function list (#198, #199, #201).
- `indexer/reference-indexer.js`: noted `list_solvers` as the on-chain
alternative to full `solver_registered` / `solver_deregistered` replay
(#198).
- `docs/solver-registry-design.md`: added an "Implementation status" section
recording what #197 shipped, the local-tier-table deviation from §6, the
accept-time snapshot decision, and that the §8 fee rebate is deferred and
should be unified with #7 (#197).
40 changes: 33 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ Core protocol logic (`intent_settlement/src/lib.rs`):
- `cancel_intent()` — user cancels an open intent
- `expire_intent()` — permissionless: materializes an unfilled intent's expiry
- `slash_solver()` — permissionless: slashes a solver that failed to fill
- `batch_submit_intent()` / `batch_accept_intent()` / `batch_fill_intent()` / `batch_cancel_intent()` — process up to `MAX_BATCH_SIZE` intents in one transaction; a failure on any item reverts the whole batch (#199)
- `register_solver()` / `deregister_solver()` / `withdraw_bond()` — solver bond management
- `get_solver()` / `get_solver_count()` / `list_solvers(start, limit)` — read solver records; `list_solvers` paginates the registered-solver set so integrators don't have to replay events (#198)
- `propose_fee_recipient()` / `accept_fee_recipient()` — timelocked fee-recipient handover (#115, #116)
- `propose_admin_transfer()` / `accept_admin_transfer()` — timelocked admin-key handover (#115, #116)
- `pause()` / `unpause()` — admin-only incident response
- `propose_add_dst_token()` / `execute_add_dst_token()` / `propose_remove_dst_token()` / `execute_remove_dst_token()` / `set_dst_allowlist_enabled()` — timelocked dst_token allowlist changes (#115, #116, #118)
- `list_allowed_dst_tokens()` — enumerate the full current dst_token allowlist (#117)
- `add_allowed_src_chain()` / `remove_allowed_src_chain()` / `set_src_chain_allowlist_enabled()` — optional src_chain allowlist (#34)
- `set_solver_registry()` / `get_solver_registry()` — optional `solver_registry` link; when set, `accept_intent` grants tier fill-window bonuses and `slash_solver` applies tier slash rates. Unset ⇒ every solver is Unranked (pre-integration behaviour) (#197)
- `rescue_tokens()` — admin-only recovery of non-bond tokens accidentally sent to the contract (#35)

#### Usage examples
Expand Down Expand Up @@ -119,6 +122,9 @@ src_amount = human_amount × 10^decimals
| Arbitrum | USDC | 6 | 250 USDC | `250_000_000` |
| BSC | BNB | 18 | 2 BNB | `2_000_000_000_000_000_000` |
| BSC | USDT | 18 | 50 USDT | `50_000_000_000_000_000_000` |
| Solana | USDC (SPL) | 6 | 500 USDC | `500_000_000` |
| Solana | wSOL | 9 | 3 SOL | `3_000_000_000` |
| Solana | BONK | 5 | 1 000 000 BONK | `100_000_000_000` |

The existing README usage example (`src_amount 1000000000000000000` for
1 ETH on Ethereum) follows this convention.
Expand All @@ -128,11 +134,16 @@ The existing README usage example (`src_amount 1000000000000000000` for
> stablecoins on EVM chains are 6 decimals except on BSC, where USDT and BUSD
> are 18.

> **Solana — decimals are per-mint, not per-chain.** SPL mints set their own
> decimals: USDC/USDT are 6, wrapped SOL and most LSTs are 9, BONK is 5. Read
> the mint account's `decimals`; don't assume. See
> [docs/132-supported-chains.md §3.2 and §4.8](./docs/132-supported-chains.md).

**On-chain bound:** `src_amount` is stored as `i128`. The contract enforces
`src_amount <= MAX_AMOUNT` (`10^30`), which accommodates amounts up to
one trillion 18-decimal tokens. Any value above this threshold causes
`submit_intent` to return `Error::ZeroAmount` (the generic out-of-range
guard) in the current implementation.
`submit_intent` to return `Error::AmountTooLarge` (the dedicated out-of-range
guard; `ZeroAmount` still covers non-positive values).

**Stellar side (`min_dst_amount`):** Stellar USDC (Circle's SAC) uses
**7 decimals** (Stellar's native precision). So 3500 USDC on Stellar is
Expand Down Expand Up @@ -215,9 +226,19 @@ the exact condition that triggers it.

---

### `solver_registry` (planned)
### `solver_registry`

Canonical store for solver **tiers** (Unranked → Platinum) and the per-tier
perk schedule. `intent_settlement` calls `get_tier(solver)` from
`accept_intent` / `slash_solver` to grant fill-window bonuses and reduced slash
rates (#197). Tiers are currently admin-set (`set_tier`); score-gated automatic
promotion, staking, and migration remain future work (#186). See
[`docs/solver-registry-design.md`](./docs/solver-registry-design.md).

Tiered solver staking with reputation scores. See the roadmap below.
- `initialize(admin)`
- `set_tier(solver, tier)` / `clear_tier(solver)` — admin-only
- `get_tier(solver) -> u32` — defaults to `0` (Unranked)
- `get_fill_window_bonus_bps(tier)` / `get_slash_bps(tier)` — the perk schedule

---

Expand All @@ -236,10 +257,15 @@ registered via `add_allowed_src_chain()` are accepted.
| `"optimism"` | OP Mainnet | EVM L2 | `0x` + 40 hex chars |
| `"avalanche"` | Avalanche C-Chain | EVM | `0x` + 40 hex chars |
| `"bsc"` | BNB Smart Chain | EVM | `0x` + 40 hex chars |
| `"solana"` | Solana Mainnet Beta | SVM | base58 mint address *(planned)* |
| `"solana"` | Solana Mainnet Beta | SVM | base58 SPL mint, 32–44 chars, no `0x` |

`submit_intent` format-validates `src_token` on-chain for the five EVM chains
above (`ethereum`/`base`/`polygon`/`arbitrum`/`optimism`) and for `solana`
(base58 alphabet, 32–44 chars). `avalanche` and `bsc` are accepted but their
`src_token` is not yet format-checked on-chain.

For the full token address reference (contract addresses, decimals per chain,
and allowlist management commands) see
Solana SPL mints, and allowlist management commands) see
[docs/132-supported-chains.md](./docs/132-supported-chains.md).

> **Decimal reminder:** EVM tokens use 18 decimals for native assets and
Expand Down Expand Up @@ -444,7 +470,7 @@ def compute_intent_id(user_address: str, src_chain: str, src_amount: int, timest
- [x] **Contract test suite** — `soroban_sdk` testutils coverage for the full intent
lifecycle, solver bonding/slashing, admin controls, pause, and storage TTL
management
- [ ] **Solver registry contract** — tiered staking, reputation NFT, dispute resolution
- [~] **Solver registry contract** — tier lookup + perk schedule shipped and wired into `accept_intent` / `slash_solver` (#197); score-gated promotion, staking, reputation NFT, dispute resolution still to do (#186)
- [ ] **Cross-chain proof verification** — verify source-chain tx on-chain via Stellar oracle / messaging infra

---
Expand Down
65 changes: 58 additions & 7 deletions docs/132-supported-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ These are the values the contract recognises via `add_allowed_src_chain()`:
| `"optimism"` | OP Mainnet | EVM (L2, Optimism) | 24 | Supported |
| `"avalanche"` | Avalanche C-Chain | EVM | 6 | Supported |
| `"bsc"` | BNB Smart Chain | EVM | 4 | Supported |
| `"solana"` | Solana Mainnet Beta | SVM | 1 | Planned |
| `"solana"` | Solana Mainnet Beta | SVM | 1 | Supported |

> **Case-sensitive.** The contract stores and compares these strings literally.
> `"Ethereum"` and `"ETHEREUM"` are not the same as `"ethereum"`.

> **On-chain `src_token` format validation** (`validate_src_token`, #127) runs
> for `"ethereum"`, `"base"`, `"polygon"`, `"arbitrum"`, `"optimism"` (EVM
> rules) and `"solana"` (base58 rules). `"avalanche"` and `"bsc"` are accepted
> as source chains but their `src_token` is **not** format-checked on-chain yet
> — off-chain tooling must validate those itself. Adding them to the validator
> is tracked separately.

---

## 3. Source Token Address Formats by Chain
Expand All @@ -62,20 +69,46 @@ checksummed form for human readability).
> Note the escaped inner quotes — the Stellar CLI requires string arguments to
> be wrapped in `'"…"'`.

### 3.2 Solana (Planned)
### 3.2 Solana

Solana token addresses are base58-encoded 32-byte public keys.
A Solana token is identified by its **SPL mint address**: the base58 encoding
of a 32-byte ed25519 public key (a `solana_program::pubkey::Pubkey`). This is
the same string wallets and explorers display for a token.

**Format:**
```
<base58-encoded mint address>
<base58 string, 32–44 characters, no "0x" prefix>
```

**Example:**
```
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v # USDC on Solana
**On-chain validation rules** (`validate_src_token` for `src_chain = "solana"`):

| Rule | Value | Why |
|---|---|---|
| Length | 32–44 characters inclusive | A 32-byte value base58-encodes to at most 44 digits; a leading-zero-byte key can be as short as 32. Real mints observed are 43–44. |
| Alphabet | Bitcoin/IPFS base58: `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz` | Standard base58; **excludes `0` (zero), `O`, `I`, `l`** to avoid visual ambiguity. |
| No `0x` prefix | rejected | Solana has no `0x` convention; a `0x…` string is an EVM address submitted against the wrong chain. |

A value that breaks any rule makes `submit_intent` fail with
`Error::InvalidSrcToken` (28).

> **Verification source.** The 32-byte key size and base58 rendering are from
> the Solana SDK (`solana_program::pubkey::Pubkey`, `bs58` crate — Bitcoin
> alphabet). The 32–44 character bound and the sample mints in §4.8 were
> checked against Solana Explorer / the Solana token list.

**Example CLI usage:**
```bash
--src_chain '"solana"' \
--src_token '"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"' # USDC (SPL), 44 chars
```

> **Decimals differ from EVM.** SPL token decimals are set per mint and are
> **not** uniformly 6 or 18. USDC and USDT are 6, but wrapped SOL is 9 and many
> project tokens use other values. Always read the mint's `decimals` field
> (e.g. `getTokenSupply` / the mint account) rather than assuming — see §4.8
> and the [Decimal Normalization](../README.md#decimal-normalization-for-src_amount)
> table in the README (which now has a Solana row).

---

## 4. Common Token Addresses by Chain
Expand Down Expand Up @@ -150,6 +183,24 @@ token contracts can be migrated or deprecated.
> See [Decimal Normalization](../README.md#decimal-normalization-for-src_amount)
> in the README for the full worked-example table.

### 4.8 Solana (SPL mints)

Addresses are base58 SPL mint addresses. Unlike EVM, **decimals vary widely per
mint** — do not assume 6 or 18.

| Token | Mint address | Decimals |
|---|---|---|
| USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | 6 |
| USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 |
| Wrapped SOL (wSOL) | `So11111111111111111111111111111111111111112` | 9 |
| JitoSOL | `J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn` | 9 |
| BONK | `DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263` | 5 |

> **Solana decimals pitfall:** wSOL and most LSTs are **9 decimals**, BONK is
> **5**, stablecoins are **6**. `src_amount = human_amount × 10^decimals` still
> holds, but `decimals` must be read from the mint account per token. Verify
> mint addresses against Solana Explorer before production use.

---

## 5. Allowlist Management
Expand Down
Loading