Skip to content

Commit 1dc5174

Browse files
Merge pull request #216 from iammrjude/issue/138-ci-wasm-size-all-crates
ci: enforce WASM size checks for all publishable contracts
2 parents 4ff9ab6 + 4681cff commit 1dc5174

6 files changed

Lines changed: 198 additions & 41 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ jobs:
6161
${{ runner.os }}-cargo-
6262
6363
- name: Check WASM size
64+
shell: bash
6465
run: |
6566
chmod +x scripts/check-wasm-size.sh
6667
./scripts/check-wasm-size.sh

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,25 @@ Advanced settlement with individual developer balance tracking.
8383
2. **Build and test:**
8484

8585
```bash
86+
cargo fmt --all
87+
cargo clippy --all-targets --all-features -- -D warnings
8688
cargo build
8789
cargo test
8890
```
8991

9092
3. **Build WASM:**
9193

9294
```bash
93-
# Build all contracts
94-
cargo build --target wasm32-unknown-unknown --release
95-
96-
# Or use the convenience script
95+
# Build all publishable contract crates and verify their release WASM sizes
9796
./scripts/check-wasm-size.sh
97+
98+
# Or build a specific contract manually
99+
cargo build --target wasm32-unknown-unknown --release -p callora-vault
98100
```
99101

100102
## Development
101103

