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
1 change: 1 addition & 0 deletions .github/repo-guard.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ local_dev_solana_version = "1.17.34"
[toolchain.workflow_solana_cli]
".github/workflows/anchor-test.yaml" = "1.17.31"
".github/workflows/generate-verifiable-builds.yaml" = "1.17.31"
".github/workflows/futarchy-fuzz.yaml" = "1.17.34"
".github/workflows/deploy-buffer.yaml" = "1.17.16"
".github/workflows/verify-build.yaml" = "1.17.16"

Expand Down
191 changes: 191 additions & 0 deletions .github/workflows/futarchy-fuzz.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Futarchy fuzz

on:
workflow_dispatch:
inputs:
sequences:
description: Number of independent fuzz sequences per worker
required: true
default: "500"
type: string
flows:
description: Number of flows executed in each sequence
required: true
default: "500"
type: string
seed:
description: Optional base seed for reproducing a previous run
required: false
default: ""
type: string

permissions:
contents: read

concurrency:
group: futarchy-fuzz-${{ github.ref }}
cancel-in-progress: false

jobs:
fuzz:
runs-on: ubuntu-22.04
timeout-minutes: 300

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Prepare run artifact
shell: bash
env:
FUZZ_SEQUENCES: ${{ inputs.sequences }}
FUZZ_FLOWS: ${{ inputs.flows }}
FUZZ_SEED: ${{ inputs.seed }}
run: |
mkdir -p fuzz-artifacts
{
echo "commit=${GITHUB_SHA}"
echo "sequences=${FUZZ_SEQUENCES}"
echo "flows=${FUZZ_FLOWS}"
if [ -n "$FUZZ_SEED" ]; then
echo "seed=${FUZZ_SEED}"
else
echo "seed=<generated by Wake.sol; see fuzz.log>"
fi
} > fuzz-artifacts/run-manifest.txt

