Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
d70248f
Initial sanity checks
ericeil Nov 5, 2025
213e710
WIP
ericeil Nov 7, 2025
3ec9bd5
get_sui_amount equlivalence
ericeil Nov 11, 2025
d6bc353
WIP
ericeil Nov 24, 2025
eddfdf4
WIP
ericeil Nov 24, 2025
8114691
fix: duplicate summary for system state
kel-certora Jan 9, 2026
5ccf65a
Create dummy implementation for parametric rules
kel-certora Jan 9, 2026
3a78f53
wip: solvency
kel-certora Jan 9, 2026
eed1d54
Disable system summaries
kel-certora Jan 9, 2026
7bcba80
Solvency: Assume only one validator
kel-certora Jan 9, 2026
c6b9beb
Summarize exchange rate
kel-certora Jan 9, 2026
d2ce162
Update patches
kel-certora Jan 9, 2026
ebfca3d
Verify correct accounting of total sui supply
kel-certora Jan 9, 2026
20c69bc
Remove invalid rule declaration
kel-certora Jan 12, 2026
4157643
Fix solvency rules
kel-certora Jan 12, 2026
40fc6ed
Add basic integrity rules
kel-certora Jan 12, 2026
c9b8ef5
Add integrity rules
kel-certora Jan 12, 2026
f97717b
Add rules for validator info consistency
kel-certora Jan 13, 2026
cd1c3f5
Initial CI setup
kel-certora Jan 13, 2026
d76386c
Restructure rules
kel-certora Jan 13, 2026
ab0b36b
Remove invalid summary
kel-certora Jan 13, 2026
37c29e6
Fix no sui iff no lst rule
kel-certora Jan 13, 2026
bf6b8c2
Fix spurious cex in solvency rules
kel-certora Jan 13, 2026
d075725
Value conservation rules
kel-certora Jan 13, 2026
4296281
ci: Update conf files
kel-certora Jan 13, 2026
9e44d51
ci: print sui version
kel-certora Jan 13, 2026
a7b49e3
ci: Fix sui version 1.53.2
kel-certora Jan 13, 2026
73b8cd9
Fix timeouts
kel-certora Jan 13, 2026
7b4bb9b
Add accounting integrity rules
kel-certora Jan 13, 2026
d570a4f
Cleanup
kel-certora Jan 13, 2026
7aded07
Rules for fees
kel-certora Jan 14, 2026
830bc61
Fix timeout in arbitrage opportunity rule
kel-certora Jan 14, 2026
27c6efa
wip: add liveness rule for redemptions
kel-certora Jan 14, 2026
688fbad
chore: rename
kel-certora Jan 14, 2026
bfa068a
Disable nondet summary of vector::contains
kel-certora Jan 14, 2026
8617f4e
Simply fresh storage setup
kel-certora Jan 26, 2026
38ee23e
chore: munge visibility
kel-certora Jan 26, 2026
882b467
fix: timeouts in supply increase/decrease rules
kel-certora Jan 26, 2026
64bc4fe
Verify soundness of refresh summary
kel-certora Jan 26, 2026
19cb188
nondet summary for version upgrade
kel-certora Jan 26, 2026
a5a4f7e
Add rule for spread fee rules
kel-certora Jan 26, 2026
a1e0c75
fix[ci]: renamed confs
kel-certora Jan 26, 2026
eedd668
fix: bad module name
kel-certora Jan 26, 2026
8519f37
fix: increase smt timeouts
kel-certora Jan 26, 2026
26efc2d
fix: validator consistency soundness and missing rule
kel-certora Jan 27, 2026
196117f
chore: add comments
kel-certora Jan 27, 2026
5540879
chore: polishing
kel-certora Feb 3, 2026
6b72b4f
fix: invalid configurations
kel-certora Feb 3, 2026
cae0fda
chore: Split CI steps to not hit resource limits
kel-certora Feb 4, 2026
19a2cd2
chore: Update CI job names
kel-certora Feb 4, 2026
be9b12f
fix: Sui supply reconciles only in next epoch
kel-certora Feb 4, 2026
8cb8e58
fix: validator consistency invariant only holds across epoch boundary
kel-certora Feb 5, 2026
c9ff8a1
chore: disable vacuity checks to mitigate timeout
kel-certora Feb 6, 2026
f4652aa
chore: more accurate rule messages
kel-certora Feb 6, 2026
0011a54
Move CI test to Sui v1.62.1 and update certora dependencies to match
ericeil Feb 6, 2026
47dd8a2
chore: update comments
kel-certora Feb 9, 2026
76aa33d
fix: Adjust patches
kel-certora Mar 3, 2026
e0d0c38
chore: add package_summaries to gitignore
kel-certora Mar 3, 2026
0fd94f2
chore: force Sui dependencies to match the versions in cvlm/certora_s…
kel-certora Mar 3, 2026
f1d73df
chore[ci]: bump sui version
kel-certora Mar 3, 2026
a27ce36
fix: method matching in confs
kel-certora Mar 3, 2026
79b8c88
chore[ci]: trigger on PR against main
kel-certora Mar 3, 2026
6793ecd
chore:
kel-certora Mar 3, 2026
f71b089
chore: add readme
kel-certora Mar 3, 2026
21fc74f
Bump to sui 1.65.2
kel-certora Mar 4, 2026
0f6a4ca
Pin cli version in CI
kel-certora Mar 4, 2026
d938d0f
fix[ci]: missing conf
kel-certora Mar 4, 2026
e4a1113
Workflow edit
ronanyeah Mar 5, 2026
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
67 changes: 67 additions & 0 deletions .github/workflows/certora.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Certora Prover

