From d93d954c8ff1012069dc8dc0e5f066f3d2ed27d2 Mon Sep 17 00:00:00 2001 From: Jagadeeshftw Date: Mon, 24 Feb 2025 02:30:42 +0530 Subject: [PATCH] feat: add nft dutch contract --- contracts/nft_dutch_contract/.gitignore | 2 + contracts/nft_dutch_contract/.tool-versions | 2 + contracts/nft_dutch_contract/Scarb.lock | 24 +++++ contracts/nft_dutch_contract/Scarb.toml | 52 +++++++++++ contracts/nft_dutch_contract/snfoundry.toml | 11 +++ .../nft_dutch_contract/src/interfaces.cairo | 3 + .../src/interfaces/ierc20.cairo | 25 ++++++ .../src/interfaces/ierc721.cairo | 20 +++++ .../src/interfaces/inft_dutch_auction.cairo | 5 ++ contracts/nft_dutch_contract/src/lib.cairo | 2 + .../src/nft_dutch_auction.cairo | 87 +++++++++++++++++++ .../tests/test_contract.cairo | 78 +++++++++++++++++ 12 files changed, 311 insertions(+) create mode 100644 contracts/nft_dutch_contract/.gitignore create mode 100644 contracts/nft_dutch_contract/.tool-versions create mode 100644 contracts/nft_dutch_contract/Scarb.lock create mode 100644 contracts/nft_dutch_contract/Scarb.toml create mode 100644 contracts/nft_dutch_contract/snfoundry.toml create mode 100644 contracts/nft_dutch_contract/src/interfaces.cairo create mode 100644 contracts/nft_dutch_contract/src/interfaces/ierc20.cairo create mode 100644 contracts/nft_dutch_contract/src/interfaces/ierc721.cairo create mode 100644 contracts/nft_dutch_contract/src/interfaces/inft_dutch_auction.cairo create mode 100644 contracts/nft_dutch_contract/src/lib.cairo create mode 100644 contracts/nft_dutch_contract/src/nft_dutch_auction.cairo create mode 100644 contracts/nft_dutch_contract/tests/test_contract.cairo diff --git a/contracts/nft_dutch_contract/.gitignore b/contracts/nft_dutch_contract/.gitignore new file mode 100644 index 0000000..73aa31e --- /dev/null +++ b/contracts/nft_dutch_contract/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/contracts/nft_dutch_contract/.tool-versions b/contracts/nft_dutch_contract/.tool-versions new file mode 100644 index 0000000..f0d146d --- /dev/null +++ b/contracts/nft_dutch_contract/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.10.1 +starknet-foundry 0.37.0 diff --git a/contracts/nft_dutch_contract/Scarb.lock b/contracts/nft_dutch_contract/Scarb.lock new file mode 100644 index 0000000..6ad05fd --- /dev/null +++ b/contracts/nft_dutch_contract/Scarb.lock @@ -0,0 +1,24 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "nft_dutch_contract" +version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.37.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:9dbb114f853decc27b2d6d53e2ddd207217ce63c2d24a47c5c48d5f475b0b9a5" + +[[package]] +name = "snforge_std" +version = "0.37.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:f5702c4a6d54e3563b4aa78c834de6ddcf18ef8ca8fd35dc1bceb7ece58e9571" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/contracts/nft_dutch_contract/Scarb.toml b/contracts/nft_dutch_contract/Scarb.toml new file mode 100644 index 0000000..aedf946 --- /dev/null +++ b/contracts/nft_dutch_contract/Scarb.toml @@ -0,0 +1,52 @@ +[package] +name = "nft_dutch_contract" +version = "0.1.0" +edition = "2024_07" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.10.1" + +[dev-dependencies] +snforge_std = "0.37.0" +assert_macros = "2.10.1" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std"] + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it diff --git a/contracts/nft_dutch_contract/snfoundry.toml b/contracts/nft_dutch_contract/snfoundry.toml new file mode 100644 index 0000000..306a097 --- /dev/null +++ b/contracts/nft_dutch_contract/snfoundry.toml @@ -0,0 +1,11 @@ +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html +# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information + +# [sncast.default] # Define a profile name +# url = "https://free-rpc.nethermind.io/sepolia-juno/v0_7" # Url of the RPC provider +# accounts-file = "../account-file" # Path to the file with the account data +# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions +# keystore = "~/keystore" # Path to the keystore file +# wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters +# block-explorer = "StarkScan" # Block explorer service used to display links to transaction details +# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer diff --git a/contracts/nft_dutch_contract/src/interfaces.cairo b/contracts/nft_dutch_contract/src/interfaces.cairo new file mode 100644 index 0000000..1f947a0 --- /dev/null +++ b/contracts/nft_dutch_contract/src/interfaces.cairo @@ -0,0 +1,3 @@ +pub mod ierc20; +pub mod ierc721; +pub mod inft_dutch_auction; \ No newline at end of file diff --git a/contracts/nft_dutch_contract/src/interfaces/ierc20.cairo b/contracts/nft_dutch_contract/src/interfaces/ierc20.cairo new file mode 100644 index 0000000..e92a95e --- /dev/null +++ b/contracts/nft_dutch_contract/src/interfaces/ierc20.cairo @@ -0,0 +1,25 @@ +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, + ); +} \ No newline at end of file diff --git a/contracts/nft_dutch_contract/src/interfaces/ierc721.cairo b/contracts/nft_dutch_contract/src/interfaces/ierc721.cairo new file mode 100644 index 0000000..8a50e01 --- /dev/null +++ b/contracts/nft_dutch_contract/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); +} \ No newline at end of file diff --git a/contracts/nft_dutch_contract/src/interfaces/inft_dutch_auction.cairo b/contracts/nft_dutch_contract/src/interfaces/inft_dutch_auction.cairo new file mode 100644 index 0000000..2bc9564 --- /dev/null +++ b/contracts/nft_dutch_contract/src/interfaces/inft_dutch_auction.cairo @@ -0,0 +1,5 @@ +#[starknet::interface] +pub trait INFTDutchAuction { + fn buy(ref self: TContractState, token_id: u256); + fn get_price(self: @TContractState) -> u64; +} \ No newline at end of file diff --git a/contracts/nft_dutch_contract/src/lib.cairo b/contracts/nft_dutch_contract/src/lib.cairo new file mode 100644 index 0000000..652458a --- /dev/null +++ b/contracts/nft_dutch_contract/src/lib.cairo @@ -0,0 +1,2 @@ +pub mod interfaces; +pub mod nft_dutch_auction; \ No newline at end of file diff --git a/contracts/nft_dutch_contract/src/nft_dutch_auction.cairo b/contracts/nft_dutch_contract/src/nft_dutch_auction.cairo new file mode 100644 index 0000000..ef90f36 --- /dev/null +++ b/contracts/nft_dutch_contract/src/nft_dutch_auction.cairo @@ -0,0 +1,87 @@ +#[starknet::contract] +pub mod NFTDutchAuction { + use nft_dutch_contract::interfaces::inft_dutch_auction::INFTDutchAuction; + use nft_dutch_contract::interfaces::ierc20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use nft_dutch_contract::interfaces::ierc721::{IERC721Dispatcher, IERC721DispatcherTrait}; + 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); + } + } +} \ No newline at end of file diff --git a/contracts/nft_dutch_contract/tests/test_contract.cairo b/contracts/nft_dutch_contract/tests/test_contract.cairo new file mode 100644 index 0000000..f86452a --- /dev/null +++ b/contracts/nft_dutch_contract/tests/test_contract.cairo @@ -0,0 +1,78 @@ +use starknet::ContractAddress; +use snforge_std::{ + declare, ContractClassTrait, DeclareResultTrait, spy_events, EventSpy +}; + +use core::traits::TryInto; +use core::option::OptionTrait; + +use nft_dutch_contract::interfaces::inft_dutch_auction::{INFTDutchAuctionDispatcher, INFTDutchAuctionDispatcherTrait}; +use nft_dutch_contract::interfaces::ierc20::{IERC20Dispatcher, IERC20DispatcherTrait}; +use nft_dutch_contract::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(owner: ContractAddress) -> ContractAddress { + let erc20_contract = declare("MockERC20").unwrap().contract_class(); + let mut calldata = array![]; + calldata.append(owner.into()); + let (contract_address, _) = erc20_contract.deploy(@calldata).unwrap(); + contract_address +} + +fn deploy_erc721(owner: ContractAddress) -> ContractAddress { + let erc721_contract = declare("MockERC721").unwrap().contract_class(); + let mut calldata = array![]; + calldata.append(owner.into()); + let (contract_address, _) = erc721_contract.deploy(@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(owner); + let erc721_token = deploy_erc721(owner); + + 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'); +}