diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bc7db47a..15a4ae2c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -406,6 +406,43 @@ jobs:
working-directory: ./contracts
run: cargo build --release
+ rust-gas-benchmark:
+ name: Soroban Gas Profiling & Regression Gate
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@master
+ with:
+ toolchain: ${{ env.RUST_VERSION }}
+
+ - name: Cache Rust dependencies
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: './contracts -> target'
+
+ - name: Run Gas Benchmarks & Profiling
+ run: |
+ if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
+ echo "Main branch update: updating baseline gas snapshot..."
+ ./scripts/gas-benchmark.sh --generate-baseline
+
+ # Commit and push updated baseline & trends back to main branch
+ git config --global user.name "rindicomfort"
+ git config --global user.email "kwarpojonathanrindi@gmail.com"
+ git add gas-benchmarks/
+ if git commit -m "chore: update baseline gas snapshot and trends [skip ci]"; then
+ git push
+ fi
+ else
+ echo "PR / Dev branch: running gas regression check..."
+ ./scripts/gas-benchmark.sh
+ fi
+
# ─────────────────────────────────────────────────────────
# Load Testing
# ─────────────────────────────────────────────────────────
@@ -559,6 +596,7 @@ jobs:
rust-clippy,
rust-tests,
rust-build,
+ rust-gas-benchmark,
load-test,
bundle-size,
performance,
@@ -587,6 +625,7 @@ jobs:
rust-clippy,
rust-tests,
rust-build,
+ rust-gas-benchmark,
load-test,
bundle-size,
performance,
@@ -608,6 +647,7 @@ jobs:
[ "${{ needs.rust-clippy.result }}" != "success" ] || \
[ "${{ needs.rust-tests.result }}" != "success" ] || \
[ "${{ needs.rust-build.result }}" != "success" ] || \
+ [ "${{ needs.rust-gas-benchmark.result }}" != "success" ] || \
[ "${{ needs.load-test.result }}" != "success" ] || \
[ "${{ needs.bundle-size.result }}" != "success" ] || \
[ "${{ needs.performance.result }}" != "success" ]; then
diff --git a/contracts/proxy/tests/integration_soroban.rs b/contracts/proxy/tests/integration_soroban.rs
index 908442d7..cdb7c931 100644
--- a/contracts/proxy/tests/integration_soroban.rs
+++ b/contracts/proxy/tests/integration_soroban.rs
@@ -1,6 +1,6 @@
use soroban_sdk::{
contract, contractimpl,
- testutils::{Address as _, Ledger},
+ testutils::{Address as _, Ledger, Env as _},
token, Address, Env, String,
};
use subtrackr_proxy::{UpgradeableProxy, UpgradeableProxyClient};
@@ -297,3 +297,329 @@ fn integration_lowering_plan_limit_does_not_affect_existing_plans() {
);
assert!(res.is_err());
}
+
+fn setup_client_helper(env: &Env) -> (UpgradeableProxyClient<'_>, Address, Address, Address, Address) {
+ env.mock_all_auths_allowing_non_root_auth();
+
+ let admin = Address::generate(env);
+ let merchant = Address::generate(env);
+ let subscriber = Address::generate(env);
+ let token_admin = Address::generate(env);
+
+ let storage_id = env.register_contract(None, SubTrackrStorage);
+ let implementation_id = env.register_contract(None, SubTrackrSubscription);
+
+ let proxy_id = env.register_contract(None, UpgradeableProxy);
+ let proxy = UpgradeableProxyClient::new(env, &proxy_id);
+ proxy.initialize(&admin, &storage_id, &implementation_id, &0u64, &0u64);
+
+ let token_id = env.register_stellar_asset_contract_v2(token_admin);
+
+ (proxy, admin, merchant, subscriber, token_id.address())
+}
+
+#[test]
+fn test_gas_benchmarks() {
+ // We will run each benchmarked function and print the cost.
+ // 1. initialize
+ {
+ let env = Env::default();
+ env.mock_all_auths_allowing_non_root_auth();
+ let admin = Address::generate(&env);
+ let storage_id = env.register_contract(None, SubTrackrStorage);
+ let implementation_id = env.register_contract(None, SubTrackrSubscription);
+ let proxy_id = env.register_contract(None, UpgradeableProxy);
+ let proxy = UpgradeableProxyClient::new(&env, &proxy_id);
+
+ env.enable_invocation_metering();
+ proxy.initialize(&admin, &storage_id, &implementation_id, &0u64, &0u64);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:initialize:{:?}", resources);
+ }
+
+ // Helper to get initialized contract client
+ let setup_client = setup_client_helper;
+
+ // 2. create_plan
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, _subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+
+ env.enable_invocation_metering();
+ let _plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:create_plan:{:?}", resources);
+ }
+
+ // 3. deactivate_plan
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, _subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+
+ env.enable_invocation_metering();
+ client.deactivate_plan(&merchant, &plan_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:deactivate_plan:{:?}", resources);
+ }
+
+ // 4. subscribe
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+
+ env.enable_invocation_metering();
+ let _sub_id = client.subscribe(&subscriber, &plan_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:subscribe:{:?}", resources);
+ }
+
+ // 5. cancel_subscription
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ env.enable_invocation_metering();
+ client.cancel_subscription(&subscriber, &sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:cancel_subscription:{:?}", resources);
+ }
+
+ // 6. pause_subscription
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ env.enable_invocation_metering();
+ client.pause_subscription(&subscriber, &sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:pause_subscription:{:?}", resources);
+ }
+
+ // 7. pause_by_subscriber
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ env.enable_invocation_metering();
+ client.pause_by_subscriber(&subscriber, &sub_id, &1000_u64);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:pause_by_subscriber:{:?}", resources);
+ }
+
+ // 8. resume_subscription
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+ client.pause_subscription(&subscriber, &sub_id);
+
+ env.enable_invocation_metering();
+ client.resume_subscription(&subscriber, &sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:resume_subscription:{:?}", resources);
+ }
+
+ // 9. charge_subscription
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ let token_admin_client = token::StellarAssetClient::new(&env, &token);
+ token_admin_client.mint(&subscriber, &1000);
+
+ env.ledger().set_timestamp(env.ledger().timestamp() + Interval::Monthly.seconds() + 10);
+
+ env.enable_invocation_metering();
+ client.charge_subscription(&sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:charge_subscription:{:?}", resources);
+ }
+
+ // 10. request_refund
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ let token_admin_client = token::StellarAssetClient::new(&env, &token);
+ token_admin_client.mint(&subscriber, &1000);
+
+ env.ledger().set_timestamp(env.ledger().timestamp() + Interval::Monthly.seconds() + 10);
+ client.charge_subscription(&sub_id);
+
+ env.enable_invocation_metering();
+ client.request_refund(&sub_id, &50_i128);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:request_refund:{:?}", resources);
+ }
+
+ // 11. approve_refund
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ let token_admin_client = token::StellarAssetClient::new(&env, &token);
+ token_admin_client.mint(&subscriber, &1000);
+
+ env.ledger().set_timestamp(env.ledger().timestamp() + Interval::Monthly.seconds() + 10);
+ client.charge_subscription(&sub_id);
+ client.request_refund(&sub_id, &50_i128);
+
+ env.enable_invocation_metering();
+ client.approve_refund(&sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:approve_refund:{:?}", resources);
+ }
+
+ // 12. reject_refund
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ let token_admin_client = token::StellarAssetClient::new(&env, &token);
+ token_admin_client.mint(&subscriber, &1000);
+
+ env.ledger().set_timestamp(env.ledger().timestamp() + Interval::Monthly.seconds() + 10);
+ client.charge_subscription(&sub_id);
+ client.request_refund(&sub_id, &50_i128);
+
+ env.enable_invocation_metering();
+ client.reject_refund(&sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:reject_refund:{:?}", resources);
+ }
+
+ // 13. request_transfer
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+ let recipient = Address::generate(&env);
+
+ env.enable_invocation_metering();
+ client.request_transfer(&sub_id, &recipient);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:request_transfer:{:?}", resources);
+ }
+
+ // 14. accept_transfer
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+ let recipient = Address::generate(&env);
+ client.request_transfer(&sub_id, &recipient);
+
+ env.enable_invocation_metering();
+ client.accept_transfer(&sub_id, &recipient);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:accept_transfer:{:?}", resources);
+ }
+
+ // 15. get_plan
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, _subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+
+ env.enable_invocation_metering();
+ let _plan = client.get_plan(&plan_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:get_plan:{:?}", resources);
+ }
+
+ // 16. get_subscription
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let sub_id = client.subscribe(&subscriber, &plan_id);
+
+ env.enable_invocation_metering();
+ let _sub = client.get_subscription(&sub_id);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:get_subscription:{:?}", resources);
+ }
+
+ // 17. get_user_subscriptions
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+ let _sub_id = client.subscribe(&subscriber, &plan_id);
+
+ env.enable_invocation_metering();
+ let _subs = client.get_user_subscriptions(&subscriber);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:get_user_subscriptions:{:?}", resources);
+ }
+
+ // 18. get_merchant_plans
+ {
+ let env = Env::default();
+ let (client, _admin, merchant, _subscriber, token) = setup_client(&env);
+ let name = String::from_str(&env, "Standard Plan");
+ let _plan_id = client.create_plan(&merchant, &name, &100_i128, &token, &Interval::Monthly);
+
+ env.enable_invocation_metering();
+ let _plans = client.get_merchant_plans(&merchant);
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:get_merchant_plans:{:?}", resources);
+ }
+
+ // 19. get_plan_count
+ {
+ let env = Env::default();
+ let (client, _admin, _merchant, _subscriber, _token) = setup_client(&env);
+
+ env.enable_invocation_metering();
+ let _count = client.get_plan_count();
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:get_plan_count:{:?}", resources);
+ }
+
+ // 20. get_subscription_count
+ {
+ let env = Env::default();
+ let (client, _admin, _merchant, _subscriber, _token) = setup_client(&env);
+
+ env.enable_invocation_metering();
+ let _count = client.get_subscription_count();
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:get_subscription_count:{:?}", resources);
+ }
+}
diff --git a/gas-benchmarks/README.md b/gas-benchmarks/README.md
new file mode 100644
index 00000000..c3ef9e87
--- /dev/null
+++ b/gas-benchmarks/README.md
@@ -0,0 +1,77 @@
+# ⛽ Soroban Smart Contract Gas Profiling Pipeline
+
+This directory contains the profiling results, baselines, and trend charts generated by the automated gas benchmarking pipeline.
+
+## 📋 Directory Contents
+
+- `baseline.json`: The baseline gas costs snapshot. Checked in and updated on `main` branch pushes.
+- `trends.json`: Historical log of gas costs per function over commits, keeping up to the last 50 commits.
+- `gas_trend.svg`: An automatically generated line chart showing the CPU instructions consumed by top functions over time.
+- `README.md`: This documentation.
+
+---
+
+## 🚀 How to Run the Profiling Pipeline
+
+### 1. Run Benchmarks and Check for Regressions
+To run the benchmarks and verify that current gas usage does not exceed the baseline by more than the threshold (default: 10%):
+```bash
+./scripts/gas-benchmark.sh
+```
+
+### 2. Configure Regression Threshold
+You can customize the regression threshold (e.g., set to 15%) using either the `--threshold` flag or the `GAS_REGRESSION_THRESHOLD` environment variable:
+```bash
+# Via flag
+./scripts/gas-benchmark.sh --threshold 0.15
+
+# Via environment variable
+GAS_REGRESSION_THRESHOLD=0.15 ./scripts/gas-benchmark.sh
+```
+
+### 3. Generate or Update Baseline
+To overwrite the baseline with the current contract performance:
+```bash
+./scripts/gas-benchmark.sh --generate-baseline
+```
+
+---
+
+## 🛠️ How to Add New Benchmarks
+
+All gas benchmarks are defined in the integration test suite located at:
+[`contracts/tests/integration_soroban.rs`](../contracts/tests/integration_soroban.rs#L196)
+
+To add a new benchmark:
+1. Open the file and navigate to the `test_gas_benchmarks` function.
+2. Add a new code block for the function or scenario you want to test.
+3. Construct the required test setup (e.g., using `setup_client`).
+4. Enable invocation metering:
+ ```rust
+ env.enable_invocation_metering();
+ ```
+5. Invoke the contract function you wish to profile.
+6. Retrieve the invocation resources and print them to stdout using the exact prefix format:
+ ```rust
+ let resources = env.cost_estimate().resources();
+ println!("GAS_BENCHMARK:my_new_function:{:?}", resources);
+ ```
+
+The script will automatically parse this output line, record it in historical trends, compare it to the baseline, and construct a stratified call tree for it.
+
+---
+
+## 📊 Interpreting the Results
+
+### 1. CPU Instructions
+Represent the computational resource limits of the Stellar network. A transaction has a hard limit of 100M instructions on Soroban. Lowering CPU instruction counts directly reduces the network fee.
+
+### 2. Stratified Call Trees
+The pipeline outputs a stratified call tree to show where resources are allocated:
+- **WASM Execution**: Gas spent executing compiled WebAssembly bytecode on the Soroban host.
+- **Storage Reads**: Gas associated with reading entries from the ledger. (Estimated at 12,000 CPU instructions per read).
+- **Storage Writes**: Gas associated with writing data back to the ledger. (Estimated at 25,000 CPU instructions per write + 30 instructions per byte written).
+- **Host/Auth/Events**: Overhead from event publishing, authorization checks, and built-in host functions.
+
+### 3. Historical Trends
+The line chart (`gas_trend.svg`) displays performance trends over time, helping developers visualize whether optimizations are working or if new features are introducing gradual regression over multiple commits.
diff --git a/scripts/analyze-gas.py b/scripts/analyze-gas.py
new file mode 100644
index 00000000..e4090845
--- /dev/null
+++ b/scripts/analyze-gas.py
@@ -0,0 +1,253 @@
+import sys
+import os
+import re
+import json
+
+# Setup paths
+BENCHMARK_DIR = "gas-benchmarks"
+BASELINE_PATH = os.path.join(BENCHMARK_DIR, "baseline.json")
+TRENDS_PATH = os.path.join(BENCHMARK_DIR, "trends.json")
+SVG_PATH = os.path.join(BENCHMARK_DIR, "gas_trend.svg")
+
+os.makedirs(BENCHMARK_DIR, exist_ok=True)
+
+# Regex patterns
+pattern = re.compile(r'GAS_BENCHMARK:([^:]+):(.*)')
+kv_pattern = re.compile(r'(\w+):\s*(\d+)')
+
+current_results = {}
+for line in sys.stdin:
+ sys.stdout.write(line)
+ match = pattern.search(line)
+ if match:
+ func_name = match.group(1)
+ debug_str = match.group(2)
+ kvs = kv_pattern.findall(debug_str)
+ metrics = {k: int(v) for k, v in kvs}
+ if metrics:
+ current_results[func_name] = metrics
+
+if not current_results:
+ print("Error: No benchmark results parsed from test output. Make sure test_gas_benchmarks runs.", file=sys.stderr)
+ sys.exit(1)
+
+# Get commit metadata
+commit_sha = os.environ.get("COMMIT_SHA", "unknown")
+commit_time = int(os.environ.get("COMMIT_TIME", "0"))
+
+# Load or generate baseline
+baseline = {}
+if os.path.exists(BASELINE_PATH):
+ try:
+ with open(BASELINE_PATH, "r") as f:
+ baseline = json.load(f)
+ except Exception as e:
+ print(f"Warning: failed to load baseline: {e}", file=sys.stderr)
+
+generate_baseline_mode = os.environ.get("GENERATE_BASELINE") == "true"
+if generate_baseline_mode or not baseline:
+ print(f"Saving current results as new baseline to {BASELINE_PATH}...")
+ with open(BASELINE_PATH, "w") as f:
+ json.dump(current_results, f, indent=2)
+ baseline = current_results
+
+# Load and update historical trends
+trends = []
+if os.path.exists(TRENDS_PATH):
+ try:
+ with open(TRENDS_PATH, "r") as f:
+ trends = json.load(f)
+ except Exception:
+ pass
+
+trends.append({
+ "sha": commit_sha,
+ "timestamp": commit_time,
+ "results": current_results
+})
+trends = trends[-50:]
+with open(TRENDS_PATH, "w") as f:
+ json.dump(trends, f, indent=2)
+
+# Generate trend SVG
+def generate_svg(trends, svg_path):
+ funcs = list(current_results.keys())
+ funcs.sort(key=lambda fn: current_results.get(fn, {}).get("instructions", 0), reverse=True)
+ plot_funcs = funcs[:5]
+
+ width, height = 800, 400
+ padding = 60
+
+ points_by_func = {fn: [] for fn in plot_funcs}
+ shas = []
+ for t in trends[-10:]:
+ shas.append(t["sha"])
+ for fn in plot_funcs:
+ val = t.get("results", {}).get(fn, {}).get("instructions", 0)
+ points_by_func[fn].append(val)
+
+ if not shas:
+ return
+
+ max_val = max(max(vals) if vals else 0 for vals in points_by_func.values())
+ max_val = max(max_val, 1)
+
+ colors = ["#4f46e5", "#06b6d4", "#10b981", "#f59e0b", "#ef4444"]
+
+ svg = []
+ svg.append(f'')
+
+ with open(svg_path, "w") as f:
+ f.write("\n".join(svg))
+
+try:
+ generate_svg(trends, SVG_PATH)
+except Exception as e:
+ print(f"Warning: failed to generate SVG: {e}", file=sys.stderr)
+
+# Regressions analysis
+threshold = float(os.environ.get("GAS_REGRESSION_THRESHOLD", "0.10"))
+regressions = []
+summary_table = []
+call_trees = []
+
+summary_table.append("| Function | Baseline (CPU) | Current (CPU) | Change | Status |")
+summary_table.append("| --- | --- | --- | --- | --- |")
+
+for func, metrics in current_results.items():
+ current_cpu = metrics.get("instructions", 0)
+ baseline_metrics = baseline.get(func, {})
+ baseline_cpu = baseline_metrics.get("instructions", 0)
+
+ change_pct = 0.0
+ change_str = "0.0%"
+ status = "✅ Pass"
+
+ if baseline_cpu > 0:
+ change_pct = (current_cpu - baseline_cpu) / baseline_cpu
+ change_str = f"{change_pct * 100:+.2f}%"
+ if change_pct > threshold:
+ status = "⚠️ Regression"
+ regressions.append((func, baseline_cpu, current_cpu, change_pct))
+ elif change_pct < -0.01:
+ status = "⚡ Optimized"
+ else:
+ change_str = "New"
+
+ summary_table.append(f"| `{func}` | {baseline_cpu:,} | {current_cpu:,} | {change_str} | {status} |")
+
+ # Stratified Call Tree
+ mem = metrics.get("mem_bytes", 0)
+ reads = metrics.get("disk_read_entries", 0) or metrics.get("read_entries", 0) or 0
+ writes = metrics.get("write_entries", 0) or 0
+ write_b = metrics.get("write_bytes", 0) or 0
+
+ est_read_cost = reads * 12000
+ est_write_cost = writes * 25000 + write_b * 30
+ total_est_storage = est_read_cost + est_write_cost
+
+ wasm_cost = current_cpu - total_est_storage
+ min_wasm = int(current_cpu * 0.15)
+ if wasm_cost < min_wasm:
+ wasm_cost = min_wasm
+ remaining = current_cpu - wasm_cost
+ if total_est_storage > 0:
+ est_read_cost = int(est_read_cost * remaining / total_est_storage)
+ est_write_cost = int(est_write_cost * remaining / total_est_storage)
+ else:
+ wasm_cost = current_cpu
+
+ other_host = current_cpu - wasm_cost - est_read_cost - est_write_cost
+ if other_host < 0:
+ other_host = 0
+
+ p_wasm = (wasm_cost / current_cpu) * 100 if current_cpu > 0 else 0
+ p_read = (est_read_cost / current_cpu) * 100 if current_cpu > 0 else 0
+ p_write = (est_write_cost / current_cpu) * 100 if current_cpu > 0 else 0
+ p_other = (other_host / current_cpu) * 100 if current_cpu > 0 else 0
+
+ tree = f"""**`{func}`** (Total: {current_cpu:,} CPU instructions, {mem:,} Bytes RAM)
+├── **WASM Execution**: {wasm_cost:,} CPU ({p_wasm:.1f}%)
+├── **Storage Reads**: {est_read_cost:,} CPU ({p_read:.1f}%) [{reads} entry reads]
+├── **Storage Writes**: {est_write_cost:,} CPU ({p_write:.1f}%) [{writes} entry writes, {write_b} bytes]
+└── **Host/Auth/Events**: {other_host:,} CPU ({p_other:.1f}%)"""
+ call_trees.append(tree)
+
+print("\n" + "="*50)
+print(" SOROBAN GAS BENCHMARK REPORT ")
+print("="*50)
+for r in summary_table:
+ print(r)
+print("\n" + "="*50)
+print(" STRATIFIED CALL TREES ")
+print("="*50)
+for t in call_trees:
+ print(t)
+ print()
+
+step_summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
+if step_summary_file:
+ with open(step_summary_file, "w") as sf:
+ sf.write("## ⛽ Soroban Smart Contract Gas Profiling\n\n")
+
+ if regressions:
+ sf.write("### ⚠️ Gas Cost Regressions Detected!\n")
+ sf.write(f"The following functions exceeded the baseline by more than the threshold of **{threshold*100:.0f}%**:\n\n")
+ for name, base, cur, pct in regressions:
+ sf.write(f"- **`{name}`**: {base:,} -> {cur:,} (**{pct*100:+.2f}%**)\n")
+ sf.write("\n")
+ else:
+ sf.write("### ✅ All Gas Benchmarks Passed\n")
+ sf.write("No gas regressions detected against baseline.\n\n")
+
+ sf.write("### 📊 Performance Summary\n")
+ sf.write("\n".join(summary_table) + "\n\n")
+
+ sf.write("### 📈 Gas Consumption Trends\n")
+ sf.write(f"\n\n")
+
+ sf.write("### 🌳 Stratified Call Trees\n")
+ sf.write("Identifies high-cost operations per function:\n\n")
+ for t in call_trees:
+ sf.write("```text\n" + t + "\n```\n\n")
+
+if regressions:
+ print(f"\n❌ Fail: {len(regressions)} gas cost regressions detected!", file=sys.stderr)
+ sys.exit(1)
+else:
+ print("\n✅ Success: All gas benchmarks within baseline limits.")
+ sys.exit(0)
diff --git a/scripts/gas-benchmark.sh b/scripts/gas-benchmark.sh
new file mode 100755
index 00000000..aa940051
--- /dev/null
+++ b/scripts/gas-benchmark.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# scripts/gas-benchmark.sh - Run gas cost profiling and check for regressions
+
+set -euo pipefail
+
+# Root directory of workspace
+WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$WORKSPACE_DIR"
+
+# Defaults
+THRESHOLD="0.10"
+GENERATE_BASELINE="false"
+
+# Helper for usage
+show_help() {
+ echo "Usage: $0 [options]"
+ echo "Options:"
+ echo " --generate-baseline Generate/overwrite baseline gas snapshot"
+ echo " --threshold Regression threshold as a fraction (default: 0.10)"
+ echo " -h, --help Show this help message"
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --generate-baseline)
+ GENERATE_BASELINE="true"
+ shift
+ ;;
+ --threshold)
+ THRESHOLD="$2"
+ shift 2
+ ;;
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1"
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+echo "=== Running Soroban Contract Gas Benchmarks ==="
+export GAS_REGRESSION_THRESHOLD="$THRESHOLD"
+export GENERATE_BASELINE="$GENERATE_BASELINE"
+export COMMIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")"
+export COMMIT_TIME="$(git log -1 --format=%ct 2>/dev/null || date +%s)"
+
+# Run Cargo test in contracts workspace and pipe to Python analyzer
+cd "$WORKSPACE_DIR/contracts"
+cargo test --package subtrackr-proxy --test integration_soroban test_gas_benchmarks -- --nocapture | python3 "$WORKSPACE_DIR/scripts/analyze-gas.py"