on:
push:
branches: [cvlm]

jobs:
certora_run:
runs-on: ubuntu-latest
permissions:
contents: read
statuses: write
pull-requests: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install sui
run: |
curl -sSfL https://raw.githubusercontent.com/Mystenlabs/suiup/main/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
suiup install -y [email protected]
- name: Sui version
run: |
sui -V
sui move -V
- name: Apply patch
run: ./certora/munges/munge.sh
- name: Submit Jobs to Certora Prover (Part I)
uses: Certora/certora-run-action@v2
with:
cli-version: "8.10.1"
cli-release: beta
ecosystem: sui
working-directory: certora/spec/
configurations: |-
confs/supply_control_decreases.conf
confs/supply_control_increases_p1.conf
confs/supply_control_increases_p2.conf
confs/accounting_no_lst_no_sui.conf
confs/accounting_total_sui_supply.conf
confs/accounting_value_conservation.conf
confs/fees.conf
job-name: "Verification Part I"
certora-key: ${{ secrets.CERTORAKEY }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Submit Jobs to Certora Prover (Part II)
uses: Certora/certora-run-action@v2
with:
cli-version: "8.10.1"
cli-release: beta
ecosystem: sui
working-directory: certora/spec/
configurations: |-
confs/no_arbitrage.conf
confs/solvency.conf
confs/solvency_monotonicity.conf
confs/validators_consistency_p1.conf
confs/validators_consistency_p2.conf
confs/validators_integrity.conf
job-name: "Verification Part II"
certora-key: ${{ secrets.CERTORAKEY }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7 changes: 7 additions & 0 deletions certora/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# certora
.certora_internal
.venv/
emv-*/
build/
Move.lock
package_summaries
87 changes: 87 additions & 0 deletions certora/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Certora Formal Verification: Suilend Liquid Staking

This directory contains Certora's formal verification of the Suilend liquid staking protocol, written in Move on Sui.

## Directory Structure

### `spec/`

The primary verification package. Contains all specification files (written in Move using the `cvlm` framework), per-rule configuration files, and package metadata.

- `spec/sources/`:Specification modules. Each `.move` file encodes one or more verifiable properties as rules.
- `spec/confs/`:Per-rule Certora Prover configuration files (`.conf`), one per property group.
- `spec/Move.toml`:Package manifest for spec sources. References the `liquid_staking` contract, the `cvlm` (Certora Verification Language for Move) library, Certora Sui framework summaries, and overrides for Sui system packages.

### `munges/`

Code modification scripts and patch files. These make internal functions accessible to the Prover by widening their visibility, and reduce `MAX_VALIDATORS` to make verification tractable.

- `munge.sh`:Applies all patches before running verification.
- `unmunge.sh`:Reverts all patches.
- `record_patches.sh`:Regenerates patch files from current working-directory diffs against HEAD.
- `fees.patch`:Makes `validate_fees()` public.
- `storage.patch`:Widens visibility of view functions and internal helpers; reduces `MAX_VALIDATORS` from 50 to 5.

> [!NOTE]
> The patches are applied as git patches and must be kept in sync with the source code. If the patched files change, `munge.sh` will fail to apply and verification will not run. Use `record_patches.sh` to regenerate the patches after updating the source.

### `assumptions/`

A separate package that verifies assumptions made by summaries in the main spec. Currently contains one rule proving that `liquid_staking::storage::get_sui_amount` is equivalent to `sui_system::staking_pool::get_sui_amount`.

- `assumptions/sources/assumptions.move`:Assumption validation rules.
- `assumptions/Move.toml`:Package manifest.
- `Assumptions.conf`:Prover configuration for running assumption checks.

## Certora Prover

The Certora Prover is a formal verification tool for smart contracts. It statically proves or disproves properties expressed as rules in the Certora Verification Language for Move.

## Running Instructions

0. Install the latest certora prover by following the [installation guide](https://docs.certora.com/en/latest/docs/user-guide/install.html).

1. From the repository root, apply the munges:

```sh
sh certora/munges/munge.sh
```

This only needs to be done once per working copy. **Do not commit the munged files.**

2. Change into the `certora/spec/` directory and run the desired verification job (see table below for all scripts). Example:

```sh
certoraRun confs/solvency.conf
```

Note that `certora/spec/` must be the working directory for `certoraRun`, otherwise it will fail to compile.

3. To revert munges:

```sh
sh certora/munges/unmunge.sh
```

## High-Level Properties

See the doc-comments in each spec file for detailed descriptions of individual rules.

- **Solvency and Exchange Rate Monotonicity** (`solvency.move`, `solvency.conf`, `solvency_monotonicity.conf`): `total_sui_supply >= total_lst_supply` always holds; SUI/LST exchange rate is non-decreasing
- **Total SUI Supply Accounting** (`accounting_total_sui_supply.move`, `accounting_total_sui_supply.conf`): `total_sui_supply` equals the sum of the liquid SUI pool, active stake (via exchange rate), and inactive stake principal
- **Token Supply Initialization Invariants** (`accounting_no_lst_no_sui.move`, `accounting_no_lst_no_sui.conf`): zero LST implies zero SUI backing and vice versa
- **Value Conservation** (`accounting_value_conservation.move`, `accounting_value_conservation.conf`): no value is created or destroyed; on `mint`, deposited SUI equals backing increase plus fees; on `redeem`, SUI decrease equals SUI returned plus fees
- **Fee Accounting Integrity** (`fees.move`, `fees.conf`): accrued fees never exceed total SUI supply, grow monotonically except during collection, and never fully consume a deposit or redemption
- **Supply Control Authorization** (`supply_control.move`, `supply_control_decreases.conf`, `supply_control_increases_p1/p2.conf`): only `mint` can increase supplies; only `redeem`/`custom_redeem` can decrease them
- **No Arbitrage** (`no_arbitrage.move`, `no_arbitrage.conf`): a back-to-back `mint` then `redeem` never yields more SUI than was deposited
- **Validator Registry Consistency** (`validators_consistency.move`, `validators_consistency_p1/p2.conf`): no duplicate validators, registry never exceeds `MAX_VALIDATORS`, no-stake validators have zero `total_sui_amount`
- **Validator Registry Integrity** (`validators_integrity.move`, `validators_integrity.conf`): validators added only by authorized operations, removed only by `refresh`, at most one per call; `refresh` leaves no inactive stake or empty validators
- **Assumption Validation** (`assumptions/sources/assumptions.move`, `Assumptions.conf`): validates assumptions made in summaries

## General Assumptions

- **Loop unrolling.** All specs use `optimistic_loop: true`. `loop_iter` is set to 2 for most validator-list rules and 6 for `validators_upper_bound_step` (one above the munged `MAX_VALIDATORS = 5`).
- **Validator count reduction.** `storage.patch` reduces `MAX_VALIDATORS` from 50 to 5. Several inductive step rules further restrict to 1 validator for tractability.
- **`setup_fresh` precondition.** Most inductive steps model a post-`refresh` state: no inactive stake, all validators have active stake with matching pool IDs and up-to-date exchange rates, `total_sui_supply` is correct, and `last_refresh_epoch == ctx.epoch()`. Each assumption is justified by a separately verified rule.
- **Solvent exchange rates.** `summaries.move` assumes `sui_amount >= pool_token_amount` for all exchange rates, reflecting Sui system-level pool solvency.
- **`redeem_fungible_staked_sui` summary.** The full proportional split between principal and rewards is simplified to `get_sui_amount(er, value)`. An internal `cvlm_assert` checks this is an underapproximation of the actual withdrawal.
13 changes: 13 additions & 0 deletions certora/assumptions/Assumptions.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"optimistic_loop": true,
"prover_args": [
"-maxConcurrentTransforms SIMPLIFIED:1,UNROLL:1,DSA:1",
"-disabledTransformations HOIST_LOOPS",
"-maxMergedBranchSize 1000000",
"-maxCommandCount 10000000",
"-maxBlockCount 1000000",
"-tacDumpsWithInternalFunctions true",
"-callTraceVecElemCount 0",
"-dumpCodeSizeAnalysis true",
]
}
11 changes: 11 additions & 0 deletions certora/assumptions/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "assumptions"
edition = "2024.beta"

[dependencies]
liquid_staking = { local = "../../contracts" }
cvlm = { git = "https://github.com/Certora/cvl-move-proto.git", subdir = "cvlm", rev = "main" }
certora_sui_summaries = { git = "https://github.com/Certora/cvl-move-proto.git", subdir = "certora_sui_summaries", rev = "main" }

[addresses]
assumptions = "0x0"
32 changes: 32 additions & 0 deletions certora/assumptions/sources/assumptions.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module assumptions::assumptions;

/*
Here we validate the assumptions we make in the main spec's summaries
*/

use cvlm::manifest::{rule, function_access};
use cvlm::asserts::cvlm_assert;
use sui_system::staking_pool::PoolTokenExchangeRate;

public fun cvlm_manifest() {
rule(b"get_sui_amount_equivalence");

function_access(b"ls_get_sui_amount", @liquid_staking, b"storage", b"get_sui_amount");
function_access(b"ss_get_sui_amount", @sui_system, b"staking_pool", b"get_sui_amount");
}

// private function accessors
native fun ls_get_sui_amount(exchange_rate: &PoolTokenExchangeRate, token_amount: u64): u64;
native fun ss_get_sui_amount(exchange_rate: &PoolTokenExchangeRate, token_amount: u64): u64;

// The main suilend spec's summaries assume that liquid_staking::storage::get_sui_amount is equivalent to
// sui_system::staking_pool::get_sui_amount. We validate that assumption here.
public fun get_sui_amount_equivalence(
exchange_rate: &PoolTokenExchangeRate,
token_amount: u64
) {
let ls_amount = ls_get_sui_amount(exchange_rate, token_amount);
let ss_amount = ss_get_sui_amount(exchange_rate, token_amount);
cvlm_assert(ls_amount == ss_amount);
}

13 changes: 13 additions & 0 deletions certora/munges/fees.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/contracts/sources/fees.move b/contracts/sources/fees.move
index 33df151..caa341c 100644
--- a/contracts/sources/fees.move
+++ b/contracts/sources/fees.move
@@ -133,7 +133,7 @@ module liquid_staking::fees {
// Note that while it's technically exploitable, we allow lsts to be created with 0 mint/redeem fees.
// This is because having a 0 fee LST is useful in certain cases where mint/redemption can only be done by
// a single party. It is up to the pool creator to ensure that the fees are set correctly.
- fun validate_fees(fees: &FeeConfig) {
+ public fun validate_fees(fees: &FeeConfig) {
assert!(fees.sui_mint_fee_bps <= MAX_BPS, EInvalidFeeConfig);
assert!(fees.staked_sui_mint_fee_bps <= MAX_BPS, EInvalidFeeConfig);
assert!(fees.redeem_fee_bps <= MAX_REDEEM_FEE_BPS, EInvalidFeeConfig);
2 changes: 2 additions & 0 deletions certora/munges/munge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
git apply -3 certora/munges/fees.patch
git apply -3 certora/munges/storage.patch
2 changes: 2 additions & 0 deletions certora/munges/record_patches.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
git diff HEAD:contracts/sources/fees.move contracts/sources/fees.move > certora/munges/fees.patch;
git diff HEAD:contracts/sources/storage.move contracts/sources/storage.move > certora/munges/storage.patch;
Loading
Loading