A secure withdraw() function in the Soroban smart contract that allows designated winners to withdraw collected pools for specific cycles.
- Uses
RotationOrderstorage (shuffled member list) - Enforces strict rotation: only designated member can withdraw for their cycle
- Validates cycle index against rotation order
- New helper:
get_cycle_deadline(env, cycle) - Calculates deadline:
initial_deadline + (cycles_elapsed * frequency_days * 86400) - Rejects withdrawals before cycle deadline
- New helper:
is_cycle_fully_funded(env, cycle, required_amount) - Checks all members contributed:
total_contributed >= (cycle * contribution_amount) - Ensures pool has sufficient funds
- Follows Checks-Effects-Interactions pattern
- Updates state BEFORE token transfer
- New storage:
CycleWithdrawals- tracks withdrawals per cycle - Prevents double-withdrawal attacks
-
Added DataKey:
CycleWithdrawals, // Map<cycle, Map<member, bool>>
-
New Functions:
withdraw(env, member, cycle)- Main withdrawal functionget_cycle_deadline(env, cycle)- Calculate cycle deadlineis_cycle_fully_funded(env, cycle, amount)- Verify funding
-
Updated Function:
claim_payout()- Now wrapswithdraw()for backward compatibility
-
Added Tests (8 new tests):
- Happy path withdrawal
- Double-withdrawal prevention
- Rotation enforcement
- Rotation requirement
- Cycle validation
- Panic protection
- Member standing check
- Backward compatibility
- ✅ Authorization via
member.require_auth() - ✅ Panic state blocking
- ✅ Cycle range validation (1 to max_rounds)
- ✅ Member standing verification
- ✅ Rotation order enforcement
- ✅ Double-withdrawal prevention
- ✅ State updates before transfers (reentrancy protection)
- ✅ Time-based maturity checks
- ✅ Pool funding verification
// Withdraw for cycle 1
let payout = contract.withdraw(&member_address, &1_u32)?;
// Legacy function still works
let payout = contract.claim_payout(&member_address)?;cd contracts/ajo-circle
cargo test test_withdraw # Run withdrawal tests
cargo test # Run all testscontracts/ajo-circle/src/lib.rs- Main implementationWITHDRAWAL_IMPLEMENTATION.md- Detailed documentationWITHDRAWAL_SUMMARY.md- This file
To deploy and use:
-
Build the contract:
cd contracts/ajo-circle cargo build --target wasm32-unknown-unknown --release -
Deploy to Stellar testnet using Soroban CLI
-
Update frontend to call
withdraw()function -
Test thoroughly on testnet before mainnet deployment
CirclePanicked- Emergency state activeInvalidInput- Invalid cycle or not matureDisqualified- Member inactiveInsufficientFunds- Pool not fully fundedUnauthorized- Wrong member for cycleAlreadyPaid- Already withdrawnNotFound- Data missing
✅ Existing claim_payout() calls continue to work
✅ All security improvements apply to legacy calls
✅ No breaking changes to existing integrations