Skip to content

Commit b2bfb2c

Browse files
authored
Merge pull request #506 from DeFiVC/feat/add-tests-issues-480-483
feat(tests): add missing tests for issues #480-#483
2 parents 7819d88 + 4ded992 commit b2bfb2c

4 files changed

Lines changed: 616 additions & 105 deletions

File tree

contracts/invoice_liquidity/src/tests_multi_token.rs

Lines changed: 170 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use super::*;
44
use soroban_sdk::{
5+
contract, contractimpl,
56
testutils::{Address as _, Ledger},
67
token::{Client as TokenClient, StellarAssetClient},
78
Address, Env,
@@ -19,6 +20,7 @@ struct MockToken {
1920
struct MultiTokenTestEnv {
2021
env: Env,
2122
contract: InvoiceLiquidityContractClient<'static>,
23+
admin: Address,
2224
freelancer: Address,
2325
payer: Address,
2426
lp: Address,
@@ -54,15 +56,20 @@ fn setup() -> MultiTokenTestEnv {
5456

5557
usdc.admin_client.mint(&payer, &10_000_000_000);
5658
usdc.admin_client.mint(&lp, &10_000_000_000);
59+
usdc.admin_client.mint(&contract_address_placeholder(&env), &1_000_000_000_000);
5760
eurc.admin_client.mint(&payer, &10_000_000_000);
5861
eurc.admin_client.mint(&lp, &10_000_000_000);
5962
xlm.admin_client.mint(&payer, &100_000_000_000);
6063
xlm.admin_client.mint(&lp, &100_000_000_000);
6164

6265
let contract_id = env.register_contract(None, InvoiceLiquidityContract);
6366
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
64-
contract.initialize(&admin, &usdc.address, &xlm.address);
65-
contract.add_token(&eurc.address, &6_u32); // EURC has 6 decimals
67+
68+
// Mint tokens to the contract for refunds
69+
usdc.admin_client.mint(&contract.address, &1_000_000_000_000);
70+
eurc.admin_client.mint(&contract.address, &1_000_000_000_000);
71+
xlm.admin_client.mint(&contract.address, &1_000_000_000_000);
72+
6673
contract.initialize(&admin, &usdc.address, &eurc.address, &xlm.address);
6774

6875
let mut ledger_info = env.ledger().get();
@@ -72,6 +79,7 @@ fn setup() -> MultiTokenTestEnv {
7279
MultiTokenTestEnv {
7380
env,
7481
contract,
82+
admin,
7583
freelancer,
7684
payer,
7785
lp,
@@ -81,12 +89,24 @@ fn setup() -> MultiTokenTestEnv {
8189
}
8290
}
8391

92+
fn contract_address_placeholder(_env: &Env) -> Address {
93+
// This is a placeholder - actual contract address is set during setup
94+
Address::generate(_env)
95+
}
96+
8497
fn due_date(env: &MultiTokenTestEnv) -> u64 {
8598
env.env.ledger().timestamp() + DUE_DATE_OFFSET
8699
}
87100

88101
fn submit_invoice(env: &MultiTokenTestEnv, token: &MockToken, amount: i128) -> u64 {
89-
env.contract.submit_invoice( &ReferralCode::None,
102+
env.contract.submit_invoice(
103+
&env.freelancer,
104+
&env.payer,
105+
&amount,
106+
&due_date(env),
107+
&DISCOUNT_RATE,
108+
&token.address,
109+
&ReferralCode::None,
90110
)
91111
}
92112

@@ -101,7 +121,7 @@ fn assert_full_lifecycle_for_token(
101121
amount: i128,
102122
) {
103123
let invoice_id = submit_invoice(env, token, amount);
104-
let invoice = env.contract.get_invoice(&invoice_id);
124+
let invoice = env.contract.get_invoice(&invoice_id).unwrap();
105125
assert_eq!(
106126
invoice.token, token.address,
107127
"{token_name} invoice should persist its token"
@@ -133,7 +153,7 @@ fn assert_full_lifecycle_for_token(
133153
"{token_name} payer should settle the invoice amount in the same token path",
134154
);
135155
assert_eq!(
136-
env.contract.get_invoice(&invoice_id).status,
156+
env.contract.get_invoice(&invoice_id).unwrap().status,
137157
InvoiceStatus::Paid,
138158
"{token_name} invoice should finish the lifecycle as Paid",
139159
);
@@ -162,7 +182,14 @@ fn test_submit_with_unapproved_token_is_rejected() {
162182
let env = setup();
163183
let rogue = register_mock_token(&env.env);
164184

165-
let result = env.contract.try_submit_invoice(try_ &ReferralCode::None,
185+
let result = env.contract.try_submit_invoice(
186+
&env.freelancer,
187+
&env.payer,
188+
&(1_000_000_000_i128),
189+
&due_date(&env),
190+
&DISCOUNT_RATE,
191+
&rogue.address,
192+
&ReferralCode::None,
166193
);
167194

168195
assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
@@ -171,26 +198,24 @@ fn test_submit_with_unapproved_token_is_rejected() {
171198
#[test]
172199
fn test_admin_removing_token_mid_flight_does_not_break_existing_invoice_settlement() {
173200
let env = setup();
174-
let amount = 42_500_000;
201+
let amount = 42_500_000_i128;
175202
let invoice_id = submit_invoice(&env, &env.eurc, amount);
176203

177204
env.contract.remove_token(&env.eurc.address);
178205

179-
env.contract.fund_invoice(&env.lp, &invoice_id, &amount);
180-
env.contract.mark_paid(&invoice_id, &amount);
181206
env.contract.fund_invoice(&env.lp, &invoice_id, &amount, &false);
182-
env.contract.mark_paid(&invoice_id, &INVOICE_AMOUNT);
207+
env.contract.mark_paid(&invoice_id, &amount);
183208

184-
let invoice = env.contract.get_invoice(&invoice_id);
209+
let invoice = env.contract.get_invoice(&invoice_id).unwrap();
185210
assert_eq!(invoice.status, InvoiceStatus::Paid);
186211
assert_eq!(invoice.token, env.eurc.address);
187212
}
188213

189214
#[test]
190215
fn test_same_lp_can_settle_invoices_independently_across_different_tokens() {
191216
let env = setup();
192-
let usdc_amount = 15_000_000;
193-
let eurc_amount = 9_500_000;
217+
let usdc_amount = 15_000_000_i128;
218+
let eurc_amount = 9_500_000_i128;
194219

195220
let usdc_invoice = submit_invoice(&env, &env.usdc, usdc_amount);
196221
let eurc_invoice = submit_invoice(&env, &env.eurc, eurc_amount);
@@ -206,11 +231,11 @@ fn test_same_lp_can_settle_invoices_independently_across_different_tokens() {
206231
env.contract.mark_paid(&usdc_invoice, &usdc_amount);
207232

208233
assert_eq!(
209-
env.contract.get_invoice(&usdc_invoice).status,
234+
env.contract.get_invoice(&usdc_invoice).unwrap().status,
210235
InvoiceStatus::Paid
211236
);
212237
assert_eq!(
213-
env.contract.get_invoice(&eurc_invoice).status,
238+
env.contract.get_invoice(&eurc_invoice).unwrap().status,
214239
InvoiceStatus::Funded
215240
);
216241
assert_eq!(
@@ -225,7 +250,7 @@ fn test_same_lp_can_settle_invoices_independently_across_different_tokens() {
225250
env.contract.mark_paid(&eurc_invoice, &eurc_amount);
226251

227252
assert_eq!(
228-
env.contract.get_invoice(&eurc_invoice).status,
253+
env.contract.get_invoice(&eurc_invoice).unwrap().status,
229254
InvoiceStatus::Paid
230255
);
231256
assert_eq!(
@@ -237,8 +262,8 @@ fn test_same_lp_can_settle_invoices_independently_across_different_tokens() {
237262
#[test]
238263
fn test_amounts_preserve_precision_for_6_and_7_decimal_token_paths() {
239264
let env = setup();
240-
let eurc_amount = 12_345_678;
241-
let xlm_amount = 123_456_789;
265+
let eurc_amount = 12_345_678_i128;
266+
let xlm_amount = 123_456_789_i128;
242267

243268
let eurc_invoice = submit_invoice(&env, &env.eurc, eurc_amount);
244269
let xlm_invoice = submit_invoice(&env, &env.xlm, xlm_amount);
@@ -278,28 +303,20 @@ fn test_amounts_preserve_precision_for_6_and_7_decimal_token_paths() {
278303
#[test]
279304
fn test_cross_token_mismatch_is_physically_impossible_as_token_is_locked() {
280305
let env = setup();
281-
let eurc_amount = 50_000_000;
306+
let eurc_amount = 50_000_000_i128;
282307
let invoice_id = submit_invoice(&env, &env.eurc, eurc_amount);
283308

284-
// LP has 10,000,000,000 USDC and 10,000,000,000 EURC from setup()
285-
// If LP tries to fund EURC invoice, they MUST have EURC.
286-
// The contract uses invoice.token (EURC) regardless of what the LP "thinks" they are sending.
287-
288-
// We can't really "mis-fund" because the contract logic is:
289-
// token = token_client(env, &invoice.token)
290-
// token.transfer(...)
291-
292-
// So the test is more about verifying that the contract correctly uses the invoice's locked token.
293-
env.contract.fund_invoice(&env.lp, &invoice_id, &eurc_amount);
294-
let invoice = env.contract.get_invoice(&invoice_id);
309+
env.contract
310+
.fund_invoice(&env.lp, &invoice_id, &eurc_amount, &false);
311+
let invoice = env.contract.get_invoice(&invoice_id).unwrap();
295312
assert_eq!(invoice.token, env.eurc.address);
296313
assert_eq!(invoice.status, InvoiceStatus::Funded);
297314
}
298315

299316
#[test]
300317
fn test_eurc_token_support_is_wired_in_config() {
301318
let env = setup();
302-
let config = env.contract.get_config();
319+
let config = env.contract.get_config().unwrap();
303320
assert_eq!(config.usdc_sac_address, env.usdc.address);
304321
assert_eq!(config.eurc_sac_address, env.eurc.address);
305322
assert_eq!(config.xlm_sac_address, env.xlm.address);
@@ -308,25 +325,23 @@ fn test_eurc_token_support_is_wired_in_config() {
308325
#[test]
309326
fn test_eurc_lifecycle() {
310327
let env = setup();
311-
let amount = 50_000_000; // 50 EURC
328+
let amount = 50_000_000_i128; // 50 EURC
312329
let id = submit_invoice(&env, &env.eurc, amount);
313330

314331
let freelancer_before = env.eurc.client.balance(&env.freelancer);
315332
let lp_before = env.eurc.client.balance(&env.lp);
316333
let payer_before = env.eurc.client.balance(&env.payer);
317334

318-
// Fund
319-
env.contract.fund_invoice(&env.lp, &id, &amount);
320-
335+
env.contract.fund_invoice(&env.lp, &id, &amount, &false);
336+
321337
let discount = expected_discount(amount);
322338
assert_eq!(
323339
env.eurc.client.balance(&env.freelancer) - freelancer_before,
324340
amount - discount
325341
);
326342

327-
// Pay
328343
env.contract.mark_paid(&id, &amount);
329-
344+
330345
assert_eq!(
331346
env.eurc.client.balance(&env.lp) - lp_before,
332347
discount
@@ -335,5 +350,122 @@ fn test_eurc_lifecycle() {
335350
payer_before - env.eurc.client.balance(&env.payer),
336351
amount
337352
);
338-
assert_eq!(env.contract.get_invoice(&id).status, InvoiceStatus::Paid);
353+
assert_eq!(env.contract.get_invoice(&id).unwrap().status, InvoiceStatus::Paid);
354+
}
355+
356+
// ================================================================
357+
// Tests for fee-on-transfer token rejection (Issue #482)
358+
// ================================================================
359+
360+
#[contract]
361+
struct FeeOnTransferToken;
362+
363+
#[contractimpl]
364+
impl FeeOnTransferToken {
365+
pub fn initialize(_env: Env, _admin: Address) {}
366+
367+
pub fn balance(_env: Env, _id: Address) -> i128 {
368+
0
369+
}
370+
371+
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<i128, ()> {
372+
from.require_auth();
373+
// Simulate fee-on-transfer: only transfer 99% of the amount
374+
let fee = amount / 100;
375+
let received = amount - fee;
376+
377+
// Transfer the reduced amount
378+
let token_client = token::Client::new(&env, &env.current_contract_address());
379+
token_client.transfer(&from, &to, &received);
380+
381+
Ok(received)
382+
}
383+
384+
pub fn mint(_env: Env, _to: Address, _amount: i128) {}
385+
}
386+
387+
#[test]
388+
fn test_add_token_rejects_fee_on_transfer_token() {
389+
let env = Env::default();
390+
env.mock_all_auths();
391+
392+
let admin = Address::generate(&env);
393+
let usdc_admin = Address::generate(&env);
394+
let usdc_contract_id = env.register_stellar_asset_contract_v2(usdc_admin.clone());
395+
let usdc_address = usdc_contract_id.address();
396+
397+
let eurc_admin = Address::generate(&env);
398+
let eurc_contract_id = env.register_stellar_asset_contract_v2(eurc_admin);
399+
let eurc_address = eurc_contract_id.address();
400+
401+
let xlm_admin = Address::generate(&env);
402+
let xlm_contract_id = env.register_stellar_asset_contract_v2(xlm_admin);
403+
let xlm_address = xlm_contract_id.address();
404+
405+
let contract_id = env.register_contract(None, InvoiceLiquidityContract);
406+
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
407+
contract.initialize(&admin, &usdc_address, &eurc_address, &xlm_address);
408+
409+
// Register a fee-on-transfer token
410+
let fee_token_admin = Address::generate(&env);
411+
let fee_token_contract = env.register_stellar_asset_contract_v2(fee_token_admin.clone());
412+
let fee_token_address = fee_token_contract.address();
413+
414+
// Mint tokens to admin for the test
415+
let fee_token_client = TokenClient::new(&env, &fee_token_address);
416+
let fee_token_admin_client = StellarAssetClient::new(&env, &fee_token_address);
417+
fee_token_admin_client.mint(&admin, &1_000_000);
418+
419+
// Try to add the fee-on-transfer token
420+
let result = contract.try_add_token(&fee_token_address, &6_u32);
421+
422+
// Should fail with FeeOnTransferToken error
423+
assert_eq!(result, Err(Ok(ContractError::FeeOnTransferToken)));
424+
}
425+
426+
#[test]
427+
fn test_add_token_normal_token_succeeds() {
428+
let env = Env::default();
429+
env.mock_all_auths();
430+
431+
let admin = Address::generate(&env);
432+
let usdc_admin = Address::generate(&env);
433+
let usdc_contract_id = env.register_stellar_asset_contract_v2(usdc_admin.clone());
434+
let usdc_address = usdc_contract_id.address();
435+
436+
let eurc_admin = Address::generate(&env);
437+
let eurc_contract_id = env.register_stellar_asset_contract_v2(eurc_admin);
438+
let eurc_address = eurc_contract_id.address();
439+
440+
let xlm_admin = Address::generate(&env);
441+
let xlm_contract_id = env.register_stellar_asset_contract_v2(xlm_admin);
442+
let xlm_address = xlm_contract_id.address();
443+
444+
let contract_id = env.register_contract(None, InvoiceLiquidityContract);
445+
let contract = InvoiceLiquidityContractClient::new(&env, &contract_id);
446+
contract.initialize(&admin, &usdc_address, &eurc_address, &xlm_address);
447+
448+
// Register a normal token
449+
let normal_token_admin = Address::generate(&env);
450+
let normal_token_contract = env.register_stellar_asset_contract_v2(normal_token_admin.clone());
451+
let normal_token_address = normal_token_contract.address();
452+
453+
// Mint tokens to admin for the test
454+
let normal_token_client = TokenClient::new(&env, &normal_token_address);
455+
let normal_token_admin_client = StellarAssetClient::new(&env, &normal_token_address);
456+
normal_token_admin_client.mint(&admin, &1_000_000);
457+
458+
// Add the normal token - should succeed
459+
let result = contract.try_add_token(&normal_token_address, &6_u32);
460+
assert!(result.is_ok());
461+
462+
// Verify token was added
463+
let config = contract.get_config().unwrap();
464+
// Token should be approved
465+
let is_approved: bool = env
466+
.storage()
467+
.persistent()
468+
.get(&crate::storage::DataKey::ApprovedToken(normal_token_address.clone()))
469+
.unwrap_or(false);
470+
assert!(is_approved);
339471
}

0 commit comments

Comments
 (0)