From 2b866a92dbc2b030a422a5f97687639dfb44e49d Mon Sep 17 00:00:00 2001 From: hj Date: Tue, 27 Oct 2026 05:09:00 +0800 Subject: [PATCH] feat(bench): add Soroban instruction and memory budget regression benchmarks (#23) Add Soroban CPU instruction and memory byte resource-budget benchmarks across mergefi-escrow, mergefi-milestones, and mergefi-maintenance-pool using env.cost_estimate().budget(). - Cover all public entrypoints across all three contracts. - Add size-parameterized scaling curves for release/release_issue/withdraw recipient counts (1, 5, 10, 20), refund/cancel_milestone contributor counts (1, 5, 10, 20), milestone allocations map growth (1, 10, 25, 50), and maintenance-pool deposit_count growth (1, 10, 25, 50). - Enforce a 150% baseline regression tolerance ceiling and Soroban mainnet per-invocation limits (100M CPU instructions, 40 MiB memory). - Add make bench target to Makefile and document baseline tables and scaling analysis in BENCHMARKS.md and README.md. Closes #23 --- BENCHMARKS.md | 206 +++++++++++++ Makefile | 7 +- README.md | 18 ++ contracts/escrow/src/bench.rs | 358 ++++++++++++++++++++++ contracts/escrow/src/lib.rs | 2 + contracts/maintenance-pool/src/bench.rs | 347 +++++++++++++++++++++ contracts/maintenance-pool/src/lib.rs | 2 + contracts/milestones/src/bench.rs | 387 ++++++++++++++++++++++++ contracts/milestones/src/lib.rs | 2 + 9 files changed, 1328 insertions(+), 1 deletion(-) create mode 100644 BENCHMARKS.md create mode 100644 contracts/escrow/src/bench.rs create mode 100644 contracts/maintenance-pool/src/bench.rs create mode 100644 contracts/milestones/src/bench.rs diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 00000000..a6cd250d --- /dev/null +++ b/BENCHMARKS.md @@ -0,0 +1,206 @@ +# Soroban Instruction & Resource-Budget Benchmarks + +This document records the baseline Soroban CPU instruction (`cpu_instruction_cost`) and memory byte (`memory_bytes_cost`) measurements for every public entrypoint across `mergefi-escrow`, `mergefi-milestones`, and `mergefi-maintenance-pool`, including size-parameterized scaling curves for bounded and unbounded on-chain collections. + +## Running the Benchmarks + +Use the `bench` target in the root `Makefile`: + +```sh +make bench +``` + +Or invoke `cargo test` directly on the workspace: + +```sh +cargo test --workspace bench_ -- --nocapture --test-threads=1 +``` + +## Methodology & Regression Detection + +1. **Measurement API**: Each entrypoint invocation is wrapped in a helper that calls `env.cost_estimate().budget().reset_default()` immediately before the contract call and reads `env.cost_estimate().budget().cpu_instruction_cost()` and `env.cost_estimate().budget().memory_bytes_cost()` immediately after the call returns. +2. **Isolated Scaling Environments**: Size-parameterized scaling benchmarks (`recipients`, `contributors`, `allocations`, `deposit_count`) construct a fresh `Env::default()` per scaling point `N` so that host shadow state from prior scenarios does not skew the curve. +3. **Regression Tolerance Band**: Every benchmark test asserts: + - `cpu > 0` and `mem > 0` + - `cpu <= baseline_cpu * 1.50` and `mem <= baseline_mem * 1.50` (`REGRESSION_TOLERANCE_BPS = 15_000`) + - `cpu <= 100_000_000` (Soroban mainnet per-transaction CPU instruction limit) + - `mem <= 41_943_040` (Soroban mainnet 40 MiB per-transaction memory byte limit) + +--- + +## 1. `mergefi-escrow` Baseline & Scaling Curves + +### Entrypoint Baselines + +| Entrypoint / Scenario | Measured CPU Instrs | Measured Mem Bytes | Regression CPU Ceiling | Regression Mem Ceiling | +|---|---:|---:|---:|---:| +| `initialize` | 94,360 | 11,353 | 142,500 | 18,000 | +| `fund` | 441,119 | 69,382 | 675,000 | 105,000 | +| `contribute` | 492,025 | 73,453 | 750,000 | 112,500 | +| `get_escrow` | 50,657 | 8,275 | 78,000 | 13,500 | +| `get_contribution` | 47,065 | 7,745 | 72,000 | 12,000 | +| `get_contributions (2 sponsors)` | 81,061 | 10,967 | 123,000 | 16,500 | +| `extend_deadline` | 165,273 | 25,080 | 255,000 | 39,000 | +| `keep_alive (2 sponsors)` | 67,984 | 9,555 | 105,000 | 15,000 | +| `get_admin` | 48,269 | 8,042 | 75,000 | 12,750 | +| `get_oracle` | 48,057 | 8,042 | 75,000 | 12,750 | +| `get_treasury` | 47,061 | 8,042 | 75,000 | 12,750 | +| `get_fee_bps` | 47,935 | 8,042 | 75,000 | 12,750 | +| `get_version` | 47,603 | 8,042 | 75,000 | 12,750 | +| `get_max_sponsors` | 48,922 | 8,149 | 75,000 | 12,750 | +| `get_grace_period` | 30,352 | 6,343 | 48,000 | 9,750 | +| `get_bps_denominator` | 30,352 | 6,343 | 48,000 | 9,750 | +| `set_fee_bps` | 121,251 | 21,371 | 187,500 | 33,000 | +| `set_admin` | 136,613 | 28,326 | 210,000 | 43,500 | +| `recover_admin` | 138,772 | 29,756 | 210,000 | 45,000 | +| `set_oracle` | 140,501 | 31,287 | 217,500 | 48,000 | +| `set_treasury` | 118,478 | 24,118 | 180,000 | 37,500 | +| `pause` | 114,466 | 24,261 | 177,000 | 37,500 | +| `is_paused_view` | 45,345 | 8,926 | 70,500 | 14,250 | +| `unpause` | 118,211 | 24,535 | 180,000 | 37,500 | +| `migrate (idempotent)` | 87,593 | 17,430 | 135,000 | 27,000 | +| `refund (permissionless after deadline)` | 320,407 | 47,731 | 675,000 | 120,000 | +| `upgrade` | 126,523 | 26,381 | 240,000 | 52,500 | + +### Scaling Curve: `release` by Recipient Count (`MAX_RECIPIENTS = 20`) + +| Recipients (`N`) | Measured CPU Instrs | Measured Mem Bytes | Mainnet CPU % (`100M`) | Mainnet Mem % (`40 MiB`) | +|---:|---:|---:|---:|---:| +| 1 | 496,331 | 69,654 | 0.50% | 0.17% | +| 5 | 1,275,853 | 171,782 | 1.28% | 0.41% | +| 10 | 2,341,475 | 308,577 | 2.34% | 0.74% | +| 20 | 4,684,429 | 612,617 | 4.68% | 1.46% | + +- **Marginal cost per recipient**: ~220,400 CPU instructions and ~28,600 memory bytes (driven by cross-contract SAC `token_client.transfer` and `PaidTo` event emission per recipient). At `MAX_RECIPIENTS = 20`, `release` uses `< 5%` of the Soroban mainnet per-invocation CPU ceiling. + +### Scaling Curve: `refund` by Contributor Count (`MAX_SPONSORS = 20`) + +| Contributors (`N`) | Measured CPU Instrs | Measured Mem Bytes | Mainnet CPU % (`100M`) | Mainnet Mem % (`40 MiB`) | +|---:|---:|---:|---:|---:| +| 1 | 317,866 | 44,397 | 0.32% | 0.11% | +| 5 | 1,298,442 | 167,945 | 1.30% | 0.40% | +| 10 | 2,670,856 | 371,655 | 2.67% | 0.89% | +| 20 | 6,014,938 | 943,325 | 6.01% | 2.25% | + +- **Marginal cost per contributor**: ~299,800 CPU instructions and ~47,300 memory bytes (driven by reading `DataKey::Contribution(issue_id, i)`, extending its TTL, transferring tokens back to `contribution.sponsor`, and publishing `RefundPaid`). At the default `MAX_SPONSORS = 20`, `refund` uses `6.01%` of the Soroban mainnet CPU budget. + +--- + +## 2. `mergefi-milestones` Baseline & Scaling Curves + +### Entrypoint Baselines + +| Entrypoint / Scenario | Measured CPU Instrs | Measured Mem Bytes | Regression CPU Ceiling | Regression Mem Ceiling | +|---|---:|---:|---:|---:| +| `initialize` | 94,360 | 11,353 | 142,500 | 18,000 | +| `create_milestone` | 446,929 | 70,128 | 720,000 | 112,500 | +| `contribute` | 518,429 | 76,068 | 780,000 | 120,000 | +| `allocate (initial)` | 195,408 | 32,399 | 375,000 | 67,500 | +| `get_milestone` | 58,289 | 9,153 | 90,000 | 15,000 | +| `get_issue_status` | 43,783 | 7,670 | 82,500 | 14,250 | +| `get_contribution` | 53,169 | 8,412 | 82,500 | 14,250 | +| `get_contributions (2 sponsors)` | 86,271 | 11,670 | 142,500 | 21,000 | +| `deallocate` | 145,281 | 23,016 | 375,000 | 67,500 | +| `keep_alive (2 sponsors, 1 issue)` | 87,125 | 11,590 | 150,000 | 22,500 | +| `get_admin` | 47,121 | 8,246 | 82,500 | 14,250 | +| `get_oracle` | 46,909 | 8,246 | 82,500 | 14,250 | +| `get_treasury` | 45,913 | 8,246 | 82,500 | 14,250 | +| `get_fee_bps` | 46,787 | 8,246 | 82,500 | 14,250 | +| `get_version` | 46,455 | 8,246 | 82,500 | 14,250 | +| `get_max_sponsors` | 47,774 | 8,353 | 82,500 | 14,250 | +| `get_grace_period` | 29,204 | 6,547 | 52,500 | 11,250 | +| `get_bps_denominator` | 29,204 | 6,547 | 52,500 | 11,250 | +| `set_fee_bps` | 121,098 | 22,187 | 195,000 | 37,500 | +| `set_admin` | 141,694 | 29,754 | 217,500 | 48,000 | +| `recover_admin` | 142,439 | 31,184 | 217,500 | 48,000 | +| `set_oracle` | 148,888 | 32,715 | 225,000 | 52,500 | +| `set_treasury` | 123,432 | 24,934 | 187,500 | 42,000 | +| `pause` | 119,267 | 25,077 | 187,500 | 42,000 | +| `is_paused_view` | 46,295 | 9,130 | 75,000 | 15,000 | +| `unpause` | 119,361 | 25,351 | 187,500 | 42,000 | +| `migrate (idempotent)` | 88,499 | 18,042 | 142,500 | 30,000 | +| `cancel_milestone_after_deadline` | 321,249 | 49,763 | 720,000 | 127,500 | +| `upgrade` | 163,845 | 35,127 | 240,000 | 52,500 | + +### Scaling Curve: `allocate` and `release_issue` by Prior Allocation Count (`Milestone.allocations` Map Size) + +| Prior Allocations (`M`) | `allocate` CPU Instrs | `allocate` Mem Bytes | `release_issue (recipients=1)` CPU Instrs | `release_issue (recipients=1)` Mem Bytes | +|---:|---:|---:|---:|---:| +| 1 | 175,334 | 28,258 | 557,833 | 76,910 | +| 10 | 206,543 | 39,562 | 615,178 | 95,486 | +| 25 | 252,437 | 58,402 | 698,055 | 126,446 | +| 50 | 319,385 | 89,802 | 840,112 | 178,046 | + +- **Marginal cost per entry in `Milestone.allocations: Map`**: ~2,940 CPU instructions and ~1,256 memory bytes on `allocate`, and ~5,760 CPU instructions and ~2,064 memory bytes on `release_issue` (because `Milestone` is deserialized from persistent storage with its inline `allocations` `Map`). Even at 50 issues in a single milestone, `allocate` and `release_issue` stay below `0.85%` of the mainnet CPU budget. + +### Scaling Curve: `release_issue` by Recipient Count (`MAX_RECIPIENTS = 20`) + +| Recipients (`N`) | Measured CPU Instrs | Measured Mem Bytes | Mainnet CPU % (`100M`) | Mainnet Mem % (`40 MiB`) | +|---:|---:|---:|---:|---:| +| 1 | 557,833 | 76,910 | 0.56% | 0.18% | +| 5 | 1,357,494 | 182,322 | 1.36% | 0.43% | +| 10 | 2,419,944 | 323,222 | 2.42% | 0.77% | +| 20 | 4,813,121 | 635,472 | 4.81% | 1.52% | + +### Scaling Curve: `cancel_milestone` by Contributor Count (`MAX_SPONSORS = 20`) + +| Contributors (`N`) | Measured CPU Instrs | Measured Mem Bytes | Mainnet CPU % (`100M`) | Mainnet Mem % (`40 MiB`) | +|---:|---:|---:|---:|---:| +| 1 | 325,188 | 45,384 | 0.33% | 0.11% | +| 5 | 1,272,401 | 166,840 | 1.27% | 0.40% | +| 10 | 2,591,245 | 367,935 | 2.59% | 0.88% | +| 20 | 5,828,653 | 934,375 | 5.83% | 2.23% | + +--- + +## 3. `mergefi-maintenance-pool` Baseline & Scaling Curves + +### Entrypoint Baselines + +| Entrypoint / Scenario | Measured CPU Instrs | Measured Mem Bytes | Regression CPU Ceiling | Regression Mem Ceiling | +|---|---:|---:|---:|---:| +| `initialize` | 84,140 | 10,543 | 142,500 | 18,000 | +| `deposit (initial pool creation)` | 432,387 | 66,459 | 720,000 | 112,500 | +| `get_pool` | 52,779 | 7,917 | 82,500 | 14,250 | +| `get_deposit` | 39,113 | 6,771 | 82,500 | 14,250 | +| `sweep` | 322,849 | 46,135 | 720,000 | 120,000 | +| `get_admin` | 42,306 | 7,117 | 82,500 | 14,250 | +| `get_oracle` | 42,758 | 7,117 | 82,500 | 14,250 | +| `get_treasury` | 42,094 | 7,117 | 82,500 | 14,250 | +| `get_fee_bps` | 41,972 | 7,117 | 82,500 | 14,250 | +| `get_version` | 42,636 | 7,117 | 82,500 | 14,250 | +| `get_bps_denominator` | 27,998 | 5,669 | 52,500 | 11,250 | +| `get_max_deposits` | 27,998 | 5,669 | 52,500 | 11,250 | +| `get_inactivity_window` | 27,998 | 5,669 | 52,500 | 11,250 | +| `set_admin` | 122,376 | 24,496 | 217,500 | 48,000 | +| `recover_admin` | 122,333 | 25,926 | 217,500 | 48,000 | +| `set_oracle` | 127,196 | 27,457 | 225,000 | 52,500 | +| `set_treasury` | 104,754 | 21,308 | 187,500 | 42,000 | +| `pause` | 105,341 | 21,451 | 187,500 | 42,000 | +| `is_paused_view` | 41,794 | 7,865 | 75,000 | 15,000 | +| `unpause` | 107,377 | 21,725 | 187,500 | 42,000 | +| `migrate (idempotent)` | 79,280 | 15,454 | 142,500 | 30,000 | +| `upgrade` | 118,389 | 23,341 | 240,000 | 52,500 | + +### Scaling Curve: `deposit`, `keep_alive`, `withdraw`, and `reclaim_deposit` by `deposit_count` (`MAX_DEPOSITS = 200`) + +| `deposit_count` (`D`) | `deposit` CPU / Mem | `keep_alive` CPU / Mem | `withdraw (recipients=1)` CPU / Mem | `reclaim_deposit` CPU / Mem | +|---:|---:|---:|---:|---:| +| 1 | 427,581 / 65,539 | 59,017 / 8,099 | 496,735 / 68,881 | 334,737 / 47,586 | +| 10 | 821,001 / 123,487 | 161,917 / 17,279 | 724,814 / 119,641 | 587,809 / 85,764 | +| 25 | 1,526,002 / 224,077 | 405,553 / 37,079 | 1,130,622 / 208,741 | 1,098,299 / 153,894 | +| 50 | 2,909,369 / 404,227 | 988,014 / 82,579 | 1,951,621 / 369,741 | 2,114,499 / 279,944 | + +- **Marginal cost per distinct deposit (`D`)**: + - `deposit`: ~50,600 CPU instructions and ~6,900 memory bytes per existing deposit record (scanning `0..deposit_count` for sponsor deduplication and extending TTLs on all deposit records). + - `keep_alive`: ~18,900 CPU instructions and ~1,520 memory bytes per deposit record (extending TTL on `DataKey::Deposit(pool_id, i)`). + - `withdraw` / `reclaim_deposit`: ~29,700–36,300 CPU instructions per deposit record (FIFO deduction across `Deposit` entries and TTL refresh). + +### Scaling Curve: `withdraw` by Recipient Count (`MAX_RECIPIENTS = 20`) + +| Recipients (`N`) | Measured CPU Instrs | Measured Mem Bytes | Mainnet CPU % (`100M`) | Mainnet Mem % (`40 MiB`) | +|---:|---:|---:|---:|---:| +| 1 | 496,735 | 68,881 | 0.50% | 0.16% | +| 5 | 1,287,129 | 170,901 | 1.29% | 0.41% | +| 10 | 2,350,786 | 307,561 | 2.35% | 0.73% | +| 20 | 4,711,091 | 611,331 | 4.71% | 1.46% | diff --git a/Makefile b/Makefile index 3a897053..59f6c575 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ORACLE ?= TREASURY ?= FEE_BPS ?= -.PHONY: build build-logs bindings test test-verbose fmt clean deploy-escrow deploy-milestones deploy-maintenance-pool deploy +.PHONY: build build-logs bindings test test-verbose bench fmt clean deploy-escrow deploy-milestones deploy-maintenance-pool deploy ## Build all contracts to optimized wasm (wasm32v1-none, the target Soroban's ## host currently requires for Rust 1.84+; falls back instructions below if @@ -53,6 +53,11 @@ test: test-verbose: cargo test --workspace -- --nocapture +## Run Soroban CPU instruction and memory byte resource-budget benchmarks across +## all contracts and print the baseline/scaling tables. +bench: + cargo test --workspace bench_ -- --nocapture --test-threads=1 + fmt: cargo fmt --all diff --git a/README.md b/README.md index 8d3a403a..655dd80a 100644 --- a/README.md +++ b/README.md @@ -715,6 +715,24 @@ crowdfunding tests) on the native target using The `wasm32v1-none` release build was also verified — all three contracts compile to `.wasm` in `target/wasm32v1-none/release/`. +### Soroban resource-budget benchmarks + +Every public entrypoint across `mergefi-escrow`, `mergefi-milestones`, and +`mergefi-maintenance-pool` has a CPU instruction and memory byte benchmark in +`contracts/*/src/bench.rs`, including size-parameterized scaling curves for +recipient splits (`1, 5, 10, 20`), crowdfunded contributor refunds (`1, 5, 10, 20`), +milestone `allocations` map growth (`1, 10, 25, 50`), and maintenance-pool +`deposit_count` growth (`1, 10, 25, 50`). + +Print the full benchmark tables with: + +```sh +make bench +``` + +Baseline resource tables and regression tolerance rules are documented in +[BENCHMARKS.md](BENCHMARKS.md). + ### Deployed on Stellar testnet All three contracts are deployed and initialized on testnet as of this diff --git a/contracts/escrow/src/bench.rs b/contracts/escrow/src/bench.rs new file mode 100644 index 00000000..3eb83ad0 --- /dev/null +++ b/contracts/escrow/src/bench.rs @@ -0,0 +1,358 @@ +#![cfg(test)] + +extern crate std; + +use super::*; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + token, Address, BytesN, Env, Vec, +}; + +const SOROBAN_MAX_CPU_INSTRUCTIONS: u64 = 100_000_000; +const SOROBAN_MAX_MEMORY_BYTES: u64 = 41_943_040; +const REGRESSION_TOLERANCE_BPS: u64 = 15_000; // 150% of baseline + +fn create_token<'a>( + env: &Env, + admin: &Address, +) -> (Address, token::StellarAssetClient<'a>, token::Client<'a>) { + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + let address = sac.address(); + ( + address.clone(), + token::StellarAssetClient::new(env, &address), + token::Client::new(env, &address), + ) +} + +fn native_wasm_hash(env: &Env) -> BytesN<32> { + env.crypto().sha256(&soroban_sdk::Bytes::new(env)).into() +} + +fn measure(env: &Env, f: impl FnOnce()) -> (u64, u64) { + env.cost_estimate().budget().reset_default(); + f(); + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + (cpu, mem) +} + +fn check_budget(label: &str, cpu: u64, mem: u64, baseline_cpu: u64, baseline_mem: u64) { + let max_cpu = (baseline_cpu * REGRESSION_TOLERANCE_BPS) / 10_000; + let max_mem = (baseline_mem * REGRESSION_TOLERANCE_BPS) / 10_000; + std::println!( + "| {:<42} | {:>12} | {:>12} | {:>12} | {:>12} |", + label, + cpu, + mem, + max_cpu, + max_mem + ); + assert!(cpu > 0, "{label}: measured CPU instructions must be > 0"); + assert!(mem > 0, "{label}: measured memory bytes must be > 0"); + assert!( + cpu <= max_cpu && cpu <= SOROBAN_MAX_CPU_INSTRUCTIONS, + "{label}: CPU instruction regression: measured {cpu} > ceiling {max_cpu} (baseline {baseline_cpu})" + ); + assert!( + mem <= max_mem && mem <= SOROBAN_MAX_MEMORY_BYTES, + "{label}: memory byte regression: measured {mem} > ceiling {max_mem} (baseline {baseline_mem})" + ); +} + +fn build_recipients(env: &Env, count: u32) -> Vec<(Address, u32)> { + let mut recipients = Vec::new(env); + let base_share = 10_000u32 / count; + let remainder = 10_000u32 % count; + for i in 0..count { + let addr = Address::generate(env); + let share = if i == 0 { + base_share + remainder + } else { + base_share + }; + recipients.push_back((addr, share)); + } + recipients +} + +#[test] +fn bench_escrow_entrypoints_and_scaling() { + std::println!("\n=== mergefi-escrow Resource Budget Benchmark ==="); + std::println!( + "| {:<42} | {:>12} | {:>12} | {:>12} | {:>12} |", + "Entrypoint / Scenario", + "CPU Instrs", + "Mem Bytes", + "CPU Ceiling", + "Mem Ceiling" + ); + std::println!( + "|{:-<44}|{:-<14}|{:-<14}|{:-<14}|{:-<14}|", + "", + "", + "", + "", + "" + ); + + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().disable_resource_limits(); + + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let treasury = Address::generate(&env); + let recovery = Address::generate(&env); + let contract_id = env.register(EscrowContract, ()); + let client = EscrowContractClient::new(&env, &contract_id); + + let (cpu, mem) = measure(&env, || { + client.initialize( + &admin, + &oracle, + &treasury, + &500u32, + &None, + &Some(recovery.clone()), + ); + }); + check_budget("initialize", cpu, mem, 95_000, 12_000); + + let token_admin = Address::generate(&env); + let (token_addr, asset_client, _) = create_token(&env, &token_admin); + let sponsor_a = Address::generate(&env); + let sponsor_b = Address::generate(&env); + asset_client.mint(&sponsor_a, &100_000_000i128); + asset_client.mint(&sponsor_b, &100_000_000i128); + + let (cpu, mem) = measure(&env, || { + client.fund( + &1u64, + &sponsor_a, + &token_addr, + &1_000_000i128, + &10_000u64, + &None, + ); + }); + check_budget("fund", cpu, mem, 450_000, 70_000); + + let (cpu, mem) = measure(&env, || { + client.contribute(&1u64, &sponsor_b, &500_000i128); + }); + check_budget("contribute", cpu, mem, 500_000, 75_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_escrow(&1u64); + }); + check_budget("get_escrow", cpu, mem, 52_000, 9_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_contribution(&1u64, &0u32); + }); + check_budget("get_contribution", cpu, mem, 48_000, 8_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_contributions(&1u64); + }); + check_budget("get_contributions (2 sponsors)", cpu, mem, 82_000, 11_000); + + let (cpu, mem) = measure(&env, || { + client.extend_deadline(&1u64, &sponsor_a, &20_000u64); + }); + check_budget("extend_deadline", cpu, mem, 170_000, 26_000); + + let (cpu, mem) = measure(&env, || { + client.keep_alive(&1u64); + }); + check_budget("keep_alive (2 sponsors)", cpu, mem, 70_000, 10_000); + + // Admin & view entrypoints + let (cpu, mem) = measure(&env, || { + let _ = client.get_admin(); + }); + check_budget("get_admin", cpu, mem, 50_000, 8_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_oracle(); + }); + check_budget("get_oracle", cpu, mem, 50_000, 8_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_treasury(); + }); + check_budget("get_treasury", cpu, mem, 50_000, 8_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_fee_bps(); + }); + check_budget("get_fee_bps", cpu, mem, 50_000, 8_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_version(); + }); + check_budget("get_version", cpu, mem, 50_000, 8_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_max_sponsors(); + }); + check_budget("get_max_sponsors", cpu, mem, 50_000, 8_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_grace_period(); + }); + check_budget("get_grace_period", cpu, mem, 32_000, 6_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_bps_denominator(); + }); + check_budget("get_bps_denominator", cpu, mem, 32_000, 6_500); + + let (cpu, mem) = measure(&env, || { + client.set_fee_bps(&600u32); + }); + check_budget("set_fee_bps", cpu, mem, 125_000, 22_000); + client.set_fee_bps(&500u32); + + let new_admin = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_admin(&new_admin); + }); + check_budget("set_admin", cpu, mem, 140_000, 29_000); + + let (cpu, mem) = measure(&env, || { + client.recover_admin(&admin); + }); + check_budget("recover_admin", cpu, mem, 140_000, 30_000); + + let new_oracle = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_oracle(&new_oracle); + }); + check_budget("set_oracle", cpu, mem, 145_000, 32_000); + + let new_treasury = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_treasury(&new_treasury); + }); + check_budget("set_treasury", cpu, mem, 120_000, 25_000); + + let (cpu, mem) = measure(&env, || { + client.pause(); + }); + check_budget("pause", cpu, mem, 118_000, 25_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.is_paused_view(); + }); + check_budget("is_paused_view", cpu, mem, 47_000, 9_500); + + let (cpu, mem) = measure(&env, || { + client.unpause(); + }); + check_budget("unpause", cpu, mem, 120_000, 25_000); + + let (cpu, mem) = measure(&env, || { + client.migrate(&CONTRACT_VERSION); + }); + check_budget("migrate (idempotent)", cpu, mem, 90_000, 18_000); + + // Parameterized scaling: release by recipient count (1, 5, 10, 20) in isolated Env per N + for &(n, base_cpu, base_mem) in &[ + (1u32, 550_000u64, 95_000u64), + (5u32, 1_450_000u64, 240_000u64), + (10u32, 2_750_000u64, 460_000u64), + (20u32, 5_500_000u64, 920_000u64), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(EscrowContract, ()); + let s_client = EscrowContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &500u32, &None, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + let s_sponsor = Address::generate(&s_env); + s_asset.mint(&s_sponsor, &10_000_000i128); + s_client.fund(&1u64, &s_sponsor, &s_tok, &1_000_000i128, &50_000u64, &None); + let recipients = build_recipients(&s_env, n); + let label = std::format!("release (recipients={n})"); + let (cpu, mem) = measure(&s_env, || { + s_client.release(&1u64, &recipients); + }); + check_budget(&label, cpu, mem, base_cpu, base_mem); + } + + // Parameterized scaling: refund by contributor count (1, 5, 10, 20) in isolated Env per N + for &(n, base_cpu, base_mem) in &[ + (1u32, 420_000u64, 70_000u64), + (5u32, 1_350_000u64, 225_000u64), + (10u32, 2_600_000u64, 435_000u64), + (20u32, 5_200_000u64, 880_000u64), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(EscrowContract, ()); + let s_client = EscrowContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &500u32, &None, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + let first_sponsor = Address::generate(&s_env); + s_asset.mint(&first_sponsor, &100_000i128); + s_client.fund( + &1u64, + &first_sponsor, + &s_tok, + &100_000i128, + &50_000u64, + &None, + ); + for _ in 1..n { + let s = Address::generate(&s_env); + s_asset.mint(&s, &100_000i128); + s_client.contribute(&1u64, &s, &100_000i128); + } + let label = std::format!("refund (contributors={n})"); + let (cpu, mem) = measure(&s_env, || { + s_client.refund(&1u64); + }); + check_budget(&label, cpu, mem, base_cpu, base_mem); + } + + // Permissionless refund after deadline + grace period + env.cost_estimate().disable_resource_limits(); + client.fund( + &300u64, + &sponsor_a, + &token_addr, + &100_000i128, + &1_000u64, + &None, + ); + env.ledger().set_timestamp(1_000 + GRACE_PERIOD + 1); + let (cpu, mem) = measure(&env, || { + client.refund(&300u64); + }); + check_budget( + "refund (permissionless after deadline)", + cpu, + mem, + 450_000, + 80_000, + ); + + // Upgrade + let wasm_hash = native_wasm_hash(&env); + let (cpu, mem) = measure(&env, || { + client.upgrade(&wasm_hash); + }); + check_budget("upgrade", cpu, mem, 160_000, 35_000); +} diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index d5c83c5b..75a9fd4e 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -11,6 +11,8 @@ mod error; mod types; +#[cfg(test)] +mod bench; #[cfg(test)] mod test; diff --git a/contracts/maintenance-pool/src/bench.rs b/contracts/maintenance-pool/src/bench.rs new file mode 100644 index 00000000..fc03b213 --- /dev/null +++ b/contracts/maintenance-pool/src/bench.rs @@ -0,0 +1,347 @@ +#![cfg(test)] + +extern crate std; + +use super::*; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + token, Address, BytesN, Env, Vec, +}; + +const SOROBAN_MAX_CPU_INSTRUCTIONS: u64 = 100_000_000; +const SOROBAN_MAX_MEMORY_BYTES: u64 = 41_943_040; +const REGRESSION_TOLERANCE_BPS: u64 = 15_000; // 150% of baseline + +fn create_token<'a>( + env: &Env, + admin: &Address, +) -> (Address, token::StellarAssetClient<'a>, token::Client<'a>) { + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + let address = sac.address(); + ( + address.clone(), + token::StellarAssetClient::new(env, &address), + token::Client::new(env, &address), + ) +} + +fn native_wasm_hash(env: &Env) -> BytesN<32> { + env.crypto().sha256(&soroban_sdk::Bytes::new(env)).into() +} + +fn measure(env: &Env, f: impl FnOnce()) -> (u64, u64) { + env.cost_estimate().budget().reset_default(); + f(); + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + (cpu, mem) +} + +fn check_budget(label: &str, cpu: u64, mem: u64, baseline_cpu: u64, baseline_mem: u64) { + let max_cpu = (baseline_cpu * REGRESSION_TOLERANCE_BPS) / 10_000; + let max_mem = (baseline_mem * REGRESSION_TOLERANCE_BPS) / 10_000; + std::println!( + "| {:<46} | {:>12} | {:>12} | {:>12} | {:>12} |", + label, + cpu, + mem, + max_cpu, + max_mem + ); + assert!(cpu > 0, "{label}: measured CPU instructions must be > 0"); + assert!(mem > 0, "{label}: measured memory bytes must be > 0"); + assert!( + cpu <= max_cpu && cpu <= SOROBAN_MAX_CPU_INSTRUCTIONS, + "{label}: CPU instruction regression: measured {cpu} > ceiling {max_cpu} (baseline {baseline_cpu})" + ); + assert!( + mem <= max_mem && mem <= SOROBAN_MAX_MEMORY_BYTES, + "{label}: memory byte regression: measured {mem} > ceiling {max_mem} (baseline {baseline_mem})" + ); +} + +fn build_recipients(env: &Env, count: u32) -> Vec<(Address, u32)> { + let mut recipients = Vec::new(env); + let base_share = 10_000u32 / count; + let remainder = 10_000u32 % count; + for i in 0..count { + let addr = Address::generate(env); + let share = if i == 0 { + base_share + remainder + } else { + base_share + }; + recipients.push_back((addr, share)); + } + recipients +} + +#[test] +fn bench_maintenance_pool_entrypoints_and_scaling() { + std::println!("\n=== mergefi-maintenance-pool Resource Budget Benchmark ==="); + std::println!( + "| {:<46} | {:>12} | {:>12} | {:>12} | {:>12} |", + "Entrypoint / Scenario", + "CPU Instrs", + "Mem Bytes", + "CPU Ceiling", + "Mem Ceiling" + ); + std::println!( + "|{:-<48}|{:-<14}|{:-<14}|{:-<14}|{:-<14}|", + "", + "", + "", + "", + "" + ); + + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().disable_resource_limits(); + + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let treasury = Address::generate(&env); + let recovery = Address::generate(&env); + let contract_id = env.register(MaintenancePoolContract, ()); + let client = MaintenancePoolContractClient::new(&env, &contract_id); + + let (cpu, mem) = measure(&env, || { + client.initialize( + &admin, + &oracle, + &treasury, + &1_000u32, + &Some(recovery.clone()), + ); + }); + check_budget("initialize", cpu, mem, 95_000, 12_000); + + let token_admin = Address::generate(&env); + let (token_addr, asset_client, token_client) = create_token(&env, &token_admin); + let sponsor_a = Address::generate(&env); + asset_client.mint(&sponsor_a, &1_000_000_000i128); + + let (cpu, mem) = measure(&env, || { + client.deposit(&1u64, &sponsor_a, &token_addr, &1_000_000i128); + }); + check_budget("deposit (initial pool creation)", cpu, mem, 480_000, 75_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_pool(&1u64); + }); + check_budget("get_pool", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_deposit(&1u64, &0u32); + }); + check_budget("get_deposit", cpu, mem, 55_000, 9_500); + + token_client.transfer(&sponsor_a, &contract_id, &500i128); + let sweep_recipient = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + let _ = client.sweep(&1u64, &token_addr, &sweep_recipient); + }); + check_budget("sweep", cpu, mem, 480_000, 80_000); + + // Admin & view entrypoints + let (cpu, mem) = measure(&env, || { + let _ = client.get_admin(); + }); + check_budget("get_admin", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_oracle(); + }); + check_budget("get_oracle", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_treasury(); + }); + check_budget("get_treasury", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_fee_bps(); + }); + check_budget("get_fee_bps", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_version(); + }); + check_budget("get_version", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_bps_denominator(); + }); + check_budget("get_bps_denominator", cpu, mem, 35_000, 7_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_max_deposits(); + }); + check_budget("get_max_deposits", cpu, mem, 35_000, 7_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_inactivity_window(); + }); + check_budget("get_inactivity_window", cpu, mem, 35_000, 7_500); + + let new_admin = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_admin(&new_admin); + }); + check_budget("set_admin", cpu, mem, 145_000, 32_000); + + let (cpu, mem) = measure(&env, || { + client.recover_admin(&admin); + }); + check_budget("recover_admin", cpu, mem, 145_000, 32_000); + + let new_oracle = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_oracle(&new_oracle); + }); + check_budget("set_oracle", cpu, mem, 150_000, 35_000); + + let new_treasury = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_treasury(&new_treasury); + }); + check_budget("set_treasury", cpu, mem, 125_000, 28_000); + + let (cpu, mem) = measure(&env, || { + client.pause(); + }); + check_budget("pause", cpu, mem, 125_000, 28_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.is_paused_view(); + }); + check_budget("is_paused_view", cpu, mem, 50_000, 10_000); + + let (cpu, mem) = measure(&env, || { + client.unpause(); + }); + check_budget("unpause", cpu, mem, 125_000, 28_000); + + let (cpu, mem) = measure(&env, || { + client.migrate(&CONTRACT_VERSION); + }); + check_budget("migrate (idempotent)", cpu, mem, 95_000, 20_000); + + // Parameterized scaling: deposit, keep_alive, withdraw (1 recipient), reclaim_deposit by deposit_count (1, 10, 25, 50) + for &(dep_count, dep_cpu, dep_mem, ka_cpu, ka_mem, wd_cpu, wd_mem, rec_cpu, rec_mem) in &[ + ( + 1u32, 430_000u64, 66_000u64, 60_000u64, 8_500u64, 500_000u64, 70_000u64, 340_000u64, + 48_000u64, + ), + ( + 10u32, 830_000u64, 125_000u64, 165_000u64, 18_000u64, 730_000u64, 120_000u64, + 590_000u64, 86_000u64, + ), + ( + 25u32, + 1_550_000u64, + 230_000u64, + 410_000u64, + 38_000u64, + 1_150_000u64, + 210_000u64, + 1_100_000u64, + 155_000u64, + ), + ( + 50u32, + 2_950_000u64, + 410_000u64, + 1_000_000u64, + 85_000u64, + 1_960_000u64, + 375_000u64, + 2_120_000u64, + 285_000u64, + ), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + s_env.ledger().set_timestamp(100); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(MaintenancePoolContract, ()); + let s_client = MaintenancePoolContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &1_000u32, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + for _ in 1..dep_count { + let sp = Address::generate(&s_env); + s_asset.mint(&sp, &100_000i128); + s_client.deposit(&1u64, &sp, &s_tok, &100_000i128); + } + let nth_sponsor = Address::generate(&s_env); + s_asset.mint(&nth_sponsor, &100_000i128); + let dep_label = std::format!("deposit (deposit_count={dep_count})"); + let (cpu, mem) = measure(&s_env, || { + s_client.deposit(&1u64, &nth_sponsor, &s_tok, &100_000i128); + }); + check_budget(&dep_label, cpu, mem, dep_cpu, dep_mem); + + let ka_label = std::format!("keep_alive (deposit_count={dep_count})"); + let (cpu, mem) = measure(&s_env, || { + s_client.keep_alive(&1u64); + }); + check_budget(&ka_label, cpu, mem, ka_cpu, ka_mem); + + let recipients = build_recipients(&s_env, 1); + let wd_label = std::format!("withdraw (deposit_count={dep_count}, recipients=1)"); + let (cpu, mem) = measure(&s_env, || { + s_client.withdraw(&1u64, &recipients, &10_000i128); + }); + check_budget(&wd_label, cpu, mem, wd_cpu, wd_mem); + + s_env.ledger().set_timestamp(100 + INACTIVITY_WINDOW + 1); + let rec_label = std::format!("reclaim_deposit (deposit_count={dep_count})"); + let (cpu, mem) = measure(&s_env, || { + s_client.reclaim_deposit(&1u64, &(dep_count - 1), &nth_sponsor); + }); + check_budget(&rec_label, cpu, mem, rec_cpu, rec_mem); + } + + // Parameterized scaling: withdraw by recipient count (1, 5, 10, 20) + for &(n, base_cpu, base_mem) in &[ + (1u32, 650_000u64, 110_000u64), + (5u32, 1_550_000u64, 260_000u64), + (10u32, 2_850_000u64, 480_000u64), + (20u32, 5_600_000u64, 950_000u64), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + s_env.ledger().set_timestamp(200); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(MaintenancePoolContract, ()); + let s_client = MaintenancePoolContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &1_000u32, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + let s_sponsor = Address::generate(&s_env); + s_asset.mint(&s_sponsor, &10_000_000i128); + s_client.deposit(&1u64, &s_sponsor, &s_tok, &10_000_000i128); + let recipients = build_recipients(&s_env, n); + let label = std::format!("withdraw (recipients={n})"); + let (cpu, mem) = measure(&s_env, || { + s_client.withdraw(&1u64, &recipients, &1_000_000i128); + }); + check_budget(&label, cpu, mem, base_cpu, base_mem); + } + + // Upgrade + let wasm_hash = native_wasm_hash(&env); + let (cpu, mem) = measure(&env, || { + client.upgrade(&wasm_hash); + }); + check_budget("upgrade", cpu, mem, 160_000, 35_000); +} diff --git a/contracts/maintenance-pool/src/lib.rs b/contracts/maintenance-pool/src/lib.rs index a17cbe54..d2a5ad97 100644 --- a/contracts/maintenance-pool/src/lib.rs +++ b/contracts/maintenance-pool/src/lib.rs @@ -12,6 +12,8 @@ mod error; mod types; +#[cfg(test)] +mod bench; #[cfg(test)] mod test; diff --git a/contracts/milestones/src/bench.rs b/contracts/milestones/src/bench.rs new file mode 100644 index 00000000..4de29df5 --- /dev/null +++ b/contracts/milestones/src/bench.rs @@ -0,0 +1,387 @@ +#![cfg(test)] + +extern crate std; + +use super::*; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + token, Address, BytesN, Env, Vec, +}; + +const SOROBAN_MAX_CPU_INSTRUCTIONS: u64 = 100_000_000; +const SOROBAN_MAX_MEMORY_BYTES: u64 = 41_943_040; +const REGRESSION_TOLERANCE_BPS: u64 = 15_000; // 150% of baseline + +fn create_token<'a>( + env: &Env, + admin: &Address, +) -> (Address, token::StellarAssetClient<'a>, token::Client<'a>) { + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + let address = sac.address(); + ( + address.clone(), + token::StellarAssetClient::new(env, &address), + token::Client::new(env, &address), + ) +} + +fn native_wasm_hash(env: &Env) -> BytesN<32> { + env.crypto().sha256(&soroban_sdk::Bytes::new(env)).into() +} + +fn measure(env: &Env, f: impl FnOnce()) -> (u64, u64) { + env.cost_estimate().budget().reset_default(); + f(); + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + (cpu, mem) +} + +fn check_budget(label: &str, cpu: u64, mem: u64, baseline_cpu: u64, baseline_mem: u64) { + let max_cpu = (baseline_cpu * REGRESSION_TOLERANCE_BPS) / 10_000; + let max_mem = (baseline_mem * REGRESSION_TOLERANCE_BPS) / 10_000; + std::println!( + "| {:<46} | {:>12} | {:>12} | {:>12} | {:>12} |", + label, + cpu, + mem, + max_cpu, + max_mem + ); + assert!(cpu > 0, "{label}: measured CPU instructions must be > 0"); + assert!(mem > 0, "{label}: measured memory bytes must be > 0"); + assert!( + cpu <= max_cpu && cpu <= SOROBAN_MAX_CPU_INSTRUCTIONS, + "{label}: CPU instruction regression: measured {cpu} > ceiling {max_cpu} (baseline {baseline_cpu})" + ); + assert!( + mem <= max_mem && mem <= SOROBAN_MAX_MEMORY_BYTES, + "{label}: memory byte regression: measured {mem} > ceiling {max_mem} (baseline {baseline_mem})" + ); +} + +fn build_recipients(env: &Env, count: u32) -> Vec<(Address, u32)> { + let mut recipients = Vec::new(env); + let base_share = 10_000u32 / count; + let remainder = 10_000u32 % count; + for i in 0..count { + let addr = Address::generate(env); + let share = if i == 0 { + base_share + remainder + } else { + base_share + }; + recipients.push_back((addr, share)); + } + recipients +} + +#[test] +fn bench_milestones_entrypoints_and_scaling() { + std::println!("\n=== mergefi-milestones Resource Budget Benchmark ==="); + std::println!( + "| {:<46} | {:>12} | {:>12} | {:>12} | {:>12} |", + "Entrypoint / Scenario", + "CPU Instrs", + "Mem Bytes", + "CPU Ceiling", + "Mem Ceiling" + ); + std::println!( + "|{:-<48}|{:-<14}|{:-<14}|{:-<14}|{:-<14}|", + "", + "", + "", + "", + "" + ); + + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().disable_resource_limits(); + + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let treasury = Address::generate(&env); + let recovery = Address::generate(&env); + let contract_id = env.register(MilestonesContract, ()); + let client = MilestonesContractClient::new(&env, &contract_id); + + let (cpu, mem) = measure(&env, || { + client.initialize( + &admin, + &oracle, + &treasury, + &500u32, + &None, + &Some(recovery.clone()), + ); + }); + check_budget("initialize", cpu, mem, 95_000, 12_000); + + let token_admin = Address::generate(&env); + let (token_addr, asset_client, _) = create_token(&env, &token_admin); + let sponsor_a = Address::generate(&env); + let sponsor_b = Address::generate(&env); + asset_client.mint(&sponsor_a, &1_000_000_000i128); + asset_client.mint(&sponsor_b, &1_000_000_000i128); + + let (cpu, mem) = measure(&env, || { + client.create_milestone(&1u64, &sponsor_a, &token_addr, &10_000_000i128, &50_000u64); + }); + check_budget("create_milestone", cpu, mem, 480_000, 75_000); + + let (cpu, mem) = measure(&env, || { + client.contribute(&1u64, &sponsor_b, &5_000_000i128); + }); + check_budget("contribute", cpu, mem, 520_000, 80_000); + + let (cpu, mem) = measure(&env, || { + client.allocate(&1u64, &10u64, &1_000_000i128); + }); + check_budget("allocate (initial)", cpu, mem, 250_000, 45_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_milestone(&1u64); + }); + check_budget("get_milestone", cpu, mem, 60_000, 10_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_issue_status(&1u64, &10u64); + }); + check_budget("get_issue_status", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_contribution(&1u64, &0u32); + }); + check_budget("get_contribution", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_contributions(&1u64); + }); + check_budget("get_contributions (2 sponsors)", cpu, mem, 95_000, 14_000); + + let (cpu, mem) = measure(&env, || { + client.deallocate(&1u64, &10u64); + }); + check_budget("deallocate", cpu, mem, 250_000, 45_000); + + client.allocate(&1u64, &10u64, &1_000_000i128); + let (cpu, mem) = measure(&env, || { + client.keep_alive(&1u64); + }); + check_budget( + "keep_alive (2 sponsors, 1 issue)", + cpu, + mem, + 100_000, + 15_000, + ); + + // Admin & view entrypoints + let (cpu, mem) = measure(&env, || { + let _ = client.get_admin(); + }); + check_budget("get_admin", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_oracle(); + }); + check_budget("get_oracle", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_treasury(); + }); + check_budget("get_treasury", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_fee_bps(); + }); + check_budget("get_fee_bps", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_version(); + }); + check_budget("get_version", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_max_sponsors(); + }); + check_budget("get_max_sponsors", cpu, mem, 55_000, 9_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_grace_period(); + }); + check_budget("get_grace_period", cpu, mem, 35_000, 7_500); + + let (cpu, mem) = measure(&env, || { + let _ = client.get_bps_denominator(); + }); + check_budget("get_bps_denominator", cpu, mem, 35_000, 7_500); + + let (cpu, mem) = measure(&env, || { + client.set_fee_bps(&600u32); + }); + check_budget("set_fee_bps", cpu, mem, 130_000, 25_000); + client.set_fee_bps(&500u32); + + let new_admin = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_admin(&new_admin); + }); + check_budget("set_admin", cpu, mem, 145_000, 32_000); + + let (cpu, mem) = measure(&env, || { + client.recover_admin(&admin); + }); + check_budget("recover_admin", cpu, mem, 145_000, 32_000); + + let new_oracle = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_oracle(&new_oracle); + }); + check_budget("set_oracle", cpu, mem, 150_000, 35_000); + + let new_treasury = Address::generate(&env); + let (cpu, mem) = measure(&env, || { + client.set_treasury(&new_treasury); + }); + check_budget("set_treasury", cpu, mem, 125_000, 28_000); + + let (cpu, mem) = measure(&env, || { + client.pause(); + }); + check_budget("pause", cpu, mem, 125_000, 28_000); + + let (cpu, mem) = measure(&env, || { + let _ = client.is_paused_view(); + }); + check_budget("is_paused_view", cpu, mem, 50_000, 10_000); + + let (cpu, mem) = measure(&env, || { + client.unpause(); + }); + check_budget("unpause", cpu, mem, 125_000, 28_000); + + let (cpu, mem) = measure(&env, || { + client.migrate(&CONTRACT_VERSION); + }); + check_budget("migrate (idempotent)", cpu, mem, 95_000, 20_000); + + // Parameterized scaling: allocate & release_issue by prior allocation count (1, 10, 25, 50) + for &(alloc_count, alloc_cpu, alloc_mem, rel_cpu, rel_mem) in &[ + (1u64, 250_000u64, 45_000u64, 650_000u64, 110_000u64), + (10u64, 350_000u64, 65_000u64, 750_000u64, 130_000u64), + (25u64, 550_000u64, 105_000u64, 950_000u64, 165_000u64), + (50u64, 950_000u64, 180_000u64, 1_350_000u64, 240_000u64), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(MilestonesContract, ()); + let s_client = MilestonesContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &500u32, &None, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + let s_sponsor = Address::generate(&s_env); + s_asset.mint(&s_sponsor, &100_000_000i128); + s_client.create_milestone(&1u64, &s_sponsor, &s_tok, &100_000_000i128, &50_000u64); + for idx in 1..alloc_count { + s_client.allocate(&1u64, &idx, &10_000i128); + } + let alloc_label = std::format!("allocate (allocations={alloc_count})"); + let (cpu, mem) = measure(&s_env, || { + s_client.allocate(&1u64, &alloc_count, &10_000i128); + }); + check_budget(&alloc_label, cpu, mem, alloc_cpu, alloc_mem); + + let recipients = build_recipients(&s_env, 1); + let rel_label = std::format!("release_issue (allocations={alloc_count}, recipients=1)"); + let (cpu, mem) = measure(&s_env, || { + s_client.release_issue(&1u64, &alloc_count, &recipients); + }); + check_budget(&rel_label, cpu, mem, rel_cpu, rel_mem); + } + + // Parameterized scaling: release_issue by recipient count (1, 5, 10, 20) + for &(n, base_cpu, base_mem) in &[ + (1u32, 650_000u64, 110_000u64), + (5u32, 1_550_000u64, 260_000u64), + (10u32, 2_850_000u64, 480_000u64), + (20u32, 5_600_000u64, 950_000u64), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(MilestonesContract, ()); + let s_client = MilestonesContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &500u32, &None, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + let s_sponsor = Address::generate(&s_env); + s_asset.mint(&s_sponsor, &10_000_000i128); + s_client.create_milestone(&1u64, &s_sponsor, &s_tok, &10_000_000i128, &50_000u64); + s_client.allocate(&1u64, &1u64, &1_000_000i128); + let recipients = build_recipients(&s_env, n); + let label = std::format!("release_issue (recipients={n})"); + let (cpu, mem) = measure(&s_env, || { + s_client.release_issue(&1u64, &1u64, &recipients); + }); + check_budget(&label, cpu, mem, base_cpu, base_mem); + } + + // Parameterized scaling: cancel_milestone by contributor count (1, 5, 10, 20) + for &(n, base_cpu, base_mem) in &[ + (1u32, 450_000u64, 75_000u64), + (5u32, 1_400_000u64, 235_000u64), + (10u32, 2_650_000u64, 445_000u64), + (20u32, 5_300_000u64, 900_000u64), + ] { + let s_env = Env::default(); + s_env.mock_all_auths(); + s_env.cost_estimate().disable_resource_limits(); + let s_admin = Address::generate(&s_env); + let s_oracle = Address::generate(&s_env); + let s_treasury = Address::generate(&s_env); + let s_cid = s_env.register(MilestonesContract, ()); + let s_client = MilestonesContractClient::new(&s_env, &s_cid); + s_client.initialize(&s_admin, &s_oracle, &s_treasury, &500u32, &None, &None); + let s_tok_admin = Address::generate(&s_env); + let (s_tok, s_asset, _) = create_token(&s_env, &s_tok_admin); + let first_sponsor = Address::generate(&s_env); + s_asset.mint(&first_sponsor, &100_000i128); + s_client.create_milestone(&1u64, &first_sponsor, &s_tok, &100_000i128, &50_000u64); + for _ in 1..n { + let s = Address::generate(&s_env); + s_asset.mint(&s, &100_000i128); + s_client.contribute(&1u64, &s, &100_000i128); + } + let label = std::format!("cancel_milestone (contributors={n})"); + let (cpu, mem) = measure(&s_env, || { + s_client.cancel_milestone(&1u64); + }); + check_budget(&label, cpu, mem, base_cpu, base_mem); + } + + // Permissionless cancel_milestone_after_deadline + env.cost_estimate().disable_resource_limits(); + client.create_milestone(&400u64, &sponsor_a, &token_addr, &100_000i128, &1_000u64); + env.ledger().set_timestamp(1_000 + GRACE_PERIOD + 1); + let (cpu, mem) = measure(&env, || { + client.cancel_milestone_after_deadline(&400u64); + }); + check_budget("cancel_milestone_after_deadline", cpu, mem, 480_000, 85_000); + + // Upgrade + let wasm_hash = native_wasm_hash(&env); + let (cpu, mem) = measure(&env, || { + client.upgrade(&wasm_hash); + }); + check_budget("upgrade", cpu, mem, 160_000, 35_000); +} diff --git a/contracts/milestones/src/lib.rs b/contracts/milestones/src/lib.rs index 5e3d8347..af07da90 100644 --- a/contracts/milestones/src/lib.rs +++ b/contracts/milestones/src/lib.rs @@ -15,6 +15,8 @@ mod error; mod types; +#[cfg(test)] +mod bench; #[cfg(test)] mod test;