Skip to content
Merged
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/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
${{ runner.os }}-cargo-

- name: Check WASM size
shell: bash
run: |
chmod +x scripts/check-wasm-size.sh
./scripts/check-wasm-size.sh
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,25 @@ Advanced settlement with individual developer balance tracking.
2. **Build and test:**

```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo build
cargo test
```

3. **Build WASM:**

```bash
# Build all contracts
cargo build --target wasm32-unknown-unknown --release

# Or use the convenience script
# Build all publishable contract crates and verify their release WASM sizes
./scripts/check-wasm-size.sh

# Or build a specific contract manually
cargo build --target wasm32-unknown-unknown --release -p callora-vault
```

## Development

Use one branch per issue or feature. Run `cargo fmt`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test` before pushing.
Use one branch per issue or feature. Run `cargo fmt --all`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test`, and `./scripts/check-wasm-size.sh` before pushing so every publishable contract stays within Soroban's WASM size limit.

### Test coverage

Expand Down
87 changes: 86 additions & 1 deletion contracts/revenue_pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::*;
use soroban_sdk::testutils::{Address as _, Events as _};
use soroban_sdk::token;
use soroban_sdk::TryFromVal;
use soroban_sdk::{Address, Env, Symbol, Vec};
use soroban_sdk::{Address, Env, IntoVal, Symbol, Vec};

fn create_usdc<'a>(
env: &'a Env,
Expand Down Expand Up @@ -266,3 +266,88 @@ fn batch_distribute_success_events() {
}
}
}

#[test]
fn receive_payment_emits_event_for_admin() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc, _, _) = create_usdc(&env, &admin);

client.init(&admin, &usdc);
client.receive_payment(&admin, &250, &true);

let events = env.events().all();
let receive_event = events.last().unwrap();
let event_name = Symbol::try_from_val(&env, &receive_event.1.get(0).unwrap()).unwrap();
assert_eq!(event_name, Symbol::new(&env, "receive_payment"));

let caller: Address = Address::try_from_val(&env, &receive_event.1.get(1).unwrap()).unwrap();
assert_eq!(caller, admin);

let (amount, from_vault): (i128, bool) = receive_event.2.into_val(&env);
assert_eq!(amount, 250);
assert!(from_vault);
}

#[test]
#[should_panic(expected = "no pending admin")]
fn claim_admin_without_pending_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let candidate = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc, _, _) = create_usdc(&env, &admin);

client.init(&admin, &usdc);
client.claim_admin(&candidate);
}

#[test]
#[should_panic(expected = "unauthorized: caller is not pending admin")]
fn claim_admin_wrong_caller_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let pending_admin = Address::generate(&env);
let attacker = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc, _, _) = create_usdc(&env, &admin);

client.init(&admin, &usdc);
client.set_admin(&admin, &pending_admin);
client.claim_admin(&attacker);
}

#[test]
#[should_panic(expected = "invalid recipient: cannot distribute to the contract itself")]
fn distribute_to_self_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin);

client.init(&admin, &usdc_address);
fund_pool(&usdc_admin, &pool_addr, 100);
client.distribute(&admin, &pool_addr, &50);
}

#[test]
#[should_panic(expected = "amount must be positive")]
fn batch_distribute_zero_amount_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let dev = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc_address, _, _) = create_usdc(&env, &admin);

client.init(&admin, &usdc_address);

