From 39c82bf9d775a214cc854e4583c311911e7618bb Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Fri, 31 May 2024 11:08:17 +0100 Subject: [PATCH 01/10] feat: nft auction --- Scarb.lock | 4 + listings/applications/nft_auction/.gitignore | 1 + listings/applications/nft_auction/Scarb.toml | 12 ++ .../applications/nft_auction/src/lib.cairo | 4 + .../nft_auction/src/nft_auction.cairo | 106 ++++++++++++++++++ .../applications/nft_auction/src/tests.cairo | 2 + 6 files changed, 129 insertions(+) create mode 100644 listings/applications/nft_auction/.gitignore create mode 100644 listings/applications/nft_auction/Scarb.toml create mode 100644 listings/applications/nft_auction/src/lib.cairo create mode 100644 listings/applications/nft_auction/src/nft_auction.cairo create mode 100644 listings/applications/nft_auction/src/tests.cairo diff --git a/Scarb.lock b/Scarb.lock index 31034f8c..9bc4f960 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -88,6 +88,10 @@ version = "0.1.0" name = "mappings" version = "0.1.0" +[[package]] +name = "nft_auction" +version = "0.1.0" + [[package]] name = "openzeppelin" version = "0.11.0" diff --git a/listings/applications/nft_auction/.gitignore b/listings/applications/nft_auction/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/listings/applications/nft_auction/.gitignore @@ -0,0 +1 @@ +target diff --git a/listings/applications/nft_auction/Scarb.toml b/listings/applications/nft_auction/Scarb.toml new file mode 100644 index 00000000..2b36d9f6 --- /dev/null +++ b/listings/applications/nft_auction/Scarb.toml @@ -0,0 +1,12 @@ +[package] +name = "nft_auction" +version.workspace = true +edition = '2023_11' + +[dependencies] +starknet.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] diff --git a/listings/applications/nft_auction/src/lib.cairo b/listings/applications/nft_auction/src/lib.cairo new file mode 100644 index 00000000..ea0345d0 --- /dev/null +++ b/listings/applications/nft_auction/src/lib.cairo @@ -0,0 +1,4 @@ +mod nft_auction; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/nft_auction/src/nft_auction.cairo b/listings/applications/nft_auction/src/nft_auction.cairo new file mode 100644 index 00000000..ddce34ce --- /dev/null +++ b/listings/applications/nft_auction/src/nft_auction.cairo @@ -0,0 +1,106 @@ +use starknet::ContractAddress; + +// In order to make contract calls within our Vault, +// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher. +#[starknet::interface] +pub trait IERC20 { + fn name(self: @TContractState) -> felt252; + fn symbol(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::interface] +pub trait IERC721 { + fn total_supply(self: @TContractState) -> u128; + fn safe_mint(ref self: TContractState, recipient: ContractAddress, tokenId: u256); +} + +#[starknet::interface] +pub trait INFTAuction { + fn buy(ref self: TContractState); +} + +#[starknet::contract] +pub mod NFTAuction { + use super::{IERC20Dispatcher, IERC20DispatcherTrait, IERC721Dispatcher, IERC721DispatcherTrait}; + use starknet::{ContractAddress, get_caller_address, get_contract_address, get_block_timestamp}; + + #[storage] + struct Storage { + erc20_token: IERC20Dispatcher, + erc721_token: IERC721Dispatcher, + starting_price: u64, + seller: ContractAddress, + duration: u64, + discount_rate: u64, + start_at: u64, + expires_at: u64, + purchase_count: u128 + } + + #[constructor] + fn constructor( + ref self: ContractState, + erc20_token: ContractAddress, + erc721_token: ContractAddress, + starting_price: u64, + seller: ContractAddress, + duration: u64, + discount_rate: u64, + ) { + assert(starting_price >= discount_rate * duration, 'starting price too low'); + + self.erc20_token.write(IERC20Dispatcher { contract_address: erc20_token }); + self.erc721_token.write(IERC721Dispatcher { contract_address: 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); + } + + #[generate_trait] + impl PrivateFunctions of PrivateFunctionsTrait { + fn get_price(self: @ContractState) -> u64 { + let time_elapsed = get_block_timestamp() - self.start_at.read(); + let discount = self.discount_rate.read() * time_elapsed; + self.starting_price.read() - discount + } + } + + #[abi(embed_v0)] + impl NFTAuction of super::INFTAuction { + fn buy(ref self: ContractState) { + // Check duration + assert(get_block_timestamp() < self.expires_at.read(), 'auction ended'); + // Check total supply + assert( + self.purchase_count.read() <= self.erc721_token.read().total_supply(), + 'auction ended' + ); + + let caller = get_caller_address(); + + // Get NFT price + let price: u256 = self.get_price().into(); + // Check payment token balance + assert(self.erc20_token.read().balance_of(caller) >= price, 'insufficient balance'); + // Transfer payment token to contract + self.erc20_token.read().transfer(self.seller.read(), price); + // Mint token to buyer's address + self.erc721_token.read().safe_mint(caller, 1); + + // Increase purchase count + self.purchase_count.write(self.purchase_count.read() + 1); + } + } +} diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo new file mode 100644 index 00000000..361dba07 --- /dev/null +++ b/listings/applications/nft_auction/src/tests.cairo @@ -0,0 +1,2 @@ +mod tests { // TODO +} From 645e179713a4a4155e1ff5ac773f3be5626e5f8b Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Mon, 3 Jun 2024 08:46:02 +0100 Subject: [PATCH 02/10] test: add tests to nft_auction app --- Scarb.lock | 3 + Scarb.toml | 4 +- listings/applications/nft_auction/Scarb.toml | 3 + .../applications/nft_auction/src/erc20.cairo | 211 +++++++++++++ .../applications/nft_auction/src/erc721.cairo | 287 ++++++++++++++++++ .../applications/nft_auction/src/lib.cairo | 2 + .../applications/nft_auction/src/tests.cairo | 58 +++- 7 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 listings/applications/nft_auction/src/erc20.cairo create mode 100644 listings/applications/nft_auction/src/erc721.cairo diff --git a/Scarb.lock b/Scarb.lock index 9bc4f960..3e334983 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -91,6 +91,9 @@ version = "0.1.0" [[package]] name = "nft_auction" version = "0.1.0" +dependencies = [ + "snforge_std", +] [[package]] name = "openzeppelin" diff --git a/Scarb.toml b/Scarb.toml index 4a232645..a19c3793 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -3,13 +3,13 @@ members = [ "listings/getting-started/*", "listings/applications/*", "listings/advanced-concepts/*", - "listings/templates/*" + "listings/templates/*", ] [workspace.scripts] test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh" -# [workspace.tool.snforge] +[workspace.tool.snforge] [workspace.dependencies] starknet = ">=2.6.3" diff --git a/listings/applications/nft_auction/Scarb.toml b/listings/applications/nft_auction/Scarb.toml index 2b36d9f6..d4f20db7 100644 --- a/listings/applications/nft_auction/Scarb.toml +++ b/listings/applications/nft_auction/Scarb.toml @@ -6,6 +6,9 @@ edition = '2023_11' [dependencies] starknet.workspace = true +[dev-dependencies] +snforge_std.workspace = true + [scripts] test.workspace = true diff --git a/listings/applications/nft_auction/src/erc20.cairo b/listings/applications/nft_auction/src/erc20.cairo new file mode 100644 index 00000000..23acef39 --- /dev/null +++ b/listings/applications/nft_auction/src/erc20.cairo @@ -0,0 +1,211 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC20 { + fn get_name(self: @TContractState) -> felt252; + fn get_symbol(self: @TContractState) -> felt252; + fn get_decimals(self: @TContractState) -> u8; + fn get_total_supply(self: @TContractState) -> felt252; + fn balance_of(self: @TContractState, account: ContractAddress) -> felt252; + fn allowance( + self: @TContractState, owner: ContractAddress, spender: ContractAddress + ) -> felt252; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252); + fn transfer_from( + ref self: TContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: felt252 + ); + fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252); + fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252); + fn decrease_allowance( + ref self: TContractState, spender: ContractAddress, subtracted_value: felt252 + ); +} + +#[starknet::contract] +mod ERC20 { + use core::num::traits::Zero; + use starknet::get_caller_address; + use starknet::contract_address_const; + use starknet::ContractAddress; + + #[storage] + struct Storage { + name: felt252, + symbol: felt252, + decimals: u8, + total_supply: felt252, + balances: LegacyMap::, + allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + } + #[derive(Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + value: felt252, + } + #[derive(Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + spender: ContractAddress, + value: felt252, + } + + mod Errors { + pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; + pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; + pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; + pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; + pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + recipient: ContractAddress, + name: felt252, + decimals: u8, + initial_supply: felt252, + symbol: felt252 + ) { + self.name.write(name); + self.symbol.write(symbol); + self.decimals.write(decimals); + self.mint(recipient, initial_supply); + } + + #[abi(embed_v0)] + impl IERC20Impl of super::IERC20 { + fn get_name(self: @ContractState) -> felt252 { + self.name.read() + } + + fn get_symbol(self: @ContractState) -> felt252 { + self.symbol.read() + } + + fn get_decimals(self: @ContractState) -> u8 { + self.decimals.read() + } + + fn get_total_supply(self: @ContractState) -> felt252 { + self.total_supply.read() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 { + self.balances.read(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> felt252 { + self.allowances.read((owner, spender)) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: felt252 + ) { + let caller = get_caller_address(); + self.spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) { + let caller = get_caller_address(); + self.approve_helper(caller, spender, amount); + } + + fn increase_allowance( + ref self: ContractState, spender: ContractAddress, added_value: felt252 + ) { + let caller = get_caller_address(); + self + .approve_helper( + caller, spender, self.allowances.read((caller, spender)) + added_value + ); + } + + fn decrease_allowance( + ref self: ContractState, spender: ContractAddress, subtracted_value: felt252 + ) { + let caller = get_caller_address(); + self + .approve_helper( + caller, spender, self.allowances.read((caller, spender)) - subtracted_value + ); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _transfer( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: felt252 + ) { + assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); + assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO); + self.balances.write(sender, self.balances.read(sender) - amount); + self.balances.write(recipient, self.balances.read(recipient) + amount); + self.emit(Transfer { from: sender, to: recipient, value: amount }); + } + + fn spend_allowance( + ref self: ContractState, + owner: ContractAddress, + spender: ContractAddress, + amount: felt252 + ) { + let allowance = self.allowances.read((owner, spender)); + self.allowances.write((owner, spender), allowance - amount); + } + + fn approve_helper( + ref self: ContractState, + owner: ContractAddress, + spender: ContractAddress, + amount: felt252 + ) { + assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO); + self.allowances.write((owner, spender), amount); + self.emit(Approval { owner, spender, value: amount }); + } + + fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) { + assert(!recipient.is_zero(), Errors::MINT_TO_ZERO); + let supply = self.total_supply.read() + amount; + self.total_supply.write(supply); + let balance = self.balances.read(recipient) + amount; + self.balances.write(recipient, balance); + self + .emit( + Event::Transfer( + Transfer { + from: contract_address_const::<0>(), to: recipient, value: amount + } + ) + ); + } + } +} + + diff --git a/listings/applications/nft_auction/src/erc721.cairo b/listings/applications/nft_auction/src/erc721.cairo new file mode 100644 index 00000000..08d14fb0 --- /dev/null +++ b/listings/applications/nft_auction/src/erc721.cairo @@ -0,0 +1,287 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC721Trait { + fn get_name(self: @T) -> felt252; + fn get_symbol(self: @T) -> felt252; + fn get_token_uri(self: @T, token_id: u256) -> felt252; + fn balance_of(self: @T, account: ContractAddress) -> u256; + fn owner_of(self: @T, token_id: u256) -> ContractAddress; + fn get_approved(self: @T, token_id: u256) -> ContractAddress; + fn is_approved_for_all(self: @T, owner: ContractAddress, operator: ContractAddress) -> bool; + fn approve(ref self: T, to: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: T, operator: ContractAddress, approved: bool); + fn transfer_from(ref self: T, from: ContractAddress, to: ContractAddress, token_id: u256); + fn mint(ref self: T, token_id: u256); +} + +#[starknet::contract] +mod ERC721 { + //////////////////////////////// + // library imports + //////////////////////////////// + use starknet::{ContractAddress, get_caller_address}; + use core::traits::TryInto; + use core::num::traits::zero::Zero; + + //////////////////////////////// + // storage variables + //////////////////////////////// + #[storage] + struct Storage { + name: felt252, + symbol: felt252, + owners: LegacyMap::, + balances: LegacyMap::, + token_approvals: LegacyMap::, + operator_approvals: LegacyMap::<(ContractAddress, ContractAddress), bool>, + token_uri: LegacyMap, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Approval: Approval, + Transfer: Transfer, + ApprovalForAll: ApprovalForAll + } + + //////////////////////////////// + // Approval event emitted on token approval + //////////////////////////////// + #[derive(Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + to: ContractAddress, + token_id: u256 + } + + //////////////////////////////// + // Transfer event emitted on token transfer + //////////////////////////////// + #[derive(Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + token_id: u256 + } + + //////////////////////////////// + // ApprovalForAll event emitted on approval for operators + //////////////////////////////// + #[derive(Drop, starknet::Event)] + struct ApprovalForAll { + owner: ContractAddress, + operator: ContractAddress, + approved: bool + } + + + //////////////////////////////// + // Constructor - initialized on deployment + //////////////////////////////// + #[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::IERC721Trait { + //////////////////////////////// + // get_name function returns token name + //////////////////////////////// + fn get_name(self: @ContractState) -> felt252 { + self.name.read() + } + + //////////////////////////////// + // get_symbol function returns token symbol + //////////////////////////////// + fn get_symbol(self: @ContractState) -> felt252 { + self.symbol.read() + } + + //////////////////////////////// + // token_uri returns the token uri + //////////////////////////////// + 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) + } + + //////////////////////////////// + // balance_of function returns token balance + //////////////////////////////// + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + assert(account.is_non_zero(), 'ERC721: address zero'); + self.balances.read(account) + } + + //////////////////////////////// + // owner_of function returns owner of token_id + //////////////////////////////// + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + let owner = self.owners.read(token_id); + assert(owner.is_non_zero(), 'ERC721: invalid token ID'); + owner + } + + //////////////////////////////// + // get_approved function returns approved address for a token + //////////////////////////////// + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + assert(self._exists(token_id), 'ERC721: invalid token ID'); + self.token_approvals.read(token_id) + } + + //////////////////////////////// + // is_approved_for_all function returns approved operator for a token + //////////////////////////////// + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.operator_approvals.read((owner, operator)) + } + + //////////////////////////////// + // approve function approves an address to spend a token + //////////////////////////////// + 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 }); + } + + //////////////////////////////// + // set_approval_for_all function approves an operator to spend all tokens + //////////////////////////////// + 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 }); + } + + //////////////////////////////// + // transfer_from function is used to transfer a token + //////////////////////////////// + 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, token_id: u256) { + let caller = get_caller_address(); + self._mint(caller, token_id); + } + } + + #[generate_trait] + impl ERC721HelperImpl of ERC721HelperTrait { + //////////////////////////////// + // internal function to check if a token exists + //////////////////////////////// + fn _exists(self: @ContractState, token_id: u256) -> bool { + // check that owner of token is not zero + self.owner_of(token_id).is_non_zero() + } + + //////////////////////////////// + // _is_approved_or_owner checks if an address is an approved spender or owner + //////////////////////////////// + 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 + } + + //////////////////////////////// + // internal function that sets the token uri + //////////////////////////////// + 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) + } + + //////////////////////////////// + // internal function that performs the transfer logic + //////////////////////////////// + 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 }); + } + + //////////////////////////////// + // _mint function mints a new token to the to address + //////////////////////////////// + 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 }); + } + + //////////////////////////////// + // _burn function burns token from owner's account + //////////////////////////////// + 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/listings/applications/nft_auction/src/lib.cairo b/listings/applications/nft_auction/src/lib.cairo index ea0345d0..cb0b7516 100644 --- a/listings/applications/nft_auction/src/lib.cairo +++ b/listings/applications/nft_auction/src/lib.cairo @@ -1,4 +1,6 @@ mod nft_auction; +mod erc20; +mod erc721; #[cfg(test)] mod tests; diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo index 361dba07..cda0ddc0 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_auction/src/tests.cairo @@ -1,2 +1,58 @@ -mod tests { // TODO +use core::option::OptionTrait; +use core::traits::{Into, TryInto}; +use starknet::ContractAddress; +use snforge_std::{declare, ContractClassTrait}; + +// ERC721 token +pub const erc721_name: felt252 = 'My NFT'; +pub const erc721_symbol: felt252 = 'MNFT'; + + +// ERC20 token +pub const erc20_name: felt252 = 'My Token'; +pub const erc20_symbol: felt252 = 'MTKN'; +pub const erc20_recipient: felt252 = 'admin'; +pub const erc20_decimals: u8 = 8_u8; +pub const erc20_initial_supply: u128 = 100000000000_u128; + + +// NFT Auction +pub const starting_price: u64 = 10000_u64; +pub const seller: felt252 = 'seller'; +pub const duration: u64 = 60_u64; +pub const discount_rate: u64 = 5_u64; + +fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddress) { + let erc721 = declare("ERC721").unwrap(); + let erc721_constructor_calldata = array![erc721_name, erc721_symbol]; + let (erc721_address, _) = erc721.deploy(@erc721_constructor_calldata).unwrap(); + + let erc20 = declare("ERC20").unwrap(); + let erc20_constructor_calldata = array![ + erc20_recipient, + erc20_name, + erc20_decimals.into(), + erc20_initial_supply.into(), + erc20_symbol + ]; + let (erc20_address, _) = erc20.deploy(@erc20_constructor_calldata).unwrap(); + + let nft_auction = declare("NFTAuction").unwrap(); + let nft_auction_constructor_calldata = array![ + erc20_address.into(), + erc721_address.into(), + starting_price.into(), + seller, + duration.into(), + discount_rate.into() + ]; + let (nft_auction_address, _) = nft_auction.deploy(@nft_auction_constructor_calldata).unwrap(); + + (erc721_address, erc20_address, nft_auction_address) +} + +#[test] +fn test_deployment() { + let (erc721, erc20, nft_auction) = get_contract_addresses(); + println!("CAs: {:?}, {:?}, {:?}", erc721, erc20, nft_auction); } From 9147382e4fee5753daec8cb6df4ec2aab1ca73f7 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Mon, 3 Jun 2024 10:48:13 +0100 Subject: [PATCH 03/10] chore: improve code and add more tests --- .../applications/nft_auction/src/erc20.cairo | 2 +- .../applications/nft_auction/src/erc721.cairo | 35 +++++++++-------- .../applications/nft_auction/src/lib.cairo | 8 ++-- .../nft_auction/src/nft_auction.cairo | 36 ++++++++++++------ .../applications/nft_auction/src/tests.cairo | 38 ++++++++++++++++--- 5 files changed, 81 insertions(+), 38 deletions(-) diff --git a/listings/applications/nft_auction/src/erc20.cairo b/listings/applications/nft_auction/src/erc20.cairo index 23acef39..3ac920f3 100644 --- a/listings/applications/nft_auction/src/erc20.cairo +++ b/listings/applications/nft_auction/src/erc20.cairo @@ -1,7 +1,7 @@ use starknet::ContractAddress; #[starknet::interface] -trait IERC20 { +pub trait IERC20 { fn get_name(self: @TContractState) -> felt252; fn get_symbol(self: @TContractState) -> felt252; fn get_decimals(self: @TContractState) -> u8; diff --git a/listings/applications/nft_auction/src/erc721.cairo b/listings/applications/nft_auction/src/erc721.cairo index 08d14fb0..c862ae8a 100644 --- a/listings/applications/nft_auction/src/erc721.cairo +++ b/listings/applications/nft_auction/src/erc721.cairo @@ -1,18 +1,22 @@ use starknet::ContractAddress; #[starknet::interface] -trait IERC721Trait { - fn get_name(self: @T) -> felt252; - fn get_symbol(self: @T) -> felt252; - fn get_token_uri(self: @T, token_id: u256) -> felt252; - fn balance_of(self: @T, account: ContractAddress) -> u256; - fn owner_of(self: @T, token_id: u256) -> ContractAddress; - fn get_approved(self: @T, token_id: u256) -> ContractAddress; - fn is_approved_for_all(self: @T, owner: ContractAddress, operator: ContractAddress) -> bool; - fn approve(ref self: T, to: ContractAddress, token_id: u256); - fn set_approval_for_all(ref self: T, operator: ContractAddress, approved: bool); - fn transfer_from(ref self: T, from: ContractAddress, to: ContractAddress, token_id: u256); - fn mint(ref self: T, token_id: u256); +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] @@ -87,7 +91,7 @@ mod ERC721 { } #[abi(embed_v0)] - impl IERC721Impl of super::IERC721Trait { + impl IERC721Impl of super::IERC721 { //////////////////////////////// // get_name function returns token name //////////////////////////////// @@ -184,9 +188,8 @@ mod ERC721 { self._transfer(from, to, token_id); } - fn mint(ref self: ContractState, token_id: u256) { - let caller = get_caller_address(); - self._mint(caller, token_id); + fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + self._mint(to, token_id); } } diff --git a/listings/applications/nft_auction/src/lib.cairo b/listings/applications/nft_auction/src/lib.cairo index cb0b7516..9df7ab72 100644 --- a/listings/applications/nft_auction/src/lib.cairo +++ b/listings/applications/nft_auction/src/lib.cairo @@ -1,6 +1,6 @@ -mod nft_auction; -mod erc20; -mod erc721; +pub mod nft_auction; +pub mod erc20; +pub mod erc721; #[cfg(test)] -mod tests; +pub mod tests; diff --git a/listings/applications/nft_auction/src/nft_auction.cairo b/listings/applications/nft_auction/src/nft_auction.cairo index ddce34ce..b259b1e9 100644 --- a/listings/applications/nft_auction/src/nft_auction.cairo +++ b/listings/applications/nft_auction/src/nft_auction.cairo @@ -17,15 +17,29 @@ pub trait IERC20 { fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; } + #[starknet::interface] -pub trait IERC721 { - fn total_supply(self: @TContractState) -> u128; - fn safe_mint(ref self: TContractState, recipient: ContractAddress, tokenId: u256); +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::interface] pub trait INFTAuction { - fn buy(ref self: TContractState); + fn buy(ref self: TContractState, token_id: u256); } #[starknet::contract] @@ -43,7 +57,8 @@ pub mod NFTAuction { discount_rate: u64, start_at: u64, expires_at: u64, - purchase_count: u128 + purchase_count: u128, + total_supply: u128 } #[constructor] @@ -55,6 +70,7 @@ pub mod NFTAuction { seller: ContractAddress, duration: u64, discount_rate: u64, + total_supply: u128 ) { assert(starting_price >= discount_rate * duration, 'starting price too low'); @@ -66,6 +82,7 @@ pub mod NFTAuction { self.discount_rate.write(discount_rate); self.start_at.write(get_block_timestamp()); self.expires_at.write(get_block_timestamp() + duration); + self.total_supply.write(total_supply); } #[generate_trait] @@ -79,14 +96,11 @@ pub mod NFTAuction { #[abi(embed_v0)] impl NFTAuction of super::INFTAuction { - fn buy(ref self: ContractState) { + fn buy(ref self: ContractState, token_id: u256) { // Check duration assert(get_block_timestamp() < self.expires_at.read(), 'auction ended'); // Check total supply - assert( - self.purchase_count.read() <= self.erc721_token.read().total_supply(), - 'auction ended' - ); + assert(self.purchase_count.read() < self.total_supply.read(), 'auction ended'); let caller = get_caller_address(); @@ -97,7 +111,7 @@ pub mod NFTAuction { // Transfer payment token to contract self.erc20_token.read().transfer(self.seller.read(), price); // Mint token to buyer's address - self.erc721_token.read().safe_mint(caller, 1); + self.erc721_token.read().mint(caller, 1); // Increase purchase count self.purchase_count.write(self.purchase_count.read() + 1); diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo index cda0ddc0..88730d0a 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_auction/src/tests.cairo @@ -1,7 +1,14 @@ use core::option::OptionTrait; use core::traits::{Into, TryInto}; use starknet::ContractAddress; -use snforge_std::{declare, ContractClassTrait}; +use snforge_std::{ + BlockId, declare, ContractClassTrait, ContractClass, prank, CheatSpan, CheatTarget, roll +}; +use super::{ + erc20::{IERC20Dispatcher, IERC20DispatcherTrait}, + erc721::{IERC721Dispatcher, IERC721DispatcherTrait}, + nft_auction::{INFTAuctionDispatcher, INFTAuctionDispatcherTrait} +}; // ERC721 token pub const erc721_name: felt252 = 'My NFT'; @@ -12,7 +19,7 @@ pub const erc721_symbol: felt252 = 'MNFT'; pub const erc20_name: felt252 = 'My Token'; pub const erc20_symbol: felt252 = 'MTKN'; pub const erc20_recipient: felt252 = 'admin'; -pub const erc20_decimals: u8 = 8_u8; +pub const erc20_decimals: u8 = 1_u8; pub const erc20_initial_supply: u128 = 100000000000_u128; @@ -21,6 +28,7 @@ pub const starting_price: u64 = 10000_u64; pub const seller: felt252 = 'seller'; pub const duration: u64 = 60_u64; pub const discount_rate: u64 = 5_u64; +pub const total_supply: u128 = 2_u128; fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddress) { let erc721 = declare("ERC721").unwrap(); @@ -44,7 +52,8 @@ fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddres starting_price.into(), seller, duration.into(), - discount_rate.into() + discount_rate.into(), + total_supply.into() ]; let (nft_auction_address, _) = nft_auction.deploy(@nft_auction_constructor_calldata).unwrap(); @@ -52,7 +61,24 @@ fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddres } #[test] -fn test_deployment() { - let (erc721, erc20, nft_auction) = get_contract_addresses(); - println!("CAs: {:?}, {:?}, {:?}", erc721, erc20, nft_auction); +fn test_buy() { + let (erc721_address, erc20_address, nft_auction_address) = get_contract_addresses(); + + let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let nft_auction_dispatcher = INFTAuctionDispatcher { contract_address: nft_auction_address }; + + let erc20_admin: ContractAddress = 'admin'.try_into().unwrap(); + // let seller: ContractAddress = 'seller'.try_into().unwrap(); + let buyer: ContractAddress = 'buyer'.try_into().unwrap(); + + // Transfer erc20 tokens to buyer + prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); + erc20_dispatcher.transfer(buyer, 10.into()); + + // Buy token + prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(1)); + nft_auction_dispatcher.buy(1); + + assert_eq!(erc721_dispatcher.owner_of(1), buyer); } From 935762c90b80b914dc195adda64b04426834d47e Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Mon, 3 Jun 2024 21:18:49 +0100 Subject: [PATCH 04/10] chore: improvements and more tests --- .../applications/nft_auction/src/erc20.cairo | 1 - .../applications/nft_auction/src/erc721.cairo | 1 - .../nft_auction/src/nft_auction.cairo | 69 +++++++++++-------- .../applications/nft_auction/src/tests.cairo | 65 +++++++++++------ 4 files changed, 85 insertions(+), 51 deletions(-) diff --git a/listings/applications/nft_auction/src/erc20.cairo b/listings/applications/nft_auction/src/erc20.cairo index 3ac920f3..18f5d923 100644 --- a/listings/applications/nft_auction/src/erc20.cairo +++ b/listings/applications/nft_auction/src/erc20.cairo @@ -208,4 +208,3 @@ mod ERC20 { } } - diff --git a/listings/applications/nft_auction/src/erc721.cairo b/listings/applications/nft_auction/src/erc721.cairo index c862ae8a..9e1733ca 100644 --- a/listings/applications/nft_auction/src/erc721.cairo +++ b/listings/applications/nft_auction/src/erc721.cairo @@ -127,7 +127,6 @@ mod ERC721 { //////////////////////////////// fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { let owner = self.owners.read(token_id); - assert(owner.is_non_zero(), 'ERC721: invalid token ID'); owner } diff --git a/listings/applications/nft_auction/src/nft_auction.cairo b/listings/applications/nft_auction/src/nft_auction.cairo index b259b1e9..331c12f9 100644 --- a/listings/applications/nft_auction/src/nft_auction.cairo +++ b/listings/applications/nft_auction/src/nft_auction.cairo @@ -1,23 +1,29 @@ use starknet::ContractAddress; -// In order to make contract calls within our Vault, -// we need to have the interface of the remote ERC20 contract defined to import the Dispatcher. #[starknet::interface] pub trait IERC20 { - fn name(self: @TContractState) -> felt252; - fn symbol(self: @TContractState) -> felt252; - fn decimals(self: @TContractState) -> u8; - fn total_supply(self: @TContractState) -> u256; - fn balance_of(self: @TContractState, account: ContractAddress) -> u256; - fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; + fn get_name(self: @TContractState) -> felt252; + fn get_symbol(self: @TContractState) -> felt252; + fn get_decimals(self: @TContractState) -> u8; + fn get_total_supply(self: @TContractState) -> felt252; + fn balance_of(self: @TContractState, account: ContractAddress) -> felt252; + fn allowance( + self: @TContractState, owner: ContractAddress, spender: ContractAddress + ) -> felt252; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252); fn transfer_from( - ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + ref self: TContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: felt252 + ); + fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252); + fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252); + fn decrease_allowance( + ref self: TContractState, spender: ContractAddress, subtracted_value: felt252 + ); } - #[starknet::interface] trait IERC721 { fn get_name(self: @TContractState) -> felt252; @@ -40,6 +46,7 @@ trait IERC721 { #[starknet::interface] pub trait INFTAuction { fn buy(ref self: TContractState, token_id: u256); + fn get_price(self: @TContractState) -> u64; } #[starknet::contract] @@ -49,8 +56,8 @@ pub mod NFTAuction { #[storage] struct Storage { - erc20_token: IERC20Dispatcher, - erc721_token: IERC721Dispatcher, + erc20_token: ContractAddress, + erc721_token: ContractAddress, starting_price: u64, seller: ContractAddress, duration: u64, @@ -74,44 +81,48 @@ pub mod NFTAuction { ) { assert(starting_price >= discount_rate * duration, 'starting price too low'); - self.erc20_token.write(IERC20Dispatcher { contract_address: erc20_token }); - self.erc721_token.write(IERC721Dispatcher { contract_address: erc721_token }); + 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); + self.expires_at.write(get_block_timestamp() + duration * 1000); self.total_supply.write(total_supply); } - #[generate_trait] - impl PrivateFunctions of PrivateFunctionsTrait { + #[abi(embed_v0)] + impl NFTAuction of super::INFTAuction { fn get_price(self: @ContractState) -> u64 { - let time_elapsed = get_block_timestamp() - self.start_at.read(); + 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 + let price: u64 = self.starting_price.read() - discount; + price } - } - #[abi(embed_v0)] - impl NFTAuction of super::INFTAuction { fn buy(ref self: ContractState, token_id: u256) { // Check duration assert(get_block_timestamp() < self.expires_at.read(), 'auction ended'); // Check total supply assert(self.purchase_count.read() < self.total_supply.read(), 'auction ended'); - let caller = get_caller_address(); + 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(); // Check payment token balance - assert(self.erc20_token.read().balance_of(caller) >= price, 'insufficient balance'); + assert(buyer_balance >= price, 'insufficient balance'); // Transfer payment token to contract - self.erc20_token.read().transfer(self.seller.read(), price); + erc20_dispatcher.transfer_from(caller, self.seller.read(), price.try_into().unwrap()); // Mint token to buyer's address - self.erc721_token.read().mint(caller, 1); + erc721_dispatcher.mint(caller, token_id); // Increase purchase count self.purchase_count.write(self.purchase_count.read() + 1); diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo index 88730d0a..b7cdf18e 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_auction/src/tests.cairo @@ -2,7 +2,7 @@ use core::option::OptionTrait; use core::traits::{Into, TryInto}; use starknet::ContractAddress; use snforge_std::{ - BlockId, declare, ContractClassTrait, ContractClass, prank, CheatSpan, CheatTarget, roll + BlockId, declare, ContractClassTrait, ContractClass, prank, CheatSpan, CheatTarget, roll, warp }; use super::{ erc20::{IERC20Dispatcher, IERC20DispatcherTrait}, @@ -20,21 +20,20 @@ pub const erc20_name: felt252 = 'My Token'; pub const erc20_symbol: felt252 = 'MTKN'; pub const erc20_recipient: felt252 = 'admin'; pub const erc20_decimals: u8 = 1_u8; -pub const erc20_initial_supply: u128 = 100000000000_u128; +pub const erc20_initial_supply: u128 = 10000_u128; // NFT Auction -pub const starting_price: u64 = 10000_u64; +pub const starting_price: felt252 = 500; pub const seller: felt252 = 'seller'; -pub const duration: u64 = 60_u64; -pub const discount_rate: u64 = 5_u64; -pub const total_supply: u128 = 2_u128; +pub const duration: felt252 = 60; // in seconds +pub const discount_rate: felt252 = 5; +pub const total_supply: felt252 = 2; fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddress) { let erc721 = declare("ERC721").unwrap(); let erc721_constructor_calldata = array![erc721_name, erc721_symbol]; let (erc721_address, _) = erc721.deploy(@erc721_constructor_calldata).unwrap(); - let erc20 = declare("ERC20").unwrap(); let erc20_constructor_calldata = array![ erc20_recipient, @@ -44,41 +43,67 @@ fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddres erc20_symbol ]; let (erc20_address, _) = erc20.deploy(@erc20_constructor_calldata).unwrap(); - let nft_auction = declare("NFTAuction").unwrap(); let nft_auction_constructor_calldata = array![ erc20_address.into(), erc721_address.into(), - starting_price.into(), + starting_price, seller, - duration.into(), - discount_rate.into(), - total_supply.into() + duration, + discount_rate, + total_supply ]; let (nft_auction_address, _) = nft_auction.deploy(@nft_auction_constructor_calldata).unwrap(); - (erc721_address, erc20_address, nft_auction_address) } #[test] fn test_buy() { let (erc721_address, erc20_address, nft_auction_address) = get_contract_addresses(); - let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; let nft_auction_dispatcher = INFTAuctionDispatcher { contract_address: nft_auction_address }; - let erc20_admin: ContractAddress = 'admin'.try_into().unwrap(); - // let seller: ContractAddress = 'seller'.try_into().unwrap(); + let seller: ContractAddress = 'seller'.try_into().unwrap(); let buyer: ContractAddress = 'buyer'.try_into().unwrap(); // Transfer erc20 tokens to buyer + assert_eq!(erc20_dispatcher.balance_of(buyer), 0.into()); prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); - erc20_dispatcher.transfer(buyer, 10.into()); + let transfer_amt = 5000; + erc20_dispatcher.transfer(buyer, transfer_amt.into()); + assert_eq!(erc20_dispatcher.balance_of(buyer), transfer_amt.into()); // Buy token - prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(1)); - nft_auction_dispatcher.buy(1); + prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(3)); + prank(CheatTarget::One(erc20_address), buyer, CheatSpan::TargetCalls(2)); + + 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(); + + // 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; + warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + + // Buy token again after some time + let nft_id_2 = 2; - assert_eq!(erc721_dispatcher.owner_of(1), buyer); + // buyer approves nft auction contract to spend own erc20 token + erc20_dispatcher.approve(nft_auction_address, nft_price); + nft_auction_dispatcher.buy(nft_id_2); + assert_eq!(erc721_dispatcher.owner_of(nft_id_2), buyer); } From e98fa0944984ab5c0cf8e76c784bed9115f97594 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Mon, 3 Jun 2024 21:48:20 +0100 Subject: [PATCH 05/10] test: add more test cases for nft_auction --- .../applications/nft_auction/src/tests.cairo | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo index b7cdf18e..2440d85d 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_auction/src/tests.cairo @@ -96,7 +96,7 @@ fn test_buy() { 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; + let forward_blocktime_by = 4000; // milliseconds warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); // Buy token again after some time @@ -104,6 +104,103 @@ fn test_buy() { // 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 ended')] +fn test_buy_should_panic_when_total_supply_reached() { + let (_, erc20_address, nft_auction_address) = get_contract_addresses(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let nft_auction_dispatcher = INFTAuctionDispatcher { 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 + prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); + let transfer_amt = 5000; + erc20_dispatcher.transfer(buyer, transfer_amt.into()); + + // Buy token + prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(4)); + prank(CheatTarget::One(erc20_address), buyer, CheatSpan::TargetCalls(3)); + + let nft_id_1 = 1; + let nft_price = nft_auction_dispatcher.get_price().into(); + + // 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); + + // Forward block timestamp in order for a reduced nft price + let forward_blocktime_by = 4000; // 4 seconds (in milliseconds) + warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + + // Buy token again after some time + let nft_id_2 = 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_2); + + // Buy token again after the total supply has reached + let nft_id_3 = 3; + // buyer approves nft auction contract to spend own erc20 token + erc20_dispatcher.approve(nft_auction_address, nft_price); + nft_auction_dispatcher.buy(nft_id_3); +} + +#[test] +#[should_panic(expected: 'auction ended')] +fn test_buy_should_panic_when_duration_ended() { + let (_, erc20_address, nft_auction_address) = get_contract_addresses(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; + let nft_auction_dispatcher = INFTAuctionDispatcher { 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 + prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); + let transfer_amt = 5000; + erc20_dispatcher.transfer(buyer, transfer_amt.into()); + + // Buy token + prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(4)); + prank(CheatTarget::One(erc20_address), buyer, CheatSpan::TargetCalls(3)); + + let nft_id_1 = 1; + let nft_price = nft_auction_dispatcher.get_price().into(); + + // 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); + + // 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) + warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + + // Buy token again after some time + let nft_id_2 = 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_2); +} + +#[test] +fn test_price_decreases_after_some_time() { + let (_, _, nft_auction_address) = get_contract_addresses(); + let nft_auction_dispatcher = INFTAuctionDispatcher { 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) + warp(CheatTarget::One(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); +} From 253fb59fb4b1f6dd4d2d70d83c84a4c1ca68c365 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Mon, 3 Jun 2024 22:23:44 +0100 Subject: [PATCH 06/10] chore: update mdbook --- .../applications/nft_auction/src/nft_auction.cairo | 5 ++--- listings/applications/nft_auction/src/tests.cairo | 2 -- src/SUMMARY.md | 1 + src/ch01/nft_dutch_auction.md | 14 ++++++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 src/ch01/nft_dutch_auction.md diff --git a/listings/applications/nft_auction/src/nft_auction.cairo b/listings/applications/nft_auction/src/nft_auction.cairo index 331c12f9..062e7c94 100644 --- a/listings/applications/nft_auction/src/nft_auction.cairo +++ b/listings/applications/nft_auction/src/nft_auction.cairo @@ -117,13 +117,12 @@ pub mod NFTAuction { // Get NFT price let price: u256 = self.get_price().into(); let buyer_balance: u256 = erc20_dispatcher.balance_of(caller).into(); - // Check payment token balance + // Ensure buyer has enough token for payment assert(buyer_balance >= price, 'insufficient balance'); - // Transfer payment token to contract + // 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/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo index 2440d85d..54ddfd8c 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_auction/src/tests.cairo @@ -14,7 +14,6 @@ use super::{ pub const erc721_name: felt252 = 'My NFT'; pub const erc721_symbol: felt252 = 'MNFT'; - // ERC20 token pub const erc20_name: felt252 = 'My Token'; pub const erc20_symbol: felt252 = 'MTKN'; @@ -22,7 +21,6 @@ pub const erc20_recipient: felt252 = 'admin'; pub const erc20_decimals: u8 = 1_u8; pub const erc20_initial_supply: u128 = 10000_u128; - // NFT Auction pub const starting_price: felt252 = 500; pub const seller: felt252 = 'seller'; diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 799cb263..955beabe 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -55,6 +55,7 @@ Summary - [Upgradeable Contract](./ch01/upgradeable_contract.md) - [Defi Vault](./ch01/simple_vault.md) - [ERC20 Token](./ch01/erc20.md) +- [NFT Dutch Auction](./ch01/nft_dutch_auction.md) - [Constant Product AMM](./ch01/constant-product-amm.md) - [TimeLock](./ch01/timelock.md) - [Staking](./ch01/staking.md) diff --git a/src/ch01/nft_dutch_auction.md b/src/ch01/nft_dutch_auction.md new file mode 100644 index 00000000..765b3736 --- /dev/null +++ b/src/ch01/nft_dutch_auction.md @@ -0,0 +1,14 @@ +# NFT Dutch Auction + +This is the Cairo adaptation (with some modifications) of the [Solidity by example NFT Dutch Auction](https://solidity-by-example.org/app/dutch-auction/). + +Here's how it works: +- The seller of the NFT deploys this contract with a startingPrice. +- The auction lasts for a specified duration. +- The price decreases over time. +- Participants can purchase NFTs at any time as long as the totalSupply has not been reached. +- The auction ends when either the totalSupply is reached or the duration has elapsed. + +```rust +{{#include ../../listings/applications/nft_auction/src/nft_auction.cairo}} +``` \ No newline at end of file From 8a59ee296265b2a8bfe5179f0c820498c4e470c8 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Wed, 5 Jun 2024 02:24:53 +0100 Subject: [PATCH 07/10] chore: update nft_auction package - Add error module - Update snforge version to 0.24.0 --- Scarb.toml | 3 -- .../nft_auction/src/nft_auction.cairo | 17 ++++++---- .../applications/nft_auction/src/tests.cairo | 33 ++++++++++--------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Scarb.toml b/Scarb.toml index a19c3793..4c5c042d 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -15,10 +15,7 @@ test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh" starknet = ">=2.6.3" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" } components = { path = "listings/applications/components" } - -# [workspace.dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.24.0" } -# openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" } [workspace.package] description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet." diff --git a/listings/applications/nft_auction/src/nft_auction.cairo b/listings/applications/nft_auction/src/nft_auction.cairo index 062e7c94..0a63b7e8 100644 --- a/listings/applications/nft_auction/src/nft_auction.cairo +++ b/listings/applications/nft_auction/src/nft_auction.cairo @@ -68,6 +68,12 @@ pub mod NFTAuction { 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, @@ -79,7 +85,7 @@ pub mod NFTAuction { discount_rate: u64, total_supply: u128 ) { - assert(starting_price >= discount_rate * duration, 'starting price too low'); + assert(starting_price >= discount_rate * duration, Errors::LOW_STARTING_PRICE); self.erc20_token.write(erc20_token); self.erc721_token.write(erc721_token); @@ -98,15 +104,14 @@ pub mod NFTAuction { let time_elapsed = (get_block_timestamp() - self.start_at.read()) / 1000; // Ignore milliseconds let discount = self.discount_rate.read() * time_elapsed; - let price: u64 = self.starting_price.read() - discount; - price + self.starting_price.read() - discount } fn buy(ref self: ContractState, token_id: u256) { // Check duration - assert(get_block_timestamp() < self.expires_at.read(), 'auction ended'); + assert(get_block_timestamp() < self.expires_at.read(), Errors::AUCTION_ENDED); // Check total supply - assert(self.purchase_count.read() < self.total_supply.read(), 'auction ended'); + 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 { @@ -118,7 +123,7 @@ pub mod NFTAuction { 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, 'insufficient balance'); + 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 diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_auction/src/tests.cairo index 54ddfd8c..413403cf 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_auction/src/tests.cairo @@ -2,7 +2,8 @@ use core::option::OptionTrait; use core::traits::{Into, TryInto}; use starknet::ContractAddress; use snforge_std::{ - BlockId, declare, ContractClassTrait, ContractClass, prank, CheatSpan, CheatTarget, roll, warp + BlockId, declare, ContractClassTrait, ContractClass, cheat_caller_address, CheatSpan, + cheat_block_timestamp }; use super::{ erc20::{IERC20Dispatcher, IERC20DispatcherTrait}, @@ -67,14 +68,14 @@ fn test_buy() { // Transfer erc20 tokens to buyer assert_eq!(erc20_dispatcher.balance_of(buyer), 0.into()); - prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); + 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()); // Buy token - prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(3)); - prank(CheatTarget::One(erc20_address), buyer, CheatSpan::TargetCalls(2)); + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(3)); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(2)); let nft_id_1 = 1; let seller_bal_before_buy = erc20_dispatcher.balance_of(seller); @@ -95,7 +96,7 @@ fn test_buy() { // Forward block timestamp in order for a reduced nft price let forward_blocktime_by = 4000; // milliseconds - warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); // Buy token again after some time let nft_id_2 = 2; @@ -109,7 +110,7 @@ fn test_buy() { } #[test] -#[should_panic(expected: 'auction ended')] +#[should_panic(expected: 'auction has ended')] fn test_buy_should_panic_when_total_supply_reached() { let (_, erc20_address, nft_auction_address) = get_contract_addresses(); let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; @@ -118,13 +119,13 @@ fn test_buy_should_panic_when_total_supply_reached() { let buyer: ContractAddress = 'buyer'.try_into().unwrap(); // Transfer erc20 tokens to buyer - prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); + cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1)); let transfer_amt = 5000; erc20_dispatcher.transfer(buyer, transfer_amt.into()); // Buy token - prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(4)); - prank(CheatTarget::One(erc20_address), buyer, CheatSpan::TargetCalls(3)); + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(4)); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(3)); let nft_id_1 = 1; let nft_price = nft_auction_dispatcher.get_price().into(); @@ -135,7 +136,7 @@ fn test_buy_should_panic_when_total_supply_reached() { // Forward block timestamp in order for a reduced nft price let forward_blocktime_by = 4000; // 4 seconds (in milliseconds) - warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); // Buy token again after some time let nft_id_2 = 2; @@ -151,7 +152,7 @@ fn test_buy_should_panic_when_total_supply_reached() { } #[test] -#[should_panic(expected: 'auction ended')] +#[should_panic(expected: 'auction has ended')] fn test_buy_should_panic_when_duration_ended() { let (_, erc20_address, nft_auction_address) = get_contract_addresses(); let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; @@ -160,13 +161,13 @@ fn test_buy_should_panic_when_duration_ended() { let buyer: ContractAddress = 'buyer'.try_into().unwrap(); // Transfer erc20 tokens to buyer - prank(CheatTarget::One(erc20_address), erc20_admin, CheatSpan::TargetCalls(1)); + cheat_caller_address(erc20_address, erc20_admin, CheatSpan::TargetCalls(1)); let transfer_amt = 5000; erc20_dispatcher.transfer(buyer, transfer_amt.into()); // Buy token - prank(CheatTarget::One(nft_auction_address), buyer, CheatSpan::TargetCalls(4)); - prank(CheatTarget::One(erc20_address), buyer, CheatSpan::TargetCalls(3)); + cheat_caller_address(nft_auction_address, buyer, CheatSpan::TargetCalls(4)); + cheat_caller_address(erc20_address, buyer, CheatSpan::TargetCalls(3)); let nft_id_1 = 1; let nft_price = nft_auction_dispatcher.get_price().into(); @@ -178,7 +179,7 @@ fn test_buy_should_panic_when_duration_ended() { // 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) - warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); // Buy token again after some time let nft_id_2 = 2; @@ -196,7 +197,7 @@ fn test_price_decreases_after_some_time() { // Forward time let forward_blocktime_by = 10000; // 10 seconds (in milliseconds) - warp(CheatTarget::One(nft_auction_address), forward_blocktime_by, CheatSpan::TargetCalls(1)); + cheat_block_timestamp(nft_auction_address, forward_blocktime_by, CheatSpan::TargetCalls(1)); let nft_price_after_time_travel = nft_auction_dispatcher.get_price(); From d891f7c8b7aa1eb54fffebe5a3e21afd998581b9 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Wed, 5 Jun 2024 16:35:57 +0100 Subject: [PATCH 08/10] chore: rename package and related files from `nft_auction` to `nft_dutch_auction` --- Scarb.lock | 2 +- .../.gitignore | 0 .../Scarb.toml | 2 +- .../src/erc20.cairo | 0 .../src/erc721.cairo | 0 .../src/lib.cairo | 2 +- .../src/nft_dutch_auction.cairo} | 6 ++--- .../src/tests.cairo | 22 ++++++++++++++----- 8 files changed, 22 insertions(+), 12 deletions(-) rename listings/applications/{nft_auction => nft_dutch_auction}/.gitignore (100%) rename listings/applications/{nft_auction => nft_dutch_auction}/Scarb.toml (88%) rename listings/applications/{nft_auction => nft_dutch_auction}/src/erc20.cairo (100%) rename listings/applications/{nft_auction => nft_dutch_auction}/src/erc721.cairo (100%) rename listings/applications/{nft_auction => nft_dutch_auction}/src/lib.cairo (68%) rename listings/applications/{nft_auction/src/nft_auction.cairo => nft_dutch_auction/src/nft_dutch_auction.cairo} (97%) rename listings/applications/{nft_auction => nft_dutch_auction}/src/tests.cairo (92%) diff --git a/Scarb.lock b/Scarb.lock index 3e334983..0a52de08 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -89,7 +89,7 @@ name = "mappings" version = "0.1.0" [[package]] -name = "nft_auction" +name = "nft_dutch_auction" version = "0.1.0" dependencies = [ "snforge_std", diff --git a/listings/applications/nft_auction/.gitignore b/listings/applications/nft_dutch_auction/.gitignore similarity index 100% rename from listings/applications/nft_auction/.gitignore rename to listings/applications/nft_dutch_auction/.gitignore diff --git a/listings/applications/nft_auction/Scarb.toml b/listings/applications/nft_dutch_auction/Scarb.toml similarity index 88% rename from listings/applications/nft_auction/Scarb.toml rename to listings/applications/nft_dutch_auction/Scarb.toml index d4f20db7..fd949827 100644 --- a/listings/applications/nft_auction/Scarb.toml +++ b/listings/applications/nft_dutch_auction/Scarb.toml @@ -1,5 +1,5 @@ [package] -name = "nft_auction" +name = "nft_dutch_auction" version.workspace = true edition = '2023_11' diff --git a/listings/applications/nft_auction/src/erc20.cairo b/listings/applications/nft_dutch_auction/src/erc20.cairo similarity index 100% rename from listings/applications/nft_auction/src/erc20.cairo rename to listings/applications/nft_dutch_auction/src/erc20.cairo diff --git a/listings/applications/nft_auction/src/erc721.cairo b/listings/applications/nft_dutch_auction/src/erc721.cairo similarity index 100% rename from listings/applications/nft_auction/src/erc721.cairo rename to listings/applications/nft_dutch_auction/src/erc721.cairo diff --git a/listings/applications/nft_auction/src/lib.cairo b/listings/applications/nft_dutch_auction/src/lib.cairo similarity index 68% rename from listings/applications/nft_auction/src/lib.cairo rename to listings/applications/nft_dutch_auction/src/lib.cairo index 9df7ab72..d6ee6d7b 100644 --- a/listings/applications/nft_auction/src/lib.cairo +++ b/listings/applications/nft_dutch_auction/src/lib.cairo @@ -1,4 +1,4 @@ -pub mod nft_auction; +pub mod nft_dutch_auction; pub mod erc20; pub mod erc721; diff --git a/listings/applications/nft_auction/src/nft_auction.cairo b/listings/applications/nft_dutch_auction/src/nft_dutch_auction.cairo similarity index 97% rename from listings/applications/nft_auction/src/nft_auction.cairo rename to listings/applications/nft_dutch_auction/src/nft_dutch_auction.cairo index 0a63b7e8..2ffaf7e5 100644 --- a/listings/applications/nft_auction/src/nft_auction.cairo +++ b/listings/applications/nft_dutch_auction/src/nft_dutch_auction.cairo @@ -44,13 +44,13 @@ trait IERC721 { } #[starknet::interface] -pub trait INFTAuction { +pub trait INFTDutchAuction { fn buy(ref self: TContractState, token_id: u256); fn get_price(self: @TContractState) -> u64; } #[starknet::contract] -pub mod NFTAuction { +pub mod NFTDutchAuction { use super::{IERC20Dispatcher, IERC20DispatcherTrait, IERC721Dispatcher, IERC721DispatcherTrait}; use starknet::{ContractAddress, get_caller_address, get_contract_address, get_block_timestamp}; @@ -99,7 +99,7 @@ pub mod NFTAuction { } #[abi(embed_v0)] - impl NFTAuction of super::INFTAuction { + impl NFTDutchAuction of super::INFTDutchAuction { fn get_price(self: @ContractState) -> u64 { let time_elapsed = (get_block_timestamp() - self.start_at.read()) / 1000; // Ignore milliseconds diff --git a/listings/applications/nft_auction/src/tests.cairo b/listings/applications/nft_dutch_auction/src/tests.cairo similarity index 92% rename from listings/applications/nft_auction/src/tests.cairo rename to listings/applications/nft_dutch_auction/src/tests.cairo index 413403cf..8fde2248 100644 --- a/listings/applications/nft_auction/src/tests.cairo +++ b/listings/applications/nft_dutch_auction/src/tests.cairo @@ -8,7 +8,7 @@ use snforge_std::{ use super::{ erc20::{IERC20Dispatcher, IERC20DispatcherTrait}, erc721::{IERC721Dispatcher, IERC721DispatcherTrait}, - nft_auction::{INFTAuctionDispatcher, INFTAuctionDispatcherTrait} + nft_dutch_auction::{INFTDutchAuctionDispatcher, INFTDutchAuctionDispatcherTrait} }; // ERC721 token @@ -42,7 +42,7 @@ fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddres erc20_symbol ]; let (erc20_address, _) = erc20.deploy(@erc20_constructor_calldata).unwrap(); - let nft_auction = declare("NFTAuction").unwrap(); + let nft_auction = declare("NFTDutchAuction").unwrap(); let nft_auction_constructor_calldata = array![ erc20_address.into(), erc721_address.into(), @@ -61,7 +61,9 @@ fn test_buy() { let (erc721_address, erc20_address, nft_auction_address) = get_contract_addresses(); let erc721_dispatcher = IERC721Dispatcher { contract_address: erc721_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; - let nft_auction_dispatcher = INFTAuctionDispatcher { contract_address: nft_auction_address }; + let nft_auction_dispatcher = INFTDutchAuctionDispatcher { + contract_address: nft_auction_address + }; let erc20_admin: ContractAddress = 'admin'.try_into().unwrap(); let seller: ContractAddress = 'seller'.try_into().unwrap(); let buyer: ContractAddress = 'buyer'.try_into().unwrap(); @@ -114,7 +116,9 @@ fn test_buy() { fn test_buy_should_panic_when_total_supply_reached() { let (_, erc20_address, nft_auction_address) = get_contract_addresses(); let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; - let nft_auction_dispatcher = INFTAuctionDispatcher { contract_address: nft_auction_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(); @@ -156,7 +160,9 @@ fn test_buy_should_panic_when_total_supply_reached() { fn test_buy_should_panic_when_duration_ended() { let (_, erc20_address, nft_auction_address) = get_contract_addresses(); let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; - let nft_auction_dispatcher = INFTAuctionDispatcher { contract_address: nft_auction_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(); @@ -191,7 +197,9 @@ fn test_buy_should_panic_when_duration_ended() { #[test] fn test_price_decreases_after_some_time() { let (_, _, nft_auction_address) = get_contract_addresses(); - let nft_auction_dispatcher = INFTAuctionDispatcher { contract_address: nft_auction_address }; + let nft_auction_dispatcher = INFTDutchAuctionDispatcher { + contract_address: nft_auction_address + }; let nft_price_before_time_travel = nft_auction_dispatcher.get_price(); @@ -201,5 +209,7 @@ fn test_price_decreases_after_some_time() { let nft_price_after_time_travel = nft_auction_dispatcher.get_price(); + println!("price: {:?}", nft_price_after_time_travel); + assert_gt!(nft_price_before_time_travel, nft_price_after_time_travel); } From 89d43bebf68d067928b9f564b864c7d36d67b79f Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Wed, 5 Jun 2024 19:57:10 +0100 Subject: [PATCH 09/10] chore: reused existing package --- Scarb.lock | 1 + listings/applications/erc20/src/lib.cairo | 5 +- .../applications/nft_dutch_auction/Scarb.toml | 2 + .../nft_dutch_auction/src/erc20.cairo | 210 ------------------ .../nft_dutch_auction/src/lib.cairo | 1 - .../nft_dutch_auction/src/tests.cairo | 4 +- 6 files changed, 9 insertions(+), 214 deletions(-) delete mode 100644 listings/applications/nft_dutch_auction/src/erc20.cairo diff --git a/Scarb.lock b/Scarb.lock index 0a52de08..ef3d064e 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -92,6 +92,7 @@ version = "0.1.0" name = "nft_dutch_auction" version = "0.1.0" dependencies = [ + "erc20", "snforge_std", ] diff --git a/listings/applications/erc20/src/lib.cairo b/listings/applications/erc20/src/lib.cairo index 40d3ff58..51c78654 100644 --- a/listings/applications/erc20/src/lib.cairo +++ b/listings/applications/erc20/src/lib.cairo @@ -1 +1,4 @@ -mod token; +pub mod token; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/nft_dutch_auction/Scarb.toml b/listings/applications/nft_dutch_auction/Scarb.toml index fd949827..a6af238d 100644 --- a/listings/applications/nft_dutch_auction/Scarb.toml +++ b/listings/applications/nft_dutch_auction/Scarb.toml @@ -4,6 +4,7 @@ version.workspace = true edition = '2023_11' [dependencies] +erc20 = { path = "../erc20" } starknet.workspace = true [dev-dependencies] @@ -13,3 +14,4 @@ snforge_std.workspace = true test.workspace = true [[target.starknet-contract]] +build-external-contracts = ["erc20::token::erc20"] diff --git a/listings/applications/nft_dutch_auction/src/erc20.cairo b/listings/applications/nft_dutch_auction/src/erc20.cairo deleted file mode 100644 index 18f5d923..00000000 --- a/listings/applications/nft_dutch_auction/src/erc20.cairo +++ /dev/null @@ -1,210 +0,0 @@ -use starknet::ContractAddress; - -#[starknet::interface] -pub trait IERC20 { - fn get_name(self: @TContractState) -> felt252; - fn get_symbol(self: @TContractState) -> felt252; - fn get_decimals(self: @TContractState) -> u8; - fn get_total_supply(self: @TContractState) -> felt252; - fn balance_of(self: @TContractState, account: ContractAddress) -> felt252; - fn allowance( - self: @TContractState, owner: ContractAddress, spender: ContractAddress - ) -> felt252; - fn transfer(ref self: TContractState, recipient: ContractAddress, amount: felt252); - fn transfer_from( - ref self: TContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: felt252 - ); - fn approve(ref self: TContractState, spender: ContractAddress, amount: felt252); - fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: felt252); - fn decrease_allowance( - ref self: TContractState, spender: ContractAddress, subtracted_value: felt252 - ); -} - -#[starknet::contract] -mod ERC20 { - use core::num::traits::Zero; - use starknet::get_caller_address; - use starknet::contract_address_const; - use starknet::ContractAddress; - - #[storage] - struct Storage { - name: felt252, - symbol: felt252, - decimals: u8, - total_supply: felt252, - balances: LegacyMap::, - allowances: LegacyMap::<(ContractAddress, ContractAddress), felt252>, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - Transfer: Transfer, - Approval: Approval, - } - #[derive(Drop, starknet::Event)] - struct Transfer { - from: ContractAddress, - to: ContractAddress, - value: felt252, - } - #[derive(Drop, starknet::Event)] - struct Approval { - owner: ContractAddress, - spender: ContractAddress, - value: felt252, - } - - mod Errors { - pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; - pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; - pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; - pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; - pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; - pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; - } - - #[constructor] - fn constructor( - ref self: ContractState, - recipient: ContractAddress, - name: felt252, - decimals: u8, - initial_supply: felt252, - symbol: felt252 - ) { - self.name.write(name); - self.symbol.write(symbol); - self.decimals.write(decimals); - self.mint(recipient, initial_supply); - } - - #[abi(embed_v0)] - impl IERC20Impl of super::IERC20 { - fn get_name(self: @ContractState) -> felt252 { - self.name.read() - } - - fn get_symbol(self: @ContractState) -> felt252 { - self.symbol.read() - } - - fn get_decimals(self: @ContractState) -> u8 { - self.decimals.read() - } - - fn get_total_supply(self: @ContractState) -> felt252 { - self.total_supply.read() - } - - fn balance_of(self: @ContractState, account: ContractAddress) -> felt252 { - self.balances.read(account) - } - - fn allowance( - self: @ContractState, owner: ContractAddress, spender: ContractAddress - ) -> felt252 { - self.allowances.read((owner, spender)) - } - - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: felt252) { - let sender = get_caller_address(); - self._transfer(sender, recipient, amount); - } - - fn transfer_from( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: felt252 - ) { - let caller = get_caller_address(); - self.spend_allowance(sender, caller, amount); - self._transfer(sender, recipient, amount); - } - - fn approve(ref self: ContractState, spender: ContractAddress, amount: felt252) { - let caller = get_caller_address(); - self.approve_helper(caller, spender, amount); - } - - fn increase_allowance( - ref self: ContractState, spender: ContractAddress, added_value: felt252 - ) { - let caller = get_caller_address(); - self - .approve_helper( - caller, spender, self.allowances.read((caller, spender)) + added_value - ); - } - - fn decrease_allowance( - ref self: ContractState, spender: ContractAddress, subtracted_value: felt252 - ) { - let caller = get_caller_address(); - self - .approve_helper( - caller, spender, self.allowances.read((caller, spender)) - subtracted_value - ); - } - } - - #[generate_trait] - impl InternalImpl of InternalTrait { - fn _transfer( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: felt252 - ) { - assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); - assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO); - self.balances.write(sender, self.balances.read(sender) - amount); - self.balances.write(recipient, self.balances.read(recipient) + amount); - self.emit(Transfer { from: sender, to: recipient, value: amount }); - } - - fn spend_allowance( - ref self: ContractState, - owner: ContractAddress, - spender: ContractAddress, - amount: felt252 - ) { - let allowance = self.allowances.read((owner, spender)); - self.allowances.write((owner, spender), allowance - amount); - } - - fn approve_helper( - ref self: ContractState, - owner: ContractAddress, - spender: ContractAddress, - amount: felt252 - ) { - assert(!spender.is_zero(), Errors::APPROVE_TO_ZERO); - self.allowances.write((owner, spender), amount); - self.emit(Approval { owner, spender, value: amount }); - } - - fn mint(ref self: ContractState, recipient: ContractAddress, amount: felt252) { - assert(!recipient.is_zero(), Errors::MINT_TO_ZERO); - let supply = self.total_supply.read() + amount; - self.total_supply.write(supply); - let balance = self.balances.read(recipient) + amount; - self.balances.write(recipient, balance); - self - .emit( - Event::Transfer( - Transfer { - from: contract_address_const::<0>(), to: recipient, value: amount - } - ) - ); - } - } -} - diff --git a/listings/applications/nft_dutch_auction/src/lib.cairo b/listings/applications/nft_dutch_auction/src/lib.cairo index d6ee6d7b..33c4ff24 100644 --- a/listings/applications/nft_dutch_auction/src/lib.cairo +++ b/listings/applications/nft_dutch_auction/src/lib.cairo @@ -1,5 +1,4 @@ pub mod nft_dutch_auction; -pub mod erc20; pub mod erc721; #[cfg(test)] diff --git a/listings/applications/nft_dutch_auction/src/tests.cairo b/listings/applications/nft_dutch_auction/src/tests.cairo index 8fde2248..be89a25f 100644 --- a/listings/applications/nft_dutch_auction/src/tests.cairo +++ b/listings/applications/nft_dutch_auction/src/tests.cairo @@ -6,10 +6,10 @@ use snforge_std::{ cheat_block_timestamp }; use super::{ - erc20::{IERC20Dispatcher, IERC20DispatcherTrait}, erc721::{IERC721Dispatcher, IERC721DispatcherTrait}, nft_dutch_auction::{INFTDutchAuctionDispatcher, INFTDutchAuctionDispatcherTrait} }; +use erc20::token::{IERC20Dispatcher, IERC20DispatcherTrait}; // ERC721 token pub const erc721_name: felt252 = 'My NFT'; @@ -33,7 +33,7 @@ fn get_contract_addresses() -> (ContractAddress, ContractAddress, ContractAddres let erc721 = declare("ERC721").unwrap(); let erc721_constructor_calldata = array![erc721_name, erc721_symbol]; let (erc721_address, _) = erc721.deploy(@erc721_constructor_calldata).unwrap(); - let erc20 = declare("ERC20").unwrap(); + let erc20 = declare("erc20").unwrap(); let erc20_constructor_calldata = array![ erc20_recipient, erc20_name, From 48335cba73ed7cfff9b24edb20b09d89236dad51 Mon Sep 17 00:00:00 2001 From: julio4 Date: Sun, 9 Jun 2024 15:34:26 +0900 Subject: [PATCH 10/10] fix: cli#204 --- listings/applications/erc20/src/lib.cairo | 3 --- listings/applications/erc20/src/token.cairo | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/listings/applications/erc20/src/lib.cairo b/listings/applications/erc20/src/lib.cairo index 51c78654..79c66ba6 100644 --- a/listings/applications/erc20/src/lib.cairo +++ b/listings/applications/erc20/src/lib.cairo @@ -1,4 +1 @@ pub mod token; - -#[cfg(test)] -mod tests; diff --git a/listings/applications/erc20/src/token.cairo b/listings/applications/erc20/src/token.cairo index 7d04f8b8..c2477789 100644 --- a/listings/applications/erc20/src/token.cairo +++ b/listings/applications/erc20/src/token.cairo @@ -249,7 +249,7 @@ mod tests { fn test_deploy_when_recipient_is_address_zero() { let recipient: ContractAddress = Zero::zero(); - let (contract_address, _) = deploy_syscall( + let (_contract_address, _) = deploy_syscall( erc20::TEST_CLASS_HASH.try_into().unwrap(), recipient.into(), array![recipient.into(), token_name, decimals.into(), initial_supply, symbols].span(),