1919mod storage;
2020mod test;
2121
22+ mod treasury_interface;
23+
2224use soroban_sdk:: { contract, contractimpl, symbol_short, Address , Env , String , Symbol } ;
2325
2426pub 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 }
0 commit comments