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
7 changes: 4 additions & 3 deletions INVARIANTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ Helper and view functions such as `get_meta`, `get_max_deduct`, `get_revenue_poo
**Pre-conditions**
- Vault is not already initialized:
- `!env.storage().instance().has(META_KEY)`
- `initial_balance.unwrap_or(0) >= 0`
- `max_deduct.unwrap_or(DEFAULT_MAX_DEDUCT) > 0`
- If `initial_balance > 0`, the contract already holds at least that much USDC:
- `usdc.balance(current_contract_address) >= initial_balance`
- The on-ledger USDC balance already covers the requested internal starting balance:
- `usdc.balance(current_contract_address) >= initial_balance.unwrap_or(0)`

**Post-conditions**
- `VaultMeta.balance == initial_balance.unwrap_or(0)`
- `VaultMeta.balance >= 0` (since `initial_balance` is an `i128` and enforced via the token-balance check).
- `VaultMeta.balance >= 0` (because `initial_balance.unwrap_or(0)` is explicitly checked to be non-negative before storage is written).

---

Expand Down
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
18 changes: 16 additions & 2 deletions contracts/vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ impl CalloraVault {
let max_d = max_deduct.unwrap_or(DEFAULT_MAX_DEDUCT);
assert!(max_d > 0, "max_deduct must be positive");
assert!(min_d <= max_d, "min_deposit cannot exceed max_deduct");
if balance > 0 {
let onchain_usdc_balance =
token::Client::new(&env, &usdc_token).balance(&env.current_contract_address());
assert!(
onchain_usdc_balance >= balance,
"initial_balance exceeds on-ledger USDC balance"
);
}
let meta = VaultMeta {
owner: owner.clone(),
balance,
Expand Down Expand Up @@ -325,7 +333,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 +383,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
35 changes: 33 additions & 2 deletions contracts/vault/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ fn init_sets_owner_and_min_deposit() {
assert_eq!(client.get_admin(), owner);
}

#[test]
fn init_succeeds_when_onchain_usdc_balance_covers_initial_balance() {
let env = Env::default();
let owner = Address::generate(&env);
let (vault_address, client) = create_vault(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &owner);

env.mock_all_auths();
fund_vault(&usdc_admin, &vault_address, 500);

let meta = client.init(&owner, &usdc, &Some(400), &None, &None, &None, &None);

assert_eq!(meta.balance, 400);
assert_eq!(client.balance(), 400);
}

#[test]
#[should_panic(expected = "initial_balance exceeds on-ledger USDC balance")]
fn init_fails_when_initial_balance_exceeds_onchain_usdc_balance() {
let env = Env::default();
let owner = Address::generate(&env);
let (vault_address, client) = create_vault(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &owner);

env.mock_all_auths();
fund_vault(&usdc_admin, &vault_address, 99);

client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None);
}

#[test]
fn double_init_fails() {
let env = Env::default();
Expand Down Expand Up @@ -539,10 +569,11 @@ fn set_authorized_caller_sets_and_emits_event() {
let env = Env::default();
let owner = Address::generate(&env);
let new_caller = Address::generate(&env);
let (_, client) = create_vault(&env);
let (usdc, _, _) = create_usdc(&env, &owner);
let (vault_address, client) = create_vault(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &owner);

env.mock_all_auths();
fund_vault(&usdc_admin, &vault_address, 200);
client.init(&owner, &usdc, &Some(200), &None, &None, &None, &None);

client.set_authorized_caller(&new_caller);
Expand Down
6 changes: 3 additions & 3 deletions contracts/vault/src/test_init_hardening.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ fn test_double_initialization_fails() {
let client = CalloraVaultClient::new(&env, &addr);

// First init
client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None);
client.init(&owner, &usdc, &Some(0), &None, &None, &None, &None);
// Second init should panic
client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None);
client.init(&owner, &usdc, &Some(0), &None, &None, &None, &None);
}

#[test]
Expand Down Expand Up @@ -126,7 +126,7 @@ fn test_init_validates_successfully() {
client.init(
&owner,
&usdc,
&Some(100),
&Some(0),
&None,
&Some(10),
&Some(pool.clone()),
Expand Down
Loading
Loading