diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index 937c53f1..323efcec 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -1,10 +1,16 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "alexandria_storage" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria?tag=v0.2.0#bcc4fa417abbc8b067ae6f2d382b127006b5077c" + [[package]] name = "contracts" version = "0.1.0" dependencies = [ + "alexandria_storage", "openzeppelin", "openzeppelin_testing", "snforge_std", diff --git a/contracts/src/NFTDutchAuction.cairo b/contracts/src/NFTDutchAuction.cairo new file mode 100644 index 00000000..dfcd3bac --- /dev/null +++ b/contracts/src/NFTDutchAuction.cairo @@ -0,0 +1,88 @@ +#[starknet::contract] +pub mod NFTDutchAuction { + use core::integer::u256; + use contracts::interfaces::INFTDutchAuction::INFTDutchAuction; + use contracts::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + struct Storage { + erc20_token: ContractAddress, + erc721_token: ContractAddress, + starting_price: u64, + seller: ContractAddress, + duration: u64, + discount_rate: u64, + start_at: u64, + expires_at: u64, + purchase_count: u128, + total_supply: u128, + } + + mod Errors { + pub const AUCTION_ENDED: felt252 = 'auction has ended'; + pub const LOW_STARTING_PRICE: felt252 = 'low starting price'; + pub const INSUFFICIENT_BALANCE: felt252 = 'insufficient balance'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + erc20_token: ContractAddress, + erc721_token: ContractAddress, + starting_price: u64, + seller: ContractAddress, + duration: u64, + discount_rate: u64, + total_supply: u128, + ) { + assert(starting_price >= discount_rate * duration, Errors::LOW_STARTING_PRICE); + + self.erc20_token.write(erc20_token); + self.erc721_token.write(erc721_token); + self.starting_price.write(starting_price); + self.seller.write(seller); + self.duration.write(duration); + self.discount_rate.write(discount_rate); + self.start_at.write(get_block_timestamp()); + self.expires_at.write(get_block_timestamp() + duration * 1000); + self.total_supply.write(total_supply); + } + + #[abi(embed_v0)] + impl NFTDutchAuction of INFTDutchAuction { + fn get_price(self: @ContractState) -> u64 { + let time_elapsed = (get_block_timestamp() - self.start_at.read()) + / 1000; // Ignore milliseconds + let discount = self.discount_rate.read() * time_elapsed; + self.starting_price.read() - discount + } + + fn buy(ref self: ContractState, token_id: u256) { + // Check duration + assert(get_block_timestamp() < self.expires_at.read(), Errors::AUCTION_ENDED); + // Check total supply + assert(self.purchase_count.read() < self.total_supply.read(), Errors::AUCTION_ENDED); + + let erc20_dispatcher = IERC20Dispatcher { contract_address: self.erc20_token.read() }; + let erc721_dispatcher = IERC721Dispatcher { + contract_address: self.erc721_token.read(), + }; + + let caller = get_caller_address(); + // Get NFT price + let price: u256 = self.get_price().into(); + let buyer_balance: u256 = erc20_dispatcher.balance_of(caller).into(); + // Ensure buyer has enough token for payment + assert(buyer_balance >= price, Errors::INSUFFICIENT_BALANCE); + // Transfer payment token from buyer to seller + erc20_dispatcher.transfer_from(caller, self.seller.read(), price.try_into().unwrap()); + // Mint token to buyer's address + erc721_dispatcher.mint(caller, token_id); + // Increase purchase count + self.purchase_count.write(self.purchase_count.read() + 1); + } + } +} diff --git a/contracts/src/interfaces.cairo b/contracts/src/interfaces.cairo index 99fb7e62..bdf9a918 100644 --- a/contracts/src/interfaces.cairo +++ b/contracts/src/interfaces.cairo @@ -1,3 +1,5 @@ pub mod IDefiVault; pub mod IStarkIdentity; pub mod timelock; +pub mod INFTDutchAuction; +pub mod IERC721; diff --git a/contracts/src/interfaces/IERC721.cairo b/contracts/src/interfaces/IERC721.cairo new file mode 100644 index 00000000..5a6defb2 --- /dev/null +++ b/contracts/src/interfaces/IERC721.cairo @@ -0,0 +1,20 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC721 { + fn get_name(self: @TContractState) -> felt252; + fn get_symbol(self: @TContractState) -> felt252; + fn get_token_uri(self: @TContractState, token_id: u256) -> felt252; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TContractState, owner: ContractAddress, operator: ContractAddress, + ) -> bool; + fn approve(ref self: TContractState, to: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ); + fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); +} diff --git a/contracts/src/interfaces/INFTDutchAuction.cairo b/contracts/src/interfaces/INFTDutchAuction.cairo new file mode 100644 index 00000000..d6ce0656 --- /dev/null +++ b/contracts/src/interfaces/INFTDutchAuction.cairo @@ -0,0 +1,5 @@ +#[starknet::interface] +pub trait INFTDutchAuction { + fn buy(ref self: TContractState, token_id: u256); + fn get_price(self: @TContractState) -> u64; +} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index ebd7d96a..db1e340b 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -6,4 +6,6 @@ pub mod merkle; pub mod starkfinder; pub mod starkidentity; pub mod mock_erc20; +pub mod mock_erc721; +pub mod NFTDutchAuction; pub mod timelock; diff --git a/contracts/src/mock_erc721.cairo b/contracts/src/mock_erc721.cairo new file mode 100644 index 00000000..02041106 --- /dev/null +++ b/contracts/src/mock_erc721.cairo @@ -0,0 +1,220 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC721 { + fn get_name(self: @TContractState) -> felt252; + fn get_symbol(self: @TContractState) -> felt252; + fn get_token_uri(self: @TContractState, token_id: u256) -> felt252; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TContractState, owner: ContractAddress, operator: ContractAddress, + ) -> bool; + fn approve(ref self: TContractState, to: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ); + fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); +} + +#[starknet::contract] +mod MockERC721 { + use starknet::{ContractAddress, get_caller_address}; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use core::num::traits::Zero; + + #[storage] + struct Storage { + name: felt252, + symbol: felt252, + owners: Map::, + balances: Map::, + token_approvals: Map::, + operator_approvals: Map::<(ContractAddress, ContractAddress), bool>, + token_uri: Map, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Approval: Approval, + Transfer: Transfer, + ApprovalForAll: ApprovalForAll, + } + + #[derive(Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + to: ContractAddress, + token_id: u256, + } + + #[derive(Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + token_id: u256, + } + + #[derive(Drop, starknet::Event)] + struct ApprovalForAll { + owner: ContractAddress, + operator: ContractAddress, + approved: bool, + } + + #[constructor] + fn constructor(ref self: ContractState, _name: felt252, _symbol: felt252) { + self.name.write(_name); + self.symbol.write(_symbol); + } + + #[abi(embed_v0)] + impl IERC721Impl of super::IERC721 { + fn get_name(self: @ContractState) -> felt252 { + self.name.read() + } + fn get_symbol(self: @ContractState) -> felt252 { + self.symbol.read() + } + fn get_token_uri(self: @ContractState, token_id: u256) -> felt252 { + assert(self._exists(token_id), 'ERC721: invalid token ID'); + self.token_uri.read(token_id) + } + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + assert(account.is_non_zero(), 'ERC721: address zero'); + self.balances.read(account) + } + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + let owner = self.owners.read(token_id); + owner + } + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + assert(self._exists(token_id), 'ERC721: invalid token ID'); + self.token_approvals.read(token_id) + } + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress, + ) -> bool { + self.operator_approvals.read((owner, operator)) + } + + fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + let owner = self.owner_of(token_id); + assert(to != owner, 'Approval to current owner'); + assert( + get_caller_address() == owner + || self.is_approved_for_all(owner, get_caller_address()), + 'Not token owner', + ); + self.token_approvals.write(token_id, to); + self.emit(Approval { owner: self.owner_of(token_id), to: to, token_id: token_id }); + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool, + ) { + let owner = get_caller_address(); + assert(owner != operator, 'ERC721: approve to caller'); + self.operator_approvals.write((owner, operator), approved); + self.emit(ApprovalForAll { owner: owner, operator: operator, approved: approved }); + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), + 'neither owner nor approved', + ); + self._transfer(from, to, token_id); + } + + fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + self._mint(to, token_id); + } + } + + #[generate_trait] + impl ERC721HelperImpl of ERC721HelperTrait { + fn _exists(self: @ContractState, token_id: u256) -> bool { + // check that owner of token is not zero + self.owner_of(token_id).is_non_zero() + } + + fn _is_approved_or_owner( + self: @ContractState, spender: ContractAddress, token_id: u256, + ) -> bool { + let owner = self.owners.read(token_id); + spender == owner + || self.is_approved_for_all(owner, spender) + || self.get_approved(token_id) == spender + } + + fn _set_token_uri(ref self: ContractState, token_id: u256, token_uri: felt252) { + assert(self._exists(token_id), 'ERC721: invalid token ID'); + self.token_uri.write(token_id, token_uri); + } + + fn _transfer( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ) { + // check that from address is equal to owner of token + assert(from == self.owner_of(token_id), 'ERC721: Caller is not owner'); + // check that to address is not zero + assert(to.is_non_zero(), 'ERC721: transfer to 0 address'); + + // remove previously made approvals + self.token_approvals.write(token_id, Zero::zero()); + + // increase balance of to address, decrease balance of from address + self.balances.write(from, self.balances.read(from) - 1.into()); + self.balances.write(to, self.balances.read(to) + 1.into()); + + // update token_id owner + self.owners.write(token_id, to); + + // emit the Transfer event + self.emit(Transfer { from: from, to: to, token_id: token_id }); + } + + fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + assert(to.is_non_zero(), 'TO_IS_ZERO_ADDRESS'); + + // Ensures token_id is unique + assert(!self.owner_of(token_id).is_non_zero(), 'ERC721: Token already minted'); + + // Increase receiver balance + let receiver_balance = self.balances.read(to); + self.balances.write(to, receiver_balance + 1.into()); + + // Update token_id owner + self.owners.write(token_id, to); + + // emit Transfer event + self.emit(Transfer { from: Zero::zero(), to: to, token_id: token_id }); + } + + fn _burn(ref self: ContractState, token_id: u256) { + let owner = self.owner_of(token_id); + + // Clear approvals + self.token_approvals.write(token_id, Zero::zero()); + + // Decrease owner balance + let owner_balance = self.balances.read(owner); + self.balances.write(owner, owner_balance - 1.into()); + + // Delete owner + self.owners.write(token_id, Zero::zero()); + // emit the Transfer event + self.emit(Transfer { from: owner, to: Zero::zero(), token_id: token_id }); + } + } +} + diff --git a/contracts/tests/lib.cairo b/contracts/tests/lib.cairo index b6496daa..03143005 100644 --- a/contracts/tests/lib.cairo +++ b/contracts/tests/lib.cairo @@ -4,5 +4,6 @@ mod test_defi_contract; mod test_erc20; mod test_starkfinder; mod test_starkidentity; +mod test_nft_dutch; #[feature("safe_dispatcher")] mod test_timelock; diff --git a/contracts/tests/test_nft_dutch.cairo b/contracts/tests/test_nft_dutch.cairo new file mode 100644 index 00000000..e55decc5 --- /dev/null +++ b/contracts/tests/test_nft_dutch.cairo @@ -0,0 +1,316 @@ +use starknet::ContractAddress; +use snforge_std::{ + declare, ContractClassTrait, DeclareResultTrait, cheat_block_timestamp, CheatSpan, + cheat_caller_address, +}; + +use core::traits::TryInto; +use core::option::OptionTrait; + +use contracts::interfaces::INFTDutchAuction::{ + INFTDutchAuctionDispatcher, INFTDutchAuctionDispatcherTrait, +}; +use contracts::mock_erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; +use contracts::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait}; +fn deploy_contract(name: ByteArray) -> ContractAddress { + let contract = declare(name).unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap(); + contract_address +} + +fn deploy_erc20() -> ContractAddress { + let name: ByteArray = "MockToken"; + let contract = declare("MockToken").unwrap().contract_class(); + + let mut constructor_calldata = ArrayTrait::new(); + name.serialize(ref constructor_calldata); + + let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap(); + contract_address +} + +fn deploy_erc721() -> ContractAddress { + const name: felt252 = 'MockERC721'; + const symbol: felt252 = 'MKT'; + let erc721 = declare("MockERC721").unwrap().contract_class(); + let erc721_constructor_calldata = array![name, symbol]; + let (contract_address, _) = erc721.deploy(@erc721_constructor_calldata).unwrap(); + contract_address +} + +fn deploy_dutch_auction( + erc20_token: ContractAddress, + erc721_token: ContractAddress, + starting_price: u64, + seller: ContractAddress, + duration: u64, + discount_rate: u64, + total_supply: u128, +) -> ContractAddress { + let contract = declare("NFTDutchAuction").unwrap().contract_class(); + let mut calldata = array![]; + calldata.append(erc20_token.into()); + calldata.append(erc721_token.into()); + calldata.append(starting_price.into()); + calldata.append(seller.into()); + calldata.append(duration.into()); + calldata.append(discount_rate.into()); + calldata.append(total_supply.into()); + + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address +} + +#[test] +fn test_dutch_auction_constructor() { + let owner = starknet::contract_address_const::<0x123>(); + let erc20_token = deploy_erc20(); + let erc721_token = deploy_erc721(); + + let auction = deploy_dutch_auction( + erc20_token, + erc721_token, + 1000, // starting price + owner, + 100, // duration + 10, // discount rate + 5 // total supply + ); + + let dutch_auction_dispatcher = INFTDutchAuctionDispatcher { contract_address: auction }; + + // Check initial price is the starting price + assert(dutch_auction_dispatcher.get_price() == 1000, 'Incorrect initial price'); +} + +#[test] +fn test_price_decreases_after_some_time() { + let owner = starknet::contract_address_const::<0x123>(); + let erc20_token = deploy_erc20(); + let erc721_token = deploy_erc721(); + + let nft_auction_address = deploy_dutch_auction( + erc20_token, + erc721_token, + 1000, // starting price + owner, + 100, // duration + 10, // discount rate + 5 // total supply + ); + + let nft_auction_dispatcher = INFTDutchAuctionDispatcher { + contract_address: nft_auction_address, + }; + + let nft_price_before_time_travel = nft_auction_dispatcher.get_price(); + + // Forward time + let forward_blocktime_by = 10000; // 10 seconds (in milliseconds) + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); + + let nft_price_after_time_travel = nft_auction_dispatcher.get_price(); + + assert_gt!(nft_price_before_time_travel, nft_price_after_time_travel); +} + + +#[test] +fn test_buy_asset() { + let seller = starknet::contract_address_const::<0x123>(); + let erc20_address = deploy_erc20(); + let erc721_address = deploy_erc721(); + + let nft_auction_address = deploy_dutch_auction( + erc20_address, + erc721_address, + 500, // starting price + seller, + 60, // duration + 5, // discount rate + 2 // total supply + ); + + let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let nft_auction_dispatcher = INFTDutchAuctionDispatcher { + contract_address: nft_auction_address, + }; + let erc20_admin: ContractAddress = 'admin'.try_into().unwrap(); + let buyer: ContractAddress = 'buyer'.try_into().unwrap(); + + // Transfer erc20 tokens to buyer + assert_eq!(erc20_dispatcher.balance_of(buyer), 0.into()); + erc20_dispatcher.mint(erc20_admin, 10000); + cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1)); + let transfer_amt = 5000; + erc20_dispatcher.transfer(buyer, transfer_amt.into()); + assert_eq!(erc20_dispatcher.balance_of(buyer), transfer_amt.into()); + + let nft_id_1 = 1; + let seller_bal_before_buy = erc20_dispatcher.balance_of(seller); + let buyer_bal_before_buy = erc20_dispatcher.balance_of(buyer); + let nft_price = nft_auction_dispatcher.get_price().into(); + // Buy token + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(3)); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(2)); + // buyer approves nft auction contract to spend own erc20 token + erc20_dispatcher.approve(nft_auction_address, nft_price); + + nft_auction_dispatcher.buy(nft_id_1); + + let seller_bal_after_buy = erc20_dispatcher.balance_of(seller); + let buyer_bal_after_buy = erc20_dispatcher.balance_of(buyer); + + assert_eq!(seller_bal_after_buy, seller_bal_before_buy + nft_price); + assert_eq!(buyer_bal_after_buy, buyer_bal_before_buy - nft_price); + assert_eq!(erc721_dispatcher.owner_of(nft_id_1), buyer); + + // Forward block timestamp in order for a reduced nft price + let forward_blocktime_by = 4000; // milliseconds + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); + + // Buy token again after some time + let nft_id_2 = 2; + + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(1)); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(2)); + // buyer approves nft auction contract to spend own erc20 token + erc20_dispatcher.approve(nft_auction_address, nft_price); + + assert_ne!(erc721_dispatcher.owner_of(nft_id_2), buyer); + nft_auction_dispatcher.buy(nft_id_2); + assert_eq!(erc721_dispatcher.owner_of(nft_id_2), buyer); +} + + +#[test] +#[should_panic(expected: 'auction has ended')] +fn test_buy_should_panic_when_total_supply_reached() { + let owner = starknet::contract_address_const::<0x123>(); + let erc20_address = deploy_erc20(); + let erc721_address = deploy_erc721(); + + let nft_auction_address = deploy_dutch_auction( + erc20_address, + erc721_address, + 500, // starting price + owner, + 60, // duration + 5, // discount rate + 2 // total supply + ); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let nft_auction_dispatcher = INFTDutchAuctionDispatcher { + contract_address: nft_auction_address, + }; + let erc20_admin: ContractAddress = 'admin'.try_into().unwrap(); + let buyer: ContractAddress = 'buyer'.try_into().unwrap(); + + erc20_dispatcher.mint(erc20_admin, 10000); + + // Transfer erc20 tokens to buyer + let transfer_amt = 5000; + cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1)); + erc20_dispatcher.transfer(buyer, transfer_amt.into()); + + let nft_id_1 = 1; + let nft_price = nft_auction_dispatcher.get_price().into(); + + // buyer approves nft auction contract to spend own erc20 token + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + erc20_dispatcher.approve(nft_auction_address, nft_price); + + // Buy token + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(1)); + nft_auction_dispatcher.buy(nft_id_1); + + // Forward block timestamp in order for a reduced nft price + let forward_blocktime_by = 4000; // 4 seconds (in milliseconds) + + // Buy token again after some time + let nft_id_2 = 2; + + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); + let nft_price = nft_auction_dispatcher.get_price().into(); + + // buyer approves nft auction contract to spend own erc20 token + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + erc20_dispatcher.approve(nft_auction_address, nft_price); + + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(1)); + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); + nft_auction_dispatcher.buy(nft_id_2); + + // Buy token again after the total supply has reached + let nft_id_3 = 3; + let nft_price = nft_auction_dispatcher.get_price().into(); + + // buyer approves nft auction contract to spend own erc20 token + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + erc20_dispatcher.approve(nft_auction_address, nft_price); + + // Buy token + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(4)); + nft_auction_dispatcher.buy(nft_id_3); +} + +#[test] +#[should_panic(expected: 'auction has ended')] +fn test_buy_should_panic_when_duration_ended() { + let owner = starknet::contract_address_const::<0x123>(); + let erc20_address = deploy_erc20(); + let erc721_address = deploy_erc721(); + + let nft_auction_address = deploy_dutch_auction( + erc20_address, + erc721_address, + 500, // starting price + owner, + 60, // duration + 5, // discount rate + 2 // total supply + ); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let nft_auction_dispatcher = INFTDutchAuctionDispatcher { + contract_address: nft_auction_address, + }; + + let erc20_admin: ContractAddress = 'admin'.try_into().unwrap(); + let buyer: ContractAddress = 'buyer'.try_into().unwrap(); + + erc20_dispatcher.mint(erc20_admin, 10000); + + // Transfer erc20 tokens to buyer + let transfer_amt = 5000; + + cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1)); + erc20_dispatcher.transfer(buyer, transfer_amt.into()); + + let nft_id_1 = 1; + let nft_price = nft_auction_dispatcher.get_price().into(); + + // buyer approves nft auction contract to spend own erc20 token + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + erc20_dispatcher.approve(nft_auction_address, nft_price); + + // Buy token + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(1)); + nft_auction_dispatcher.buy(nft_id_1); + + // Forward block timestamp to a time after duration has ended + // During deployment, duration was set to 60 seconds + let forward_blocktime_by = 61000; // 61 seconds (in milliseconds) + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); + let nft_price = nft_auction_dispatcher.get_price().into(); + // Buy token again after some time + let nft_id_2 = 2; + + // buyer approves nft auction contract to spend own erc20 token + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(1)); + erc20_dispatcher.approve(nft_auction_address, nft_price); + + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(1)); + nft_auction_dispatcher.buy(nft_id_2); +}