From 68403cb7e0dfeee30428cf1318416c18c64722a3 Mon Sep 17 00:00:00 2001 From: Charlotte Date: Thu, 26 Sep 2024 08:53:22 +0800 Subject: [PATCH 1/2] test add liquidity --- contracts/.tool-versions | 3 +- .../contracts/ramps/revolut/interface.cairo | 1 + .../src/contracts/ramps/revolut/revolut.cairo | 4 +- .../ramps/revolut/revolut_test.cairo | 216 ++++++++++++++++-- 4 files changed, 207 insertions(+), 17 deletions(-) diff --git a/contracts/.tool-versions b/contracts/.tool-versions index ab54af5..120e90d 100644 --- a/contracts/.tool-versions +++ b/contracts/.tool-versions @@ -1 +1,2 @@ -scarb 2.8.0 +scarb 2.8.2 +starknet-foundry 0.30.0 \ No newline at end of file diff --git a/contracts/src/contracts/ramps/revolut/interface.cairo b/contracts/src/contracts/ramps/revolut/interface.cairo index e977848..1f93846 100644 --- a/contracts/src/contracts/ramps/revolut/interface.cairo +++ b/contracts/src/contracts/ramps/revolut/interface.cairo @@ -54,6 +54,7 @@ pub trait ZKRampABI { fn liquidity_share_request(self: @TState, offchain_id: OffchainId) -> Option; fn add_liquidity(ref self: TState, amount: u256, offchain_id: OffchainId); fn retrieve_liquidity(ref self: TState, liquidity_key: LiquidityKey); + fn initiate_liquidity_retrieval(ref self: TState, liquidity_key: LiquidityKey); fn initiate_liquidity_withdrawal( ref self: TState, liquidity_key: LiquidityKey, amount: u256, offchain_id: OffchainId ); diff --git a/contracts/src/contracts/ramps/revolut/revolut.cairo b/contracts/src/contracts/ramps/revolut/revolut.cairo index ad4bad1..958ca70 100644 --- a/contracts/src/contracts/ramps/revolut/revolut.cairo +++ b/contracts/src/contracts/ramps/revolut/revolut.cairo @@ -83,7 +83,7 @@ pub mod RevolutRamp { #[event] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { #[flat] OwnableEvent: OwnableComponent::Event, #[flat] @@ -203,7 +203,7 @@ pub mod RevolutRamp { self.liquidity.write(liquidity_key, existing_amount + amount); // unlocks liquidity - self.locked_liquidity.write(liquidity_key, true); + self.locked_liquidity.write(liquidity_key, false); // use the escrow to lock the funds self.escrow.lock(from: caller, :token, :amount); diff --git a/contracts/src/contracts/ramps/revolut/revolut_test.cairo b/contracts/src/contracts/ramps/revolut/revolut_test.cairo index c4a7d46..8d57084 100644 --- a/contracts/src/contracts/ramps/revolut/revolut_test.cairo +++ b/contracts/src/contracts/ramps/revolut/revolut_test.cairo @@ -1,8 +1,14 @@ +use core::num::traits::Bounded; use core::starknet::{ContractAddress, get_caller_address}; -use openzeppelin::presets::interfaces::ERC20UpgradeableABIDispatcher; +use openzeppelin::presets::interfaces::{ERC20UpgradeableABIDispatcher, ERC20UpgradeableABIDispatcherTrait}; use openzeppelin::utils::serde::SerializedAppend; -use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; +use snforge_std::{ + EventSpyAssertionsTrait, spy_events, declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, + stop_cheat_caller_address, test_address +}; +use zkramp::components::registry::interface::OffchainId; use zkramp::contracts::ramps::revolut::interface::{ZKRampABIDispatcher, ZKRampABIDispatcherTrait, LiquidityKey}; +use zkramp::contracts::ramps::revolut::revolut::RevolutRamp::{Event, LiquidityAdded, LiquidityLocked}; use zkramp::tests::constants; use zkramp::tests::utils; @@ -35,31 +41,213 @@ fn setup() -> (ZKRampABIDispatcher, ERC20UpgradeableABIDispatcher) { // add_liquidity // -// #[test] -// #[should_panic(expected: 'Caller is not registered')] +#[test] +#[should_panic(expected: 'Caller is not registered')] fn test_add_liquidity_with_unregistered_offchain_id() { - panic!("Not implemented yet"); + let contract_address = test_address(); + + let (revolut_ramp, _) = setup(); + + start_cheat_caller_address(contract_address, constants::CALLER()); + + // adds liquidity with unregistered offchain id + revolut_ramp.add_liquidity(42, constants::REVOLUT_ID()); } -// #[test] -// #[should_panic(expected: 'Amount cannot be null')] +#[test] +#[should_panic(expected: 'Amount cannot be null')] fn test_add_zero_liquidity() { - panic!("Not implemented yet"); + let contract_address = test_address(); + let offchain_id: OffchainId = constants::REVOLUT_ID(); + + let (revolut_ramp, _) = setup(); + + start_cheat_caller_address(contract_address, constants::CALLER()); + + // registers offchain id + revolut_ramp.register(offchain_id); + + // adds zero liquidity + revolut_ramp.add_liquidity(0, offchain_id); } -// #[test] +#[test] fn test_add_liquidity() { - panic!("Not implemented yet"); + let contract_address = test_address(); + let (revolut_ramp, erc20) = setup(); + let mut spy = spy_events(); + + let offchain_id: OffchainId = constants::REVOLUT_ID(); + let amount: u256 = 42; + let liquidity_key = LiquidityKey { owner: contract_address, offchain_id: offchain_id }; + + // funds the account + start_cheat_caller_address(erc20.contract_address, constants::OWNER()); + erc20.transfer(recipient: contract_address, amount: amount); + stop_cheat_caller_address(erc20.contract_address); + + // approves spender + start_cheat_caller_address(erc20.contract_address, contract_address); + erc20.approve(spender: contract_address, amount: Bounded::MAX); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), 0); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); + assert_eq!(erc20.balance_of(contract_address), amount); + + // registers offchain id and adds liquidity + revolut_ramp.register(offchain_id); + revolut_ramp.add_liquidity(amount, offchain_id); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount); + assert_eq!(erc20.balance_of(contract_address), 0); + + spy + .assert_emitted( + @array![ + ( + revolut_ramp.contract_address, + Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount }) + ) + ] + ) } -// #[test] +#[test] fn test_add_liquidity_twice() { - panic!("Not implemented yet"); + let contract_address = test_address(); + let (revolut_ramp, erc20) = setup(); + let mut spy = spy_events(); + + let offchain_id: OffchainId = constants::REVOLUT_ID(); + let amount: u256 = 42; + let liquidity_key = LiquidityKey { owner: contract_address, offchain_id: offchain_id }; + + // funds the account + start_cheat_caller_address(erc20.contract_address, constants::OWNER()); + erc20.transfer(recipient: contract_address, amount: amount * 2); + stop_cheat_caller_address(erc20.contract_address); + + // approves spender + start_cheat_caller_address(erc20.contract_address, contract_address); + erc20.approve(spender: contract_address, amount: Bounded::MAX); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), 0); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); + assert_eq!(erc20.balance_of(contract_address), amount * 2); + + // registers offchain id and adds liquidity + revolut_ramp.register(offchain_id); + revolut_ramp.add_liquidity(amount, offchain_id); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount); + assert_eq!(erc20.balance_of(contract_address), amount); + + spy + .assert_emitted( + @array![ + ( + revolut_ramp.contract_address, + Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount }) + ) + ] + ); + + // adds liquidity + revolut_ramp.add_liquidity(amount, offchain_id); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount * 2); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount * 2); + assert_eq!(erc20.balance_of(contract_address), 0); + + spy + .assert_emitted( + @array![ + ( + revolut_ramp.contract_address, + Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount }) + ) + ] + ) } -// #[test] +#[test] fn test_add_liquidity_to_locked_liquidity() { - panic!("Not implemented yet"); + let contract_address = test_address(); + let (revolut_ramp, erc20) = setup(); + let mut spy = spy_events(); + + let offchain_id = constants::REVOLUT_ID(); + let amount1: u256 = 42; + let amount2: u256 = 50; + let liquidity_key = LiquidityKey { owner: contract_address, offchain_id: offchain_id }; + + // funds the account + start_cheat_caller_address(erc20.contract_address, constants::OWNER()); + erc20.transfer(recipient: contract_address, amount: amount1 + amount2); + stop_cheat_caller_address(erc20.contract_address); + + // approves spender + start_cheat_caller_address(erc20.contract_address, contract_address); + erc20.approve(spender: contract_address, amount: Bounded::MAX); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), 0); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); + assert_eq!(erc20.balance_of(contract_address), amount1 + amount2); + + // registers offchain id and adds liquidity + revolut_ramp.register(offchain_id); + revolut_ramp.add_liquidity(amount1, offchain_id); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount1); + assert_eq!(erc20.balance_of(contract_address), amount2); + + spy + .assert_emitted( + @array![ + ( + revolut_ramp.contract_address, + Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount1 }) + ) + ] + ); + + // locks liquidity + revolut_ramp.initiate_liquidity_retrieval(liquidity_key); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); + assert_eq!(erc20.balance_of(contract_address), amount2); + + spy + .assert_emitted( + @array![ + ( + revolut_ramp.contract_address, + Event::LiquidityLocked(LiquidityLocked { liquidity_key: liquidity_key }) + ) + ] + ); + + // adds liquidity + start_cheat_caller_address(erc20.contract_address, contract_address); + revolut_ramp.add_liquidity(amount2, offchain_id); + + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1 + amount2); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount1 + amount2); + assert_eq!(erc20.balance_of(contract_address), 0); + + spy + .assert_emitted( + @array![ + ( + revolut_ramp.contract_address, + Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount2 }) + ) + ] + ) } // From bb0a064f8281c4d373fcddfca6b9b511c36e2b22 Mon Sep 17 00:00:00 2001 From: 0xChqrles Date: Thu, 26 Sep 2024 13:18:26 +0200 Subject: [PATCH 2/2] small improvements --- .../ramps/revolut/revolut_test.cairo | 229 ++++++++---------- 1 file changed, 105 insertions(+), 124 deletions(-) diff --git a/contracts/src/contracts/ramps/revolut/revolut_test.cairo b/contracts/src/contracts/ramps/revolut/revolut_test.cairo index 8d57084..2e7d401 100644 --- a/contracts/src/contracts/ramps/revolut/revolut_test.cairo +++ b/contracts/src/contracts/ramps/revolut/revolut_test.cairo @@ -6,9 +6,8 @@ use snforge_std::{ EventSpyAssertionsTrait, spy_events, declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address, test_address }; -use zkramp::components::registry::interface::OffchainId; use zkramp::contracts::ramps::revolut::interface::{ZKRampABIDispatcher, ZKRampABIDispatcherTrait, LiquidityKey}; -use zkramp::contracts::ramps::revolut::revolut::RevolutRamp::{Event, LiquidityAdded, LiquidityLocked}; +use zkramp::contracts::ramps::revolut::revolut::RevolutRamp::{Event, LiquidityAdded}; use zkramp::tests::constants; use zkramp::tests::utils; @@ -45,128 +44,113 @@ fn setup() -> (ZKRampABIDispatcher, ERC20UpgradeableABIDispatcher) { #[should_panic(expected: 'Caller is not registered')] fn test_add_liquidity_with_unregistered_offchain_id() { let contract_address = test_address(); - let (revolut_ramp, _) = setup(); + let liquidity_owner = constants::OTHER(); + let offchain_id = constants::REVOLUT_ID(); + let amount = 42; - start_cheat_caller_address(contract_address, constants::CALLER()); + start_cheat_caller_address(contract_address, liquidity_owner); - // adds liquidity with unregistered offchain id - revolut_ramp.add_liquidity(42, constants::REVOLUT_ID()); + // add liquidity with unregistered offchain id + revolut_ramp.add_liquidity(:amount, :offchain_id); } #[test] #[should_panic(expected: 'Amount cannot be null')] fn test_add_zero_liquidity() { let contract_address = test_address(); - let offchain_id: OffchainId = constants::REVOLUT_ID(); - let (revolut_ramp, _) = setup(); + let liquidity_owner = constants::OTHER(); + let offchain_id = constants::REVOLUT_ID(); + let amount = 0; - start_cheat_caller_address(contract_address, constants::CALLER()); + start_cheat_caller_address(contract_address, liquidity_owner); - // registers offchain id - revolut_ramp.register(offchain_id); + // register offchain id + revolut_ramp.register(:offchain_id); - // adds zero liquidity - revolut_ramp.add_liquidity(0, offchain_id); + // add zero liquidity + revolut_ramp.add_liquidity(:amount, :offchain_id); } #[test] fn test_add_liquidity() { - let contract_address = test_address(); let (revolut_ramp, erc20) = setup(); let mut spy = spy_events(); + let liquidity_owner = constants::OTHER(); + let offchain_id = constants::REVOLUT_ID(); + let amount = 42; + let liquidity_key = LiquidityKey { owner: liquidity_owner, offchain_id }; - let offchain_id: OffchainId = constants::REVOLUT_ID(); - let amount: u256 = 42; - let liquidity_key = LiquidityKey { owner: contract_address, offchain_id: offchain_id }; - - // funds the account - start_cheat_caller_address(erc20.contract_address, constants::OWNER()); - erc20.transfer(recipient: contract_address, amount: amount); - stop_cheat_caller_address(erc20.contract_address); + // fund the account + fund_and_approve(token: erc20, recipient: liquidity_owner, spender: revolut_ramp.contract_address, :amount); - // approves spender - start_cheat_caller_address(erc20.contract_address, contract_address); - erc20.approve(spender: contract_address, amount: Bounded::MAX); + // register offchain ID + start_cheat_caller_address(revolut_ramp.contract_address, liquidity_owner); + revolut_ramp.register(:offchain_id); - assert_eq!(revolut_ramp.all_liquidity(liquidity_key), 0); - assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); - assert_eq!(erc20.balance_of(contract_address), amount); + // assert state before + assert_eq!(revolut_ramp.all_liquidity(:liquidity_key), 0); + assert_eq!(revolut_ramp.available_liquidity(:liquidity_key), 0); + assert_eq!(erc20.balance_of(liquidity_owner), amount); - // registers offchain id and adds liquidity - revolut_ramp.register(offchain_id); + // add liquidity revolut_ramp.add_liquidity(amount, offchain_id); - assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount); - assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount); - assert_eq!(erc20.balance_of(contract_address), 0); + // assert state after + assert_eq!(revolut_ramp.all_liquidity(:liquidity_key), amount); + assert_eq!(revolut_ramp.available_liquidity(:liquidity_key), amount); + assert_eq!(erc20.balance_of(liquidity_owner), 0); + // check on emitted events spy .assert_emitted( - @array![ - ( - revolut_ramp.contract_address, - Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount }) - ) - ] + @array![(revolut_ramp.contract_address, Event::LiquidityAdded(LiquidityAdded { liquidity_key, amount }))] ) } #[test] fn test_add_liquidity_twice() { - let contract_address = test_address(); let (revolut_ramp, erc20) = setup(); let mut spy = spy_events(); + let liquidity_owner = constants::OTHER(); + let offchain_id = constants::REVOLUT_ID(); + let amount1 = 42; + let amount2 = 75; + let liquidity_key = LiquidityKey { owner: liquidity_owner, offchain_id }; - let offchain_id: OffchainId = constants::REVOLUT_ID(); - let amount: u256 = 42; - let liquidity_key = LiquidityKey { owner: contract_address, offchain_id: offchain_id }; - - // funds the account - start_cheat_caller_address(erc20.contract_address, constants::OWNER()); - erc20.transfer(recipient: contract_address, amount: amount * 2); - stop_cheat_caller_address(erc20.contract_address); + // fund the account + fund_and_approve( + token: erc20, recipient: liquidity_owner, spender: revolut_ramp.contract_address, amount: amount1 + amount2 + ); - // approves spender - start_cheat_caller_address(erc20.contract_address, contract_address); - erc20.approve(spender: contract_address, amount: Bounded::MAX); + // register offchain ID + start_cheat_caller_address(revolut_ramp.contract_address, liquidity_owner); + revolut_ramp.register(:offchain_id); assert_eq!(revolut_ramp.all_liquidity(liquidity_key), 0); assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); - assert_eq!(erc20.balance_of(contract_address), amount * 2); + assert_eq!(erc20.balance_of(liquidity_owner), amount1 + amount2); - // registers offchain id and adds liquidity - revolut_ramp.register(offchain_id); - revolut_ramp.add_liquidity(amount, offchain_id); + // add liquidity + revolut_ramp.add_liquidity(amount: amount1, :offchain_id); + revolut_ramp.add_liquidity(amount: amount2, :offchain_id); - assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount); - assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount); - assert_eq!(erc20.balance_of(contract_address), amount); + assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1 + amount2); + assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount1 + amount2); + assert_eq!(erc20.balance_of(liquidity_owner), 0); + // check on emitted events spy .assert_emitted( @array![ ( revolut_ramp.contract_address, - Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount }) - ) - ] - ); - - // adds liquidity - revolut_ramp.add_liquidity(amount, offchain_id); - - assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount * 2); - assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount * 2); - assert_eq!(erc20.balance_of(contract_address), 0); - - spy - .assert_emitted( - @array![ + Event::LiquidityAdded(LiquidityAdded { liquidity_key, amount: amount1 }) + ), ( revolut_ramp.contract_address, - Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount }) + Event::LiquidityAdded(LiquidityAdded { liquidity_key, amount: amount2 }) ) ] ) @@ -174,77 +158,52 @@ fn test_add_liquidity_twice() { #[test] fn test_add_liquidity_to_locked_liquidity() { - let contract_address = test_address(); let (revolut_ramp, erc20) = setup(); let mut spy = spy_events(); - + let liquidity_owner = constants::OTHER(); let offchain_id = constants::REVOLUT_ID(); - let amount1: u256 = 42; - let amount2: u256 = 50; - let liquidity_key = LiquidityKey { owner: contract_address, offchain_id: offchain_id }; + let amount1 = 42; + let amount2 = 75; + let liquidity_key = LiquidityKey { owner: liquidity_owner, offchain_id }; - // funds the account - start_cheat_caller_address(erc20.contract_address, constants::OWNER()); - erc20.transfer(recipient: contract_address, amount: amount1 + amount2); - stop_cheat_caller_address(erc20.contract_address); + // fund the account + fund_and_approve( + token: erc20, recipient: liquidity_owner, spender: revolut_ramp.contract_address, amount: amount1 + amount2 + ); - // approves spender - start_cheat_caller_address(erc20.contract_address, contract_address); - erc20.approve(spender: contract_address, amount: Bounded::MAX); + // register offchain ID + start_cheat_caller_address(revolut_ramp.contract_address, liquidity_owner); + revolut_ramp.register(:offchain_id); - assert_eq!(revolut_ramp.all_liquidity(liquidity_key), 0); - assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); - assert_eq!(erc20.balance_of(contract_address), amount1 + amount2); - - // registers offchain id and adds liquidity - revolut_ramp.register(offchain_id); - revolut_ramp.add_liquidity(amount1, offchain_id); - - assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1); - assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount1); - assert_eq!(erc20.balance_of(contract_address), amount2); - - spy - .assert_emitted( - @array![ - ( - revolut_ramp.contract_address, - Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount1 }) - ) - ] - ); + // add liquidity + revolut_ramp.add_liquidity(amount: amount1, :offchain_id); // locks liquidity - revolut_ramp.initiate_liquidity_retrieval(liquidity_key); + revolut_ramp.initiate_liquidity_retrieval(:liquidity_key); assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1); assert_eq!(revolut_ramp.available_liquidity(liquidity_key), 0); - assert_eq!(erc20.balance_of(contract_address), amount2); + assert_eq!(erc20.balance_of(liquidity_owner), amount2); - spy - .assert_emitted( - @array![ - ( - revolut_ramp.contract_address, - Event::LiquidityLocked(LiquidityLocked { liquidity_key: liquidity_key }) - ) - ] - ); - - // adds liquidity - start_cheat_caller_address(erc20.contract_address, contract_address); - revolut_ramp.add_liquidity(amount2, offchain_id); + // add liquidity again + revolut_ramp.add_liquidity(amount: amount2, :offchain_id); + // assert state after assert_eq!(revolut_ramp.all_liquidity(liquidity_key), amount1 + amount2); assert_eq!(revolut_ramp.available_liquidity(liquidity_key), amount1 + amount2); - assert_eq!(erc20.balance_of(contract_address), 0); + assert_eq!(erc20.balance_of(liquidity_owner), 0); + // check on emitted events spy .assert_emitted( @array![ ( revolut_ramp.contract_address, - Event::LiquidityAdded(LiquidityAdded { liquidity_key: liquidity_key, amount: amount2 }) + Event::LiquidityAdded(LiquidityAdded { liquidity_key, amount: amount1 }) + ), + ( + revolut_ramp.contract_address, + Event::LiquidityAdded(LiquidityAdded { liquidity_key, amount: amount2 }) ) ] ) @@ -530,3 +489,25 @@ fn test_liquidity_share_request_valid() { fn test_liquidity_share_request_withdrawn() { panic!("Not implemented yet"); } + +// +// Helpers +// + +fn fund(token: ERC20UpgradeableABIDispatcher, recipient: ContractAddress, amount: u256) { + // fund from owner + start_cheat_caller_address(token.contract_address, constants::OWNER()); + token.transfer(:recipient, :amount); + stop_cheat_caller_address(token.contract_address); +} + +fn fund_and_approve( + token: ERC20UpgradeableABIDispatcher, recipient: ContractAddress, spender: ContractAddress, amount: u256 +) { + fund(:token, :recipient, :amount); + + // approve + start_cheat_caller_address(token.contract_address, recipient); + token.approve(:spender, amount: Bounded::MAX); + stop_cheat_caller_address(token.contract_address); +}