-
Notifications
You must be signed in to change notification settings - Fork 11
Fixes Issue 25: Implement goal-based and rotational group types #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
0b720b9
c85f6a3
d165dde
af23dc3
a19eeee
ba59604
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,13 @@ fn extend_instance_ttl(env: &Env) { | |||||||||||||||||||||||||||||||||||||
| .extend_ttl(LEDGERS_TO_LIVE / 2, LEDGERS_TO_LIVE); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[contracttype] | ||||||||||||||||||||||||||||||||||||||
| #[derive(Clone, PartialEq, Eq)] | ||||||||||||||||||||||||||||||||||||||
| pub enum GroupType { | ||||||||||||||||||||||||||||||||||||||
| Rotational, | ||||||||||||||||||||||||||||||||||||||
| GoalBased, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[contracttype] | ||||||||||||||||||||||||||||||||||||||
| #[derive(Clone)] | ||||||||||||||||||||||||||||||||||||||
| pub enum DataKey { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -26,6 +33,9 @@ pub enum DataKey { | |||||||||||||||||||||||||||||||||||||
| HasContributedThisCycle(Address), | ||||||||||||||||||||||||||||||||||||||
| CycleMemberCount, | ||||||||||||||||||||||||||||||||||||||
| User(Address), | ||||||||||||||||||||||||||||||||||||||
| GroupType, | ||||||||||||||||||||||||||||||||||||||
| TargetAmount, | ||||||||||||||||||||||||||||||||||||||
| LockUntilTarget, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[contracttype] | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,6 +57,9 @@ impl KoloSavingsContract { | |||||||||||||||||||||||||||||||||||||
| token: Address, | ||||||||||||||||||||||||||||||||||||||
| name: String, | ||||||||||||||||||||||||||||||||||||||
| contribution_amount: i128, | ||||||||||||||||||||||||||||||||||||||
| group_type: GroupType, | ||||||||||||||||||||||||||||||||||||||
| target_amount: Option<i128>, | ||||||||||||||||||||||||||||||||||||||
| lock_until_target: bool, | ||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||
| if env.storage().instance().has(&DataKey::Admin) { | ||||||||||||||||||||||||||||||||||||||
| panic!("Already initialized"); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -61,6 +74,13 @@ impl KoloSavingsContract { | |||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::ContributionAmount, &contribution_amount); | ||||||||||||||||||||||||||||||||||||||
| env.storage().instance().set(&DataKey::GroupType, &group_type); | ||||||||||||||||||||||||||||||||||||||
| if let Some(target) = target_amount { | ||||||||||||||||||||||||||||||||||||||
| env.storage().instance().set(&DataKey::TargetAmount, &target); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::LockUntilTarget, &lock_until_target); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Validate When 🛡️ Proposed guard env.storage().instance().set(&DataKey::GroupType, &group_type);
+if lock_until_target && target_amount.is_none() {
+ panic!("lock_until_target requires a target_amount");
+}
if let Some(target) = target_amount {
+ if target <= 0 {
+ panic!("target_amount must be positive");
+ }
env.storage().instance().set(&DataKey::TargetAmount, &target);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let empty_members: Vec<Address> = Vec::new(&env); | ||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -109,21 +129,29 @@ impl KoloSavingsContract { | |||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .get(&DataKey::ContributionAmount) | ||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||
| if amount != expected_amount { | ||||||||||||||||||||||||||||||||||||||
| let group_type: GroupType = env.storage().instance().get(&DataKey::GroupType).unwrap_or(GroupType::Rotational); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if group_type == GroupType::Rotational && amount != expected_amount { | ||||||||||||||||||||||||||||||||||||||
| panic!("Must contribute the exact amount"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if amount <= 0 { | ||||||||||||||||||||||||||||||||||||||
| panic!("Amount must be positive"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let members: Vec<Address> = env.storage().instance().get(&DataKey::Members).unwrap(); | ||||||||||||||||||||||||||||||||||||||
| if !members.contains(&member) { | ||||||||||||||||||||||||||||||||||||||
| panic!("Not a member"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Freeze the member count at the start of a cycle on the first contribution | ||||||||||||||||||||||||||||||||||||||
| if !env.storage().instance().has(&DataKey::CycleMemberCount) { | ||||||||||||||||||||||||||||||||||||||
| let count = members.len() as i128; | ||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::CycleMemberCount, &count); | ||||||||||||||||||||||||||||||||||||||
| if group_type == GroupType::Rotational { | ||||||||||||||||||||||||||||||||||||||
| if !env.storage().instance().has(&DataKey::CycleMemberCount) { | ||||||||||||||||||||||||||||||||||||||
| let count = members.len() as i128; | ||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::CycleMemberCount, &count); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+132
to
155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win GoalBased contributions are not actually "flexible" in cadence. The exact-amount and cycle-freeze branches are correctly gated to 🧰 Tools🪛 Clippy (1.96.0)[warning] 148-148: this (warning) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let has_contributed: bool = env | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -173,6 +201,11 @@ impl KoloSavingsContract { | |||||||||||||||||||||||||||||||||||||
| /// Withdraw payout (Admin triggers payout to a member) | ||||||||||||||||||||||||||||||||||||||
| /// Enforces strictly fixed rotational payout (Ajo/Esusu) rules. | ||||||||||||||||||||||||||||||||||||||
| pub fn payout(env: Env, recipient: Address) { | ||||||||||||||||||||||||||||||||||||||
| let group_type: GroupType = env.storage().instance().get(&DataKey::GroupType).unwrap_or(GroupType::Rotational); | ||||||||||||||||||||||||||||||||||||||
| if group_type == GroupType::GoalBased { | ||||||||||||||||||||||||||||||||||||||
| panic!("Payouts not allowed in GoalBased groups"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); | ||||||||||||||||||||||||||||||||||||||
| admin.require_auth(); | ||||||||||||||||||||||||||||||||||||||
| extend_instance_ttl(&env); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -225,25 +258,93 @@ impl KoloSavingsContract { | |||||||||||||||||||||||||||||||||||||
| .publish((symbol_short!("payout"), recipient), pool_size); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// Withdraw savings (GoalBased groups only) | ||||||||||||||||||||||||||||||||||||||
| pub fn withdraw_savings(env: Env, member: Address, amount: i128) { | ||||||||||||||||||||||||||||||||||||||
| member.require_auth(); | ||||||||||||||||||||||||||||||||||||||
| extend_instance_ttl(&env); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let group_type: GroupType = env | ||||||||||||||||||||||||||||||||||||||
| .storage() | ||||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .get(&DataKey::GroupType) | ||||||||||||||||||||||||||||||||||||||
| .unwrap_or(GroupType::Rotational); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if group_type == GroupType::Rotational { | ||||||||||||||||||||||||||||||||||||||
| panic!("Withdrawals not allowed in rotational groups"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if amount <= 0 { | ||||||||||||||||||||||||||||||||||||||
| panic!("Withdrawal amount must be positive"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let current_contribution: i128 = env | ||||||||||||||||||||||||||||||||||||||
| .storage() | ||||||||||||||||||||||||||||||||||||||
| .persistent() | ||||||||||||||||||||||||||||||||||||||
| .get(&DataKey::Contributions(member.clone())) | ||||||||||||||||||||||||||||||||||||||
| .unwrap_or(0); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if current_contribution < amount { | ||||||||||||||||||||||||||||||||||||||
| panic!("Insufficient savings to withdraw"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let lock_until_target: bool = env | ||||||||||||||||||||||||||||||||||||||
| .storage() | ||||||||||||||||||||||||||||||||||||||
| .instance() | ||||||||||||||||||||||||||||||||||||||
| .get(&DataKey::LockUntilTarget) | ||||||||||||||||||||||||||||||||||||||
| .unwrap_or(false); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if lock_until_target { | ||||||||||||||||||||||||||||||||||||||
| if let Some(target_amount) = env.storage().instance().get::<_, i128>(&DataKey::TargetAmount) { | ||||||||||||||||||||||||||||||||||||||
| if current_contribution < target_amount { | ||||||||||||||||||||||||||||||||||||||
| panic!("Target amount not reached yet"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+296
to
+302
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Lock re-engages after a partial withdrawal. The gate compares the live 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let new_contribution = current_contribution - amount; | ||||||||||||||||||||||||||||||||||||||
| env.storage().persistent().set( | ||||||||||||||||||||||||||||||||||||||
| &DataKey::Contributions(member.clone()), | ||||||||||||||||||||||||||||||||||||||
| &new_contribution, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| env.storage().persistent().extend_ttl( | ||||||||||||||||||||||||||||||||||||||
| &DataKey::Contributions(member.clone()), | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE / 2, | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let token: Address = env.storage().instance().get(&DataKey::Token).unwrap(); | ||||||||||||||||||||||||||||||||||||||
| let token_client = token::Client::new(&env, &token); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| token_client.transfer(&env.current_contract_address(), &member, &amount); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| env.events() | ||||||||||||||||||||||||||||||||||||||
| .publish((symbol_short!("withdraw"), member), amount); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+320
to
+321
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In the soroban-sdk, the Citations:
Migrate the remaining legacy event publishes
🧰 Tools🪛 Clippy (1.96.0)[warning] 321-321: use of deprecated method (warning) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// Resets the payout cycle so members can receive payouts again. | ||||||||||||||||||||||||||||||||||||||
| pub fn reset_cycle(env: Env) { | ||||||||||||||||||||||||||||||||||||||
| let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); | ||||||||||||||||||||||||||||||||||||||
| admin.require_auth(); | ||||||||||||||||||||||||||||||||||||||
| extend_instance_ttl(&env); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let group_type: GroupType = env.storage().instance().get(&DataKey::GroupType).unwrap_or(GroupType::Rotational); | ||||||||||||||||||||||||||||||||||||||
| let members: Vec<Address> = env.storage().instance().get(&DataKey::Members).unwrap(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for member in members.iter() { | ||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .persistent() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::HasReceivedPayout(member.clone()), &false); | ||||||||||||||||||||||||||||||||||||||
| if group_type == GroupType::Rotational { | ||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .persistent() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::HasReceivedPayout(member.clone()), &false); | ||||||||||||||||||||||||||||||||||||||
| env.storage().persistent().extend_ttl( | ||||||||||||||||||||||||||||||||||||||
| &DataKey::HasReceivedPayout(member.clone()), | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE / 2, | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| env.storage() | ||||||||||||||||||||||||||||||||||||||
| .persistent() | ||||||||||||||||||||||||||||||||||||||
| .set(&DataKey::HasContributedThisCycle(member.clone()), &false); | ||||||||||||||||||||||||||||||||||||||
| env.storage().persistent().extend_ttl( | ||||||||||||||||||||||||||||||||||||||
| &DataKey::HasReceivedPayout(member.clone()), | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE / 2, | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| env.storage().persistent().extend_ttl( | ||||||||||||||||||||||||||||||||||||||
| &DataKey::HasContributedThisCycle(member.clone()), | ||||||||||||||||||||||||||||||||||||||
| LEDGERS_TO_LIVE / 2, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -252,7 +353,9 @@ impl KoloSavingsContract { | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Clear the frozen member count so it is re-established at the next cycle's first contribution | ||||||||||||||||||||||||||||||||||||||
| env.storage().instance().remove(&DataKey::CycleMemberCount); | ||||||||||||||||||||||||||||||||||||||
| if group_type == GroupType::Rotational { | ||||||||||||||||||||||||||||||||||||||
| env.storage().instance().remove(&DataKey::CycleMemberCount); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| env.events().publish((symbol_short!("reset"),), ()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
soroban-sdk 27.0.0 migration breaking changes from 21💡 Result:
Migrating from soroban-sdk v21 to v27 involves significant changes, as you are skipping several major releases (v22 through v26) in addition to the changes in v27 [1][2][3]. Key migration areas include: 1. Protocol Support and Environment: - SDK v27 is adapted for Stellar Protocol 27 [4]. - You must update your environment to build for the wasm32v1-none target using stellar-cli (do not use cargo build directly) [5]. - The dependency on stellar-xdr has been updated, and the curr feature flag has been removed [4]. 2. CAP-71 Authorization Delegation: - SDK v27 introduces support for CAP-71, adding new APIs for delegated authentication [4]. - New functions include Address::delegate_account_auth and Env::get_delegated_signers_for_current_auth_check [4]. 3. Major Breaking Changes Across Versions (v21-v27): - v22: Env::register and Env::register_at replaced previous registration methods; DeployerWithAddress::deploy_v2 replaced deploy; fuzz_catch_panic was deprecated [2]. - v23: contractevent replaced Events::publish; MuxedAddress replaced Address in certain TokenInterface methods; changes were made to how archived persistent entries are tested [2]. - v27 Specific: Functional changes were introduced regarding cfg attributes and export arguments (see the _migrating module in the SDK source for detailed guidance) [1][3]. Recommendations: - Consult the _migrating module within the soroban-sdk documentation or source code for a comprehensive, version-by-version summary [5][6][3]. - Review the GitHub release notes for every major version release between v21 and v27 to identify specific breaking changes relevant to your contract code [1][7]. - Update your test suite, as snapshot formats for protocol v27 have been updated and some diagnostic events are no longer captured in snapshots [4][2].
Citations:
🏁 Script executed:
Repository: Kolo-Org/Kolo-smartcontract
Length of output: 961
🏁 Script executed:
Repository: Kolo-Org/Kolo-smartcontract
Length of output: 514
🏁 Script executed:
Repository: Kolo-Org/Kolo-smartcontract
Length of output: 961
🏁 Script executed:
Repository: Kolo-Org/Kolo-smartcontract
Length of output: 312
🏁 Script executed:
Repository: Kolo-Org/Kolo-smartcontract
Length of output: 43935
Update the Soroban 27 compatibility path.
.github/workflows/rust.ymlandcontracts/deploy.shstill build withcargo build --target wasm32-unknown-unknown, but this SDK bump should move to the newer Soroban CLI/target.contracts/src/lib.rsalso still usesenv.events().publish, so the contract/tests need a compatibility pass before merge.🤖 Prompt for AI Agents