22
33use super :: * ;
44use 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 {
1920struct 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+
8497fn due_date ( env : & MultiTokenTestEnv ) -> u64 {
8598 env. env . ledger ( ) . timestamp ( ) + DUE_DATE_OFFSET
8699}
87100
88101fn 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]
172199fn 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]
190215fn 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]
238263fn 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]
279304fn 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]
300317fn 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]
309326fn 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