Skip to content

Commit 2defd6f

Browse files
authored
Merge pull request #202 from eischideraa-unn/blackboxai/execute_withdraw
Implement execute_withdraw
2 parents 360af6b + 5cb1541 commit 2defd6f

5 files changed

Lines changed: 422 additions & 64 deletions

File tree

contracts/contracts/proposals/src/lib.rs

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
mod storage;
2020
mod test;
2121

22+
mod treasury_interface;
23+
2224
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String, Symbol};
2325

2426
pub use storage::{
@@ -54,12 +56,19 @@ impl ProposalsContract {
5456
proposer: Address,
5557
description: String,
5658
expires_at: u64,
59+
treasury: Address,
60+
token: Address,
61+
to: Address,
62+
amount: i128,
5763
) -> u64 {
5864
proposer.require_auth();
5965
let now = env.ledger().timestamp();
6066
if expires_at <= now {
6167
panic!("expires_at must be in the future");
6268
}
69+
if amount <= 0 {
70+
panic!("amount must be positive");
71+
}
6372

6473
let id: u64 = env
6574
.storage()
@@ -75,7 +84,13 @@ impl ProposalsContract {
7584
yes_votes: 0,
7685
no_votes: 0,
7786
status: ProposalStatus::Active,
87+
treasury: treasury.clone(),
88+
token: token.clone(),
89+
to: to.clone(),
90+
amount,
7891
};
92+
93+
7994
env.storage()
8095
.instance()
8196
.set(&DataKey::Proposal(id), &proposal);
@@ -89,8 +104,14 @@ impl ProposalsContract {
89104
id,
90105
proposer,
91106
expires_at,
107+
treasury,
108+
token,
109+
to,
110+
amount,
92111
},
93112
);
113+
114+
94115
id
95116
}
96117

@@ -132,13 +153,14 @@ impl ProposalsContract {
132153
);
133154
}
134155

135-
/// Finalise a proposal after its `expires_at`. Callable by anyone
136-
/// — the auto-rejection mechanic from the issue. Sets the status
137-
/// to `Passed` when `yes_votes > no_votes`, else `Rejected`. The
138-
/// tie (yes_votes == no_votes) breaks toward Rejected per the
139-
/// issue's `yes_votes <= no_votes` condition.
156+
/// Finalise a proposal after its `expires_at`. Callable by anyone.
157+
///
158+
/// Status mapping (required by execute_withdraw acceptance criteria):
159+
/// - `yes_votes > no_votes` => `Approved`
160+
/// - otherwise => `Rejected`
140161
pub fn finalize_proposal(env: Env, proposal_id: u64) -> ProposalStatus {
141162
let mut proposal = Self::load_proposal(&env, proposal_id);
163+
142164
if !matches!(proposal.status, ProposalStatus::Active) {
143165
panic!("proposal already finalized");
144166
}
@@ -192,6 +214,70 @@ impl ProposalsContract {
192214
);
193215
}
194216

217+
/// Withdraw from the group treasury for an approved proposal.
218+
///
219+
/// Acceptance criteria requirements:
220+
/// - caller must be a treasury member
221+
/// - proposal must be Approved
222+
/// - proposal must not already be Executed
223+
/// - treasury must have sufficient balance
224+
/// - emits WithdrawEvent (from treasury) and ProposalExecutedEvent (from proposals)
225+
pub fn execute_withdraw(env: Env, caller: Address, proposal_id: u64) {
226+
caller.require_auth();
227+
228+
let mut proposal = Self::load_proposal(&env, proposal_id);
229+
230+
if matches!(proposal.status, ProposalStatus::Executed) {
231+
panic!("proposal already executed");
232+
}
233+
if !matches!(proposal.status, ProposalStatus::Approved) {
234+
panic!("proposal not approved");
235+
}
236+
237+
238+
// Verify caller is a treasury member.
239+
let treasury_client = crate::treasury_interface::TreasuryClient::new(
240+
&env,
241+
&proposal.treasury,
242+
);
243+
244+
if !treasury_client.is_member(&caller.clone()) {
245+
panic!("caller is not a treasury member");
246+
}
247+
248+
// Verify sufficient balance.
249+
let bal = treasury_client.balance(&proposal.token.clone());
250+
if bal < proposal.amount {
251+
panic!("insufficient funds");
252+
}
253+
254+
// Withdraw from the treasury.
255+
treasury_client.withdraw(
256+
&proposal.to.clone(),
257+
&proposal.token.clone(),
258+
&proposal.amount,
259+
);
260+
261+
262+
// Update proposal status.
263+
proposal.status = ProposalStatus::Executed;
264+
env.storage()
265+
.instance()
266+
.set(&DataKey::Proposal(proposal_id), &proposal);
267+
268+
// Emit proposal execution event.
269+
env.events().publish(
270+
(symbol_short!("execut"),),
271+
ProposalExecutedEvent {
272+
id: proposal_id,
273+
executor: caller,
274+
},
275+
);
276+
277+
}
278+
279+
280+
195281
pub fn get_proposal(env: Env, proposal_id: u64) -> Proposal {
196282
Self::load_proposal(&env, proposal_id)
197283
}

contracts/contracts/proposals/src/storage.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub enum DataKey {
1212
#[derive(Clone, Debug, Eq, PartialEq)]
1313
pub enum ProposalStatus {
1414
Active,
15+
Approved,
1516
Passed,
1617
Rejected,
1718
Executed,
@@ -28,8 +29,15 @@ pub struct Proposal {
2829
pub yes_votes: u32,
2930
pub no_votes: u32,
3031
pub status: ProposalStatus,
32+
33+
// Withdrawal execution parameters.
34+
pub treasury: Address,
35+
pub token: Address,
36+
pub to: Address,
37+
pub amount: i128,
3138
}
3239

40+
3341
// ── Events ───────────────────────────────────────────────────────────────────
3442

3543
#[contracttype]
@@ -38,8 +46,14 @@ pub struct ProposalCreatedEvent {
3846
pub id: u64,
3947
pub proposer: Address,
4048
pub expires_at: u64,
49+
50+
pub treasury: Address,
51+
pub token: Address,
52+
pub to: Address,
53+
pub amount: i128,
4154
}
4255

56+
4357
#[contracttype]
4458
#[derive(Clone)]
4559
pub struct VoteCastEvent {

0 commit comments

Comments
 (0)