102-
Use one branch per issue or feature. Run `cargo fmt`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test` before pushing.
104+
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.
103105

104106
### Test coverage
105107

contracts/revenue_pool/src/test.rs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::*;
44
use soroban_sdk::testutils::{Address as _, Events as _};
55
use soroban_sdk::token;
66
use soroban_sdk::TryFromVal;
7-
use soroban_sdk::{Address, Env, Symbol, Vec};
7+
use soroban_sdk::{Address, Env, IntoVal, Symbol, Vec};
88

99
fn create_usdc<'a>(
1010
env: &'a Env,
@@ -266,3 +266,88 @@ fn batch_distribute_success_events() {
266266
}
267267
}
268268
}
269+
270+
#[test]
271+
fn receive_payment_emits_event_for_admin() {
272+
let env = Env::default();
273+
env.mock_all_auths();
274+
let admin = Address::generate(&env);
275+
let (_, client) = create_pool(&env);
276+
let (usdc, _, _) = create_usdc(&env, &admin);
277+
278+
client.init(&admin, &usdc);
279+
client.receive_payment(&admin, &250, &true);
280+
281+
let events = env.events().all();
282+
let receive_event = events.last().unwrap();
283+
let event_name = Symbol::try_from_val(&env, &receive_event.1.get(0).unwrap()).unwrap();
284+
assert_eq!(event_name, Symbol::new(&env, "receive_payment"));
285+
286+
let caller: Address = Address::try_from_val(&env, &receive_event.1.get(1).unwrap()).unwrap();
287+
assert_eq!(caller, admin);
288+
289+
let (amount, from_vault): (i128, bool) = receive_event.2.into_val(&env);
290+
assert_eq!(amount, 250);
291+
assert!(from_vault);
292+
}
293+
294+
#[test]
295+
#[should_panic(expected = "no pending admin")]
296+
fn claim_admin_without_pending_panics() {
297+
let env = Env::default();
298+
env.mock_all_auths();
299+
let admin = Address::generate(&env);
300+
let candidate = Address::generate(&env);
301+
let (_, client) = create_pool(&env);
302+
let (usdc, _, _) = create_usdc(&env, &admin);
303+
304+
client.init(&admin, &usdc);
305+
client.claim_admin(&candidate);
306+
}
307+
308+
#[test]
309+
#[should_panic(expected = "unauthorized: caller is not pending admin")]
310+
fn claim_admin_wrong_caller_panics() {
311+
let env = Env::default();
312+
env.mock_all_auths();
313+
let admin = Address::generate(&env);
314+
let pending_admin = Address::generate(&env);
315+
let attacker = Address::generate(&env);
316+
let (_, client) = create_pool(&env);
317+
let (usdc, _, _) = create_usdc(&env, &admin);
318+
319+
client.init(&admin, &usdc);
320+
client.set_admin(&admin, &pending_admin);
321+
client.claim_admin(&attacker);
322+
}
323+
324+
#[test]
325+
#[should_panic(expected = "invalid recipient: cannot distribute to the contract itself")]
326+
fn distribute_to_self_panics() {
327+
let env = Env::default();
328+
env.mock_all_auths();
329+
let admin = Address::generate(&env);
330+
let (pool_addr, client) = create_pool(&env);
331+
let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin);
332+
333+
client.init(&admin, &usdc_address);
334+
fund_pool(&usdc_admin, &pool_addr, 100);
335+
client.distribute(&admin, &pool_addr, &50);
336+
}
337+
338+
#[test]
339+
#[should_panic(expected = "amount must be positive")]
340+
fn batch_distribute_zero_amount_panics() {
341+
let env = Env::default();
342+
env.mock_all_auths();
343+
let admin = Address::generate(&env);
344+
let dev = Address::generate(&env);
345+
let (_, client) = create_pool(&env);
346+
let (usdc_address, _, _) = create_usdc(&env, &admin);
347+
348+
client.init(&admin, &usdc_address);
349+
350+
let mut payments: Vec<(Address, i128)> = Vec::new(&env);
351+
payments.push_back((dev, 0));
352+
client.batch_distribute(&admin, &payments);
353+
}

contracts/settlement/src/test.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ mod settlement_tests {
1313
env.mock_all_auths();
1414
let admin = Address::generate(&env);
1515
let vault = Address::generate(&env);
16-
let addr = env.register(CalloraSettlement, ());
16+
let third_party = Address::generate(&env);
17+
let addr = env.register(CalloraSettlement, ());
1718
let client = CalloraSettlementClient::new(&env, &addr);
1819
client.init(&admin, &vault);
1920
(env, addr, admin, vault, third_party)
@@ -152,7 +153,7 @@ mod settlement_tests {
152153
env.mock_all_auths();
153154
let admin = Address::generate(&env);
154155
let vault = Address::generate(&env);
155-
let addr = env.register(CalloraSettlement, ());
156+
let addr = env.register(CalloraSettlement, ());
156157
let client = CalloraSettlementClient::new(&env, &addr);
157158
client.init(&admin, &vault);
158159
client.receive_payment(&admin, &100i128, &true, &None);

contracts/vault/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,10 @@ impl CalloraVault {
325325
if let Some(s) = inst.get::<StorageKey, Address>(&StorageKey::Settlement) {
326326
let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap();
327327
Self::transfer_funds(&env, &ut, &s, amount);
328-
} else if inst.get::<StorageKey, Address>(&StorageKey::RevenuePool).is_some() {
328+
} else if inst
329+
.get::<StorageKey, Address>(&StorageKey::RevenuePool)
330+
.is_some()
331+
{
329332
Self::transfer_to_revenue_pool(env.clone(), amount);
330333
}
331334
let rid = request_id.unwrap_or(Symbol::new(&env, ""));
@@ -372,7 +375,10 @@ impl CalloraVault {
372375
if let Some(s) = inst.get::<StorageKey, Address>(&StorageKey::Settlement) {
373376
let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap();
374377
Self::transfer_funds(&env, &ut, &s, total);
375-
} else if inst.get::<StorageKey, Address>(&StorageKey::RevenuePool).is_some() {
378+
} else if inst
379+
.get::<StorageKey, Address>(&StorageKey::RevenuePool)
380+
.is_some()
381+
{
376382
Self::transfer_to_revenue_pool(env.clone(), total);
377383
}
378384
meta.balance

scripts/check-wasm-size.sh

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,114 @@
1-
#!/bin/bash
2-
# Check that all contract WASM binaries stay under the 64KB Soroban limit.
1+
#!/usr/bin/env bash
2+
# Check that all publishable contract WASM binaries stay under the 64 KiB Soroban limit.
33

4-
set -e
4+
set -euo pipefail
55

6-
MAX_SIZE=$((64 * 1024))
7-
FAILED=0
6+
MAX_SIZE_BYTES=$((64 * 1024))
7+
TARGET_DIR="${CARGO_TARGET_DIR:-target}/wasm32-unknown-unknown/release"
8+
9+
contract_manifests=()
10+
contract_packages=()
11+
failed=0
12+
13+
if command -v cargo >/dev/null 2>&1; then
14+
CARGO_BIN=$(command -v cargo)
15+
elif command -v cargo.exe >/dev/null 2>&1; then
16+
CARGO_BIN=$(command -v cargo.exe)
17+
elif [ -n "${HOME:-}" ] && [ -x "${HOME}/.cargo/bin/cargo" ]; then
18+
CARGO_BIN="${HOME}/.cargo/bin/cargo"
19+
elif [ -n "${HOME:-}" ] && [ -x "${HOME}/.cargo/bin/cargo.exe" ]; then
20+
CARGO_BIN="${HOME}/.cargo/bin/cargo.exe"
21+
elif [ -n "${USERPROFILE:-}" ] && [ -x "${USERPROFILE}/.cargo/bin/cargo.exe" ]; then
22+
CARGO_BIN="${USERPROFILE}/.cargo/bin/cargo.exe"
23+
elif [ -n "${USERNAME:-}" ] && [ -x "/c/Users/${USERNAME}/.cargo/bin/cargo.exe" ]; then
24+
CARGO_BIN="/c/Users/${USERNAME}/.cargo/bin/cargo.exe"
25+
else
26+
echo "ERROR: cargo was not found on PATH and no fallback binary was detected"
27+
exit 1
28+
fi
29+
30+
while IFS= read -r -d '' manifest; do
31+
contract_manifests+=("$manifest")
32+
done < <(find contracts -mindepth 2 -maxdepth 2 -name Cargo.toml -print0 | sort -z)
33+
34+
if [ "${#contract_manifests[@]}" -eq 0 ]; then
35+
echo "ERROR: no contract manifests found under contracts/*/Cargo.toml"
36+
exit 1
37+
fi
38+
39+
discover_contract_packages() {
40+
local manifest
41+
local package_name
42+
43+
for manifest in "${contract_manifests[@]}"; do
44+
if ! grep -Eq 'crate-type\s*=\s*\[[^]]*"cdylib"' "$manifest"; then
45+
continue
46+
fi
47+
48+
package_name=$(awk -F'"' '/^[[:space:]]*name[[:space:]]*=/{print $2; exit}' "$manifest")
49+
if [ -z "$package_name" ]; then
50+
echo "ERROR: unable to determine package name from $manifest"
51+
exit 1
52+
fi
53+
54+
contract_packages+=("$package_name")
55+
done
56+
57+
if [ "${#contract_packages[@]}" -eq 0 ]; then
58+
echo 'ERROR: no publishable contract crates with crate-type = ["cdylib", ...] were found'
59+
exit 1
60+
fi
61+
}
862

963
check_wasm() {
1064
local crate="$1"
11-
local wasm_name="$2"
12-
local wasm_file="target/wasm32-unknown-unknown/release/${wasm_name}.wasm"
65+
local wasm_name="${crate//-/_}"
66+
local wasm_file="$TARGET_DIR/${wasm_name}.wasm"
67+
local size_bytes
68+
local size_kib
69+
local headroom_bytes
70+
local headroom_kib
1371

1472
if [ ! -f "$wasm_file" ]; then
15-
echo "ERROR: $wasm_file not found — did the build run?"
16-
FAILED=1
73+
echo "FAIL $crate: missing artifact at $wasm_file"
74+
failed=1
1775
return
1876
fi
1977

20-
local size
21-
size=$(wc -c < "$wasm_file")
22-
local size_kb=$((size / 1024))
78+
size_bytes=$(wc -c < "$wasm_file")
79+
size_kib=$((size_bytes / 1024))
2380

24-
if [ "$size" -gt "$MAX_SIZE" ]; then
25-
echo "FAIL $crate: ${size_kb}KB — exceeds 64KB limit"
26-
FAILED=1
27-
else
28-
local headroom=$(( (MAX_SIZE - size) / 1024 ))
29-
echo "OK $crate: ${size_kb}KB (${headroom}KB headroom)"
81+
if [ "$size_bytes" -gt "$MAX_SIZE_BYTES" ]; then
82+
echo "FAIL $crate: ${size_bytes} bytes (${size_kib} KiB) exceeds 65536-byte limit"
83+
failed=1
84+
return
3085
fi
86+
87+
headroom_bytes=$((MAX_SIZE_BYTES - size_bytes))
88+
headroom_kib=$((headroom_bytes / 1024))
89+
echo "OK $crate: ${size_bytes} bytes (${size_kib} KiB, ${headroom_bytes} bytes / ${headroom_kib} KiB headroom)"
3190
}
3291

33-
echo "Building all contracts for wasm32-unknown-unknown (release)..."
34-
cargo build --target wasm32-unknown-unknown --release \
35-
-p callora-vault \
36-
-p callora-revenue-pool \
37-
-p callora-settlement
92+
discover_contract_packages
93+
94+
echo "Building publishable contracts for wasm32-unknown-unknown (release)..."
95+
cargo_args=(build --target wasm32-unknown-unknown --release)
96+
for crate in "${contract_packages[@]}"; do
97+
cargo_args+=(-p "$crate")
98+
done
99+
"$CARGO_BIN" "${cargo_args[@]}"
38100

39101
echo ""
40-
echo "WASM size check (limit: 64KB)"
41-
echo "------------------------------"
42-
check_wasm "callora-vault" "callora_vault"
43-
check_wasm "callora-revenue-pool" "callora_revenue_pool"
44-
check_wasm "callora-settlement" "callora_settlement"
102+
echo "WASM size check (limit: 65536 bytes / 64 KiB)"
103+
echo "---------------------------------------------"
104+
for crate in "${contract_packages[@]}"; do
105+
check_wasm "$crate"
106+
done
45107
echo ""
46108

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

52-
echo "All contracts within size limit."
114+
echo "All publishable contract WASM artifacts are within the Soroban size limit."

0 commit comments

Comments
 (0)