let mut payments: Vec<(Address, i128)> = Vec::new(&env);
payments.push_back((dev, 0));
client.batch_distribute(&admin, &payments);
}
5 changes: 3 additions & 2 deletions contracts/settlement/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ mod settlement_tests {
env.mock_all_auths();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let third_party = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
(env, addr, admin, vault, third_party)
Expand Down Expand Up @@ -152,7 +153,7 @@ mod settlement_tests {
env.mock_all_auths();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
client.receive_payment(&admin, &100i128, &true, &None);
Expand Down
10 changes: 8 additions & 2 deletions contracts/vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,10 @@ impl CalloraVault {
if let Some(s) = inst.get::<StorageKey, Address>(&StorageKey::Settlement) {
let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap();
Self::transfer_funds(&env, &ut, &s, amount);
} else if inst.get::<StorageKey, Address>(&StorageKey::RevenuePool).is_some() {
} else if inst
.get::<StorageKey, Address>(&StorageKey::RevenuePool)
.is_some()
{
Self::transfer_to_revenue_pool(env.clone(), amount);
}
let rid = request_id.unwrap_or(Symbol::new(&env, ""));
Expand Down Expand Up @@ -372,7 +375,10 @@ impl CalloraVault {
if let Some(s) = inst.get::<StorageKey, Address>(&StorageKey::Settlement) {
let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap();
Self::transfer_funds(&env, &ut, &s, total);
} else if inst.get::<StorageKey, Address>(&StorageKey::RevenuePool).is_some() {
} else if inst
.get::<StorageKey, Address>(&StorageKey::RevenuePool)
.is_some()
{
Self::transfer_to_revenue_pool(env.clone(), total);
}
meta.balance
Expand Down
124 changes: 93 additions & 31 deletions scripts/check-wasm-size.sh
Original file line number Diff line number Diff line change
@@ -1,52 +1,114 @@
#!/bin/bash
# Check that all contract WASM binaries stay under the 64KB Soroban limit.
#!/usr/bin/env bash
# Check that all publishable contract WASM binaries stay under the 64 KiB Soroban limit.

set -e
set -euo pipefail

MAX_SIZE=$((64 * 1024))
FAILED=0
MAX_SIZE_BYTES=$((64 * 1024))
TARGET_DIR="${CARGO_TARGET_DIR:-target}/wasm32-unknown-unknown/release"

contract_manifests=()
contract_packages=()
failed=0

if command -v cargo >/dev/null 2>&1; then
CARGO_BIN=$(command -v cargo)
elif command -v cargo.exe >/dev/null 2>&1; then
CARGO_BIN=$(command -v cargo.exe)
elif [ -n "${HOME:-}" ] && [ -x "${HOME}/.cargo/bin/cargo" ]; then
CARGO_BIN="${HOME}/.cargo/bin/cargo"
elif [ -n "${HOME:-}" ] && [ -x "${HOME}/.cargo/bin/cargo.exe" ]; then
CARGO_BIN="${HOME}/.cargo/bin/cargo.exe"
elif [ -n "${USERPROFILE:-}" ] && [ -x "${USERPROFILE}/.cargo/bin/cargo.exe" ]; then
CARGO_BIN="${USERPROFILE}/.cargo/bin/cargo.exe"
elif [ -n "${USERNAME:-}" ] && [ -x "/c/Users/${USERNAME}/.cargo/bin/cargo.exe" ]; then
CARGO_BIN="/c/Users/${USERNAME}/.cargo/bin/cargo.exe"
else
echo "ERROR: cargo was not found on PATH and no fallback binary was detected"
exit 1
fi

while IFS= read -r -d '' manifest; do
contract_manifests+=("$manifest")
done < <(find contracts -mindepth 2 -maxdepth 2 -name Cargo.toml -print0 | sort -z)

if [ "${#contract_manifests[@]}" -eq 0 ]; then
echo "ERROR: no contract manifests found under contracts/*/Cargo.toml"
exit 1
fi

discover_contract_packages() {
local manifest
local package_name

for manifest in "${contract_manifests[@]}"; do
if ! grep -Eq 'crate-type\s*=\s*\[[^]]*"cdylib"' "$manifest"; then
continue
fi

package_name=$(awk -F'"' '/^[[:space:]]*name[[:space:]]*=/{print $2; exit}' "$manifest")
if [ -z "$package_name" ]; then
echo "ERROR: unable to determine package name from $manifest"
exit 1
fi

contract_packages+=("$package_name")
done

if [ "${#contract_packages[@]}" -eq 0 ]; then
echo 'ERROR: no publishable contract crates with crate-type = ["cdylib", ...] were found'
exit 1
fi
}

check_wasm() {
local crate="$1"
local wasm_name="$2"
local wasm_file="target/wasm32-unknown-unknown/release/${wasm_name}.wasm"
local wasm_name="${crate//-/_}"
local wasm_file="$TARGET_DIR/${wasm_name}.wasm"
local size_bytes
local size_kib
local headroom_bytes
local headroom_kib

if [ ! -f "$wasm_file" ]; then
echo "ERROR: $wasm_file not found — did the build run?"
FAILED=1
echo "FAIL $crate: missing artifact at $wasm_file"
failed=1
return
fi

local size
size=$(wc -c < "$wasm_file")
local size_kb=$((size / 1024))
size_bytes=$(wc -c < "$wasm_file")
size_kib=$((size_bytes / 1024))

if [ "$size" -gt "$MAX_SIZE" ]; then
echo "FAIL $crate: ${size_kb}KB — exceeds 64KB limit"
FAILED=1
else
local headroom=$(( (MAX_SIZE - size) / 1024 ))
echo "OK $crate: ${size_kb}KB (${headroom}KB headroom)"
if [ "$size_bytes" -gt "$MAX_SIZE_BYTES" ]; then
echo "FAIL $crate: ${size_bytes} bytes (${size_kib} KiB) exceeds 65536-byte limit"
failed=1
return
fi

headroom_bytes=$((MAX_SIZE_BYTES - size_bytes))
headroom_kib=$((headroom_bytes / 1024))
echo "OK $crate: ${size_bytes} bytes (${size_kib} KiB, ${headroom_bytes} bytes / ${headroom_kib} KiB headroom)"
}

echo "Building all contracts for wasm32-unknown-unknown (release)..."
cargo build --target wasm32-unknown-unknown --release \
-p callora-vault \
-p callora-revenue-pool \
-p callora-settlement
discover_contract_packages

echo "Building publishable contracts for wasm32-unknown-unknown (release)..."
cargo_args=(build --target wasm32-unknown-unknown --release)
for crate in "${contract_packages[@]}"; do
cargo_args+=(-p "$crate")
done
"$CARGO_BIN" "${cargo_args[@]}"

echo ""
echo "WASM size check (limit: 64KB)"
echo "------------------------------"
check_wasm "callora-vault" "callora_vault"
check_wasm "callora-revenue-pool" "callora_revenue_pool"
check_wasm "callora-settlement" "callora_settlement"
echo "WASM size check (limit: 65536 bytes / 64 KiB)"
echo "---------------------------------------------"
for crate in "${contract_packages[@]}"; do
check_wasm "$crate"
done
echo ""

if [ $FAILED -ne 0 ]; then
echo "One or more contracts exceed the size limit."
if [ "$failed" -ne 0 ]; then
echo "One or more publishable contract WASM artifacts are missing or exceed the Soroban size limit."
exit 1
fi

echo "All contracts within size limit."
echo "All publishable contract WASM artifacts are within the Soroban size limit."
Loading