- name: Validate campaign inputs
shell: bash
env:
FUZZ_SEQUENCES: ${{ inputs.sequences }}
FUZZ_FLOWS: ${{ inputs.flows }}
run: |
if ! [[ "$FUZZ_SEQUENCES" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::sequences must be a positive integer"
exit 1
fi
if ! [[ "$FUZZ_FLOWS" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::flows must be a positive integer"
exit 1
fi

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
build-essential \
libssl-dev \
libudev-dev \
pkg-config \
python3-dev

- name: Install Node.js, Solana, and Anchor
uses: metadaoproject/setup-anchor@aeeb5505f3fd3d52a1080d14863c6aa428c9fdf9 # v3.4
with:
node-version: "20.18.0"
solana-cli-version: "1.17.34"
anchor-version: "0.29.0"

- name: Install Rust toolchain
run: |
rustup toolchain install 1.89.0 --profile minimal
rustup default 1.89.0

- name: Restore build cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
~/.cache/uv/
target/
${{ runner.temp }}/wake-sol/target/
key: futarchy-fuzz-build-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}-${{ github.run_id }}-${{ github.run_attempt }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Run-specific caches accumulate

The cache key includes both github.run_id and github.run_attempt, so every invocation creates a distinct cache even when Cargo.lock is unchanged. GitHub caches are immutable: the restore prefix can reuse an older entry, but the workflow will still save another copy of the Cargo, build, and Wake artifacts. Repeated fuzz runs will consume the cache quota and eventually evict useful entries. A stable, content-based key would allow these build caches to be reused without creating a duplicate for every run.

Suggested change
key: futarchy-fuzz-build-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}-${{ github.run_id }}-${{ github.run_attempt }}
key: futarchy-fuzz-build-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

restore-keys: |
futarchy-fuzz-build-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}-
futarchy-fuzz-build-${{ runner.os }}-

- name: Build Wake.sol
run: |
python3 -m pip install --user "uv==0.8.14"

wake_dir="${RUNNER_TEMP}/wake-sol"
git init "$wake_dir"
git -C "$wake_dir" remote add origin https://github.com/ack3-ai/wake.sol.git
git -C "$wake_dir" fetch --depth 1 origin 096d5bfed511d0f0a3e27c960468a756a6c6440b
git -C "$wake_dir" checkout --detach FETCH_HEAD
test "$(git -C "$wake_dir" rev-parse HEAD)" = \
"096d5bfed511d0f0a3e27c960468a756a6c6440b"

cd "$wake_dir"
"$HOME/.local/bin/uv" sync --frozen --no-install-project
"$HOME/.local/bin/uv" pip install "maturin[patchelf]==1.9.4"
"$HOME/.local/bin/uv" run --no-sync maturin develop --release --locked
.venv/bin/python -c "import wake_sol"

- name: Build Futarchy
run: |
anchor build -p futarchy -- -- --locked
test -s target/deploy/futarchy.so
test -s verifiable-builds/conditional_vault.so
test -s tests/fixtures/squads_multisig.so
test -s tests/fixtures/squads-program-config

- name: Verify generated bindings
run: |
wake_python="${RUNNER_TEMP}/wake-sol/.venv/bin/python"
wake_cli="${RUNNER_TEMP}/wake-sol/.venv/bin/wake-sol"
"$wake_python" fuzz/futarchy/gen_wake_idl.py
"$wake_cli" gen \
--target-idl target/wake-idl \
--out fuzz/futarchy/pytypes \
--only FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq \
--check \
--strict
"$wake_python" -c "import fuzz.futarchy.pytypes.futarchy"

- name: Run fuzz campaign
shell: bash
env:
FUZZ_SEQUENCES: ${{ inputs.sequences }}
FUZZ_FLOWS: ${{ inputs.flows }}
FUZZ_SEED: ${{ inputs.seed }}
run: |
wake_python="${RUNNER_TEMP}/wake-sol/.venv/bin/python"
wake_cli="${RUNNER_TEMP}/wake-sol/.venv/bin/wake-sol"
if [ -n "$FUZZ_SEED" ]; then
test_args=(
"$wake_python" -m pytest -q -s
--seed "$FUZZ_SEED"
fuzz/futarchy/test_fuzz.py
)
echo "mode=single-process seed replay" >> fuzz-artifacts/run-manifest.txt
else
test_args=(
"$wake_cli" test -P -q
fuzz/futarchy/test_fuzz.py
)
echo "mode=parallel, one worker per CPU" >> fuzz-artifacts/run-manifest.txt
fi

set +e
"${test_args[@]}" 2>&1 | tee fuzz-artifacts/fuzz.log
fuzz_status=${PIPESTATUS[0]}
set -e

if [ -d .wake-sol/logs ]; then
mkdir -p fuzz-artifacts/wake-logs
cp -R .wake-sol/logs/. fuzz-artifacts/wake-logs/
fi
echo "exit_status=${fuzz_status}" >> fuzz-artifacts/run-manifest.txt
exit "$fuzz_status"

- name: Upload fuzz results
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: futarchy-fuzz-${{ github.run_id }}-${{ github.run_attempt }}
path: fuzz-artifacts/
if-no-files-found: error
retention-days: 14
116 changes: 116 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Futarchy Fuzz Test

This directory contains the Wake.sol stateful fuzz test for the Futarchy
program at commit `658c342c2f1ea11917d5fed10539d078e9e1a3b7`. It has 29
instruction-level happy paths, 29 unhappy paths, two support flows, and twelve
global invariants. Each instruction wrapper also checks its
own postconditions or atomic rollback behavior.

## Running the test

1. Install [Wake.sol](https://github.com/ack3-ai/wake.sol) in a Python virtual
environment at commit
`096d5bfed511d0f0a3e27c960468a756a6c6440b`.

2. Activate the environment.

3. From this repository's root, build the Futarchy program:

```bash
anchor build -p futarchy
```

4. Create Wake's compatible IDL and regenerate the checked-in Python bindings:

```bash
python fuzz/futarchy/gen_wake_idl.py
wake-sol gen \
--target-idl target/wake-idl \
--out fuzz/futarchy/pytypes \
--only FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq \
--strict
```

5. Ensure the built Futarchy artifact and checked-in dependencies are present:

```text
target/deploy/futarchy.so
verifiable-builds/conditional_vault.so
tests/fixtures/squads_multisig.so
tests/fixtures/squads-program-config
```

6. Run the default 100-sequence, 500-flow campaign:

```bash
python -m pytest -q -s fuzz/futarchy/test_fuzz.py
```

Use `FUZZ_SEQUENCES` and `FUZZ_FLOWS` to change the number of sequences and
flows for local or pipeline runs.

The test prints a base seed. Reproduce that run with:

```bash
BASE_SEED="<printed-base-seed>"
python -m pytest -q -s --seed "$BASE_SEED" fuzz/futarchy/test_fuzz.py
```

Failures and their reproducible crash data are written under
`.wake-sol/logs/crashes/`.

### GitHub Actions

The `Futarchy fuzz` workflow runs only when manually started from the repository's
Actions page. Choose **Run workflow** and set the sequence and flow counts. Leave
the seed empty for a new campaign, or enter a previously printed base seed to
reproduce a run.

For a new campaign, the workflow starts one worker per available CPU. The
default is 500 sequences of 500 flows per worker, and both values can be changed
when starting the workflow. Providing a seed runs an exact single-process replay
instead. The workflow has a five-hour timeout and uploads the run log, manifest,
and any Wake crash data as a
`futarchy-fuzz-<run>-<attempt>` artifact retained for 14 days.

## Flows

Every instruction name below has a `<name>_happy` flow for a valid transition
and a `<name>_unhappy` flow for a small expected-failure case.

| Area | Instruction flows |
| --- | --- |
| DAO creation | `initialize_dao` |
| Proposal creation | `initialize_proposal`, `initialize_large_spend_proposal`, `initialize_mint_tokens_proposal`, `initialize_spending_limit_change_proposal`, `initialize_hostile_takeover_proposal`, `initialize_hostile_liquidate_proposal`, `initialize_buyback_token_proposal` |
| Proposal lifecycle | `stake_to_proposal`, `unstake_from_proposal`, `sponsor_proposal`, `launch_proposal`, `finalize_proposal` |
| AMM | `provide_liquidity`, `withdraw_liquidity`, `spot_swap`, `conditional_swap`, `collect_fees` |
| DAO configuration | `update_dao`, `set_spending_limit`, `sync_spending_limit` |
| Squads administration | `admin_enqueue_multisig_proposal_approval`, `execute_multisig_proposal_approval`, `admin_enqueue_multisig_proposal_cancellation`, `execute_multisig_proposal_cancellation`, `admin_execute_multisig_proposal` |
| Proposal administration | `admin_cancel_proposal`, `admin_remove_proposal`, `admin_update_proposal_params` |

Two additional flows support those instruction flows:

- `advance_clock` moves the deterministic clock across proposal and TWAP
boundaries.
- `execute_passed_proposal_payload` executes approved proposal payloads through
Squads.

Happy flows check their instruction's account and token transitions. Unhappy
flows require the intended error and verify that writable accounts roll back.

## Global invariants

| Invariant | Property checked after every flow |
| --- | --- |
| `dao_identity_is_canonical` | The DAO PDA, owner, mints, Squads accounts, and AMM vaults remain canonical. |
| `dao_configuration_stays_valid` | Mutable DAO configuration remains inside the program's valid bounds. |
| `underlying_token_supplies_are_conserved` | All base and quote tokens remain accounted for. |
| `spot_reserves_and_fees_are_fully_backed` | Spot vault balances equal reserves plus protocol fees. |
| `conditional_reserves_and_fees_are_fully_backed` | In Futarchy state, each outcome's spot-plus-conditional reserves and fees equal underlying vault tokens plus that outcome's vault tokens. |
| `conditional_token_supplies_are_backed` | Both conditional-token supplies are accounted for and collateral covers unresolved or resolved claims. |
| `positions_sum_to_total_liquidity` | Canonical AMM positions sum to total liquidity. |
| `proposal_account_graphs_are_canonical` | Proposal, market, vault, and conditional-mint links remain canonical. |
| `proposal_stake_custody_is_conserved` | Stake records equal proposal custody balances. |
| `enqueued_approvals_are_canonical` | Live Squads approval records have the correct PDA and transaction index. |
| `enqueued_cancellations_are_canonical` | Live Squads cancellation records have the correct PDA and transaction index. |
| `cancelled_transactions_never_execute` | Cancelled Squads proposals remain cancelled and never execute. |
1 change: 1 addition & 0 deletions fuzz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Wake.sol fuzz harness packages for workspace programs."""
1 change: 1 addition & 0 deletions fuzz/futarchy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Wake.sol fuzz harness for Futarchy."""
Loading