Skip to content

Commit ad9843e

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent a308b36 commit ad9843e

9 files changed

Lines changed: 2652 additions & 2632 deletions

File tree

contracts/escrow/src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ mod modules {
3838
mod storage {
3939
pub mod types;
4040
}
41-
mod tests {
42-
#[cfg(test)]
43-
mod test;
44-
}
41+
#[cfg(test)]
42+
mod tests;
4543

4644
pub use crate::contract::EscrowContract;
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
extern crate std;
2+
3+
use crate::storage::types::{Escrow, Flags, Milestone, Roles, Trustline};
4+
use soroban_sdk::{testutils::Address as _, vec, Address, Env, Map, String};
5+
6+
use super::helpers::{create_escrow_contract, create_usdc_token};
7+
8+
#[test]
9+
fn test_withdraw_remaining_funds_success() {
10+
let env = Env::default();
11+
env.mock_all_auths();
12+
13+
let admin = Address::generate(&env);
14+
let approver = Address::generate(&env);
15+
let service_provider = Address::generate(&env);
16+
let platform = Address::generate(&env);
17+
let release_signer = Address::generate(&env);
18+
let dispute_resolver = Address::generate(&env);
19+
let trustless_work_address = Address::generate(&env);
20+
21+
let usdc = create_usdc_token(&env, &admin);
22+
23+
let platform_fee = 3 * 100; // 3%
24+
let roles = Roles {
25+
approver: approver.clone(),
26+
service_provider: service_provider.clone(),
27+
platform_address: platform.clone(),
28+
release_signer: release_signer.clone(),
29+
dispute_resolver: dispute_resolver.clone(),
30+
};
31+
32+
let flags = Flags {
33+
disputed: false,
34+
released: false,
35+
resolved: false,
36+
approved: false,
37+
};
38+
let trustline = Trustline {
39+
address: usdc.0.address.clone(),
40+
};
41+
let milestones = vec![
42+
&env,
43+
Milestone {
44+
description: String::from_str(&env, "m1"),
45+
status: String::from_str(&env, "Pending"),
46+
evidence: String::from_str(&env, "e"),
47+
amount: 100_000,
48+
flags: flags.clone(),
49+
receiver: service_provider.clone(),
50+
},
51+
Milestone {
52+
description: String::from_str(&env, "m2"),
53+
status: String::from_str(&env, "Pending"),
54+
evidence: String::from_str(&env, "e"),
55+
amount: 100_000,
56+
flags: flags.clone(),
57+
receiver: service_provider.clone(),
58+
},
59+
];
60+
61+
let esc = Escrow {
62+
engagement_id: String::from_str(&env, "eng"),
63+
title: String::from_str(&env, "t"),
64+
description: String::from_str(&env, "d"),
65+
roles: roles.clone(),
66+
platform_fee,
67+
milestones: milestones.clone(),
68+
trustline,
69+
receiver_memo: 0,
70+
};
71+
72+
let test = create_escrow_contract(&env);
73+
let client = test.client;
74+
client.initialize_escrow(&esc);
75+
76+
// Fund contract with 250_000 so after releasing 2x100_000 there are 50_000 remaining
77+
usdc.1.mint(&client.address, &250_000);
78+
79+
// Approve and release both milestones
80+
client.approve_milestone(&0, &approver);
81+
client.approve_milestone(&1, &approver);
82+
client.release_milestone_funds(&release_signer, &trustless_work_address, &0);
83+
client.release_milestone_funds(&release_signer, &trustless_work_address, &1);
84+
85+
// Sanity: contract balance should be 50_000 now
86+
let contract_balance_before = usdc.0.balance(&client.address);
87+
assert_eq!(contract_balance_before, 50_000);
88+
89+
// Build distributions below remaining balance so fees also fit:
90+
// send 10k to TW, 5k to platform, 33k to receiver => total = 48,000
91+
let mut dist: Map<Address, i128> = Map::new(&env);
92+
dist.set(trustless_work_address.clone(), 10_000);
93+
dist.set(platform.clone(), 5_000);
94+
dist.set(service_provider.clone(), 33_000);
95+
96+
// Capture balances before
97+
let tw_before = usdc.0.balance(&trustless_work_address);
98+
let platform_before = usdc.0.balance(&platform);
99+
let receiver_before = usdc.0.balance(&service_provider);
100+
101+
client.withdraw_remaining_funds(&dispute_resolver, &trustless_work_address, &dist);
102+
103+
// Fees are computed over the total distribution (48,000). Net amounts are distribution - proportional fee share.
104+
let total_dist = 48_000i128;
105+
let tw_fee = (total_dist * 30) / 10000; // 0.3% => 144
106+
let platform_fee_amount = (total_dist * platform_fee as i128) / 10000; // 3% => 1440
107+
let total_fees = tw_fee + platform_fee_amount; // 1584
108+
109+
// Proportional fee share per beneficiary
110+
let fee_share_tw = (10_000 * total_fees) / total_dist; // 330
111+
let fee_share_platform = (5_000 * total_fees) / total_dist; // 165
112+
let fee_share_receiver = (33_000 * total_fees) / total_dist; // 1089
113+
114+
let net_tw = 10_000 - fee_share_tw; // 9,670 + fee payment 144 => balance increase 9,814 vs original model 10,144
115+
let net_platform = 5_000 - fee_share_platform; // 4,835 + platform fee 1440 => 6,275 total increase
116+
let net_receiver = 33_000 - fee_share_receiver; // 31,911
117+
118+
// Contract leftover = 50,000 - total_dist (because fees + nets == total_dist)
119+
let expected_leftover = 50_000 - total_dist; // 2,000
120+
121+
assert_eq!(usdc.0.balance(&client.address), expected_leftover);
122+
assert_eq!(
123+
usdc.0.balance(&trustless_work_address),
124+
tw_before + net_tw + tw_fee
125+
);
126+
assert_eq!(
127+
usdc.0.balance(&platform),
128+
platform_before + net_platform + platform_fee_amount
129+
);
130+
assert_eq!(
131+
usdc.0.balance(&service_provider),
132+
receiver_before + net_receiver
133+
);
134+
}
135+
136+
#[test]
137+
fn test_withdraw_remaining_funds_unauthorized() {
138+
let env = Env::default();
139+
env.mock_all_auths();
140+
let admin = Address::generate(&env);
141+
let approver = Address::generate(&env);
142+
let service_provider = Address::generate(&env);
143+
let platform = Address::generate(&env);
144+
let release_signer = Address::generate(&env);
145+
let dispute_resolver = Address::generate(&env);
146+
let attacker = Address::generate(&env);
147+
let trustless_work_address = Address::generate(&env);
148+
let usdc = create_usdc_token(&env, &admin);
149+
150+
let platform_fee = 3 * 100;
151+
let roles = Roles {
152+
approver: approver.clone(),
153+
service_provider: service_provider.clone(),
154+
platform_address: platform.clone(),
155+
release_signer: release_signer.clone(),
156+
dispute_resolver: dispute_resolver.clone(),
157+
};
158+
let flags = Flags {
159+
disputed: false,
160+
released: false,
161+
resolved: false,
162+
approved: false,
163+
};
164+
let trustline = Trustline {
165+
address: usdc.0.address.clone(),
166+
};
167+
let milestones = vec![
168+
&env,
169+
Milestone {
170+
description: String::from_str(&env, "m1"),
171+
status: String::from_str(&env, "Pending"),
172+
evidence: String::from_str(&env, "e"),
173+
amount: 100_000,
174+
flags: flags.clone(),
175+
receiver: service_provider.clone(),
176+
},
177+
];
178+
let esc = Escrow {
179+
engagement_id: String::from_str(&env, "eng"),
180+
title: String::from_str(&env, "t"),
181+
description: String::from_str(&env, "d"),
182+
roles: roles.clone(),
183+
platform_fee,
184+
milestones: milestones.clone(),
185+
trustline,
186+
receiver_memo: 0,
187+
};
188+
let test = create_escrow_contract(&env);
189+
let client = test.client;
190+
client.initialize_escrow(&esc);
191+
192+
// Process the single milestone fully and leave leftover of 10_000
193+
usdc.1.mint(&client.address, &110_000);
194+
client.approve_milestone(&0, &approver);
195+
client.release_milestone_funds(&release_signer, &trustless_work_address, &0);
196+
197+
// Attacker provides any distributions but is not resolver
198+
let mut dist: Map<Address, i128> = Map::new(&env);
199+
dist.set(service_provider.clone(), 10_000);
200+
let res = client.try_withdraw_remaining_funds(&attacker, &trustless_work_address, &dist);
201+
assert!(res.is_err(), "Only dispute_resolver should be allowed");
202+
}
203+
204+
#[test]
205+
fn test_withdraw_remaining_funds_not_fully_processed() {
206+
let env = Env::default();
207+
env.mock_all_auths();
208+
let admin = Address::generate(&env);
209+
let approver = Address::generate(&env);
210+
let service_provider = Address::generate(&env);
211+
let platform = Address::generate(&env);
212+
let release_signer = Address::generate(&env);
213+
let dispute_resolver = Address::generate(&env);
214+
let trustless_work_address = Address::generate(&env);
215+
let usdc = create_usdc_token(&env, &admin);
216+
217+
let platform_fee = 3 * 100;
218+
let roles = Roles {
219+
approver: approver.clone(),
220+
service_provider: service_provider.clone(),
221+
platform_address: platform.clone(),
222+
release_signer: release_signer.clone(),
223+
dispute_resolver: dispute_resolver.clone(),
224+
};
225+
let flags = Flags {
226+
disputed: false,
227+
released: false,
228+
resolved: false,
229+
approved: false,
230+
};
231+
let trustline = Trustline {
232+
address: usdc.0.address.clone(),
233+
};
234+
let milestones = vec![
235+
&env,
236+
Milestone {
237+
description: String::from_str(&env, "m1"),
238+
status: String::from_str(&env, "Pending"),
239+
evidence: String::from_str(&env, "e"),
240+
amount: 100_000,
241+
flags: flags.clone(),
242+
receiver: service_provider.clone(),
243+
},
244+
Milestone {
245+
description: String::from_str(&env, "m2"),
246+
status: String::from_str(&env, "Pending"),
247+
evidence: String::from_str(&env, "e"),
248+
amount: 100_000,
249+
flags: flags.clone(),
250+
receiver: service_provider.clone(),
251+
},
252+
];
253+
let esc = Escrow {
254+
engagement_id: String::from_str(&env, "eng"),
255+
title: String::from_str(&env, "t"),
256+
description: String::from_str(&env, "d"),
257+
roles: roles.clone(),
258+
platform_fee,
259+
milestones: milestones.clone(),
260+
trustline,
261+
receiver_memo: 0,
262+
};
263+
let test = create_escrow_contract(&env);
264+
let client = test.client;
265+
client.initialize_escrow(&esc);
266+
267+
usdc.1.mint(&client.address, &220_000);
268+
// Process only first milestone; second remains pending
269+
client.approve_milestone(&0, &approver);
270+
client.release_milestone_funds(&release_signer, &trustless_work_address, &0);
271+
272+
// Try withdraw while second milestone not processed
273+
let mut dist: Map<Address, i128> = Map::new(&env);
274+
dist.set(service_provider.clone(), 10_000);
275+
let res =
276+
client.try_withdraw_remaining_funds(&dispute_resolver, &trustless_work_address, &dist);
277+
assert!(
278+
res.is_err(),
279+
"Should fail when not all milestones are processed"
280+
);
281+
}
282+
283+
#[test]
284+
fn test_withdraw_remaining_funds_zero_balance_ok() {
285+
let env = Env::default();
286+
env.mock_all_auths();
287+
let admin = Address::generate(&env);
288+
let approver = Address::generate(&env);
289+
let service_provider = Address::generate(&env);
290+
let platform = Address::generate(&env);
291+
let release_signer = Address::generate(&env);
292+
let dispute_resolver = Address::generate(&env);
293+
let trustless_work_address = Address::generate(&env);
294+
let usdc = create_usdc_token(&env, &admin);
295+
296+
let platform_fee = 3 * 100;
297+
let roles = Roles {
298+
approver: approver.clone(),
299+
service_provider: service_provider.clone(),
300+
platform_address: platform.clone(),
301+
release_signer: release_signer.clone(),
302+
dispute_resolver: dispute_resolver.clone(),
303+
};
304+
let flags = Flags {
305+
disputed: false,
306+
released: false,
307+
resolved: false,
308+
approved: false,
309+
};
310+
let trustline = Trustline {
311+
address: usdc.0.address.clone(),
312+
};
313+
let milestones = vec![
314+
&env,
315+
Milestone {
316+
description: String::from_str(&env, "m1"),
317+
status: String::from_str(&env, "Pending"),
318+
evidence: String::from_str(&env, "e"),
319+
amount: 100_000,
320+
flags: flags.clone(),
321+
receiver: service_provider.clone(),
322+
},
323+
Milestone {
324+
description: String::from_str(&env, "m2"),
325+
status: String::from_str(&env, "Pending"),
326+
evidence: String::from_str(&env, "e"),
327+
amount: 100_000,
328+
flags: flags.clone(),
329+
receiver: service_provider.clone(),
330+
},
331+
];
332+
let esc = Escrow {
333+
engagement_id: String::from_str(&env, "eng"),
334+
title: String::from_str(&env, "t"),
335+
description: String::from_str(&env, "d"),
336+
roles: roles.clone(),
337+
platform_fee,
338+
milestones: milestones.clone(),
339+
trustline,
340+
receiver_memo: 0,
341+
};
342+
let test = create_escrow_contract(&env);
343+
let client = test.client;
344+
client.initialize_escrow(&esc);
345+
346+
// Fund exactly the total milestones 200_000; after releases, no leftover
347+
usdc.1.mint(&client.address, &200_000);
348+
client.approve_milestone(&0, &approver);
349+
client.approve_milestone(&1, &approver);
350+
client.release_milestone_funds(&release_signer, &trustless_work_address, &0);
351+
client.release_milestone_funds(&release_signer, &trustless_work_address, &1);
352+
353+
assert_eq!(usdc.0.balance(&client.address), 0);
354+
355+
// With empty distributions total == 0, we now expect an error (TotalAmountCannotBeZero)
356+
let dist: Map<Address, i128> = Map::new(&env);
357+
let res =
358+
client.try_withdraw_remaining_funds(&dispute_resolver, &trustless_work_address, &dist);
359+
assert!(
360+
res.is_err(),
361+
"Expected error when total distribution amount is zero"
362+
);
363+
}

0 commit comments

Comments
 (0)