Skip to content

Commit

Permalink
feat: add nft dutch contract
Browse files Browse the repository at this point in the history
  • Loading branch information
Jagadeeshftw committed Feb 27, 2025
1 parent 5713ff7 commit 70bec42
Show file tree
Hide file tree
Showing 9 changed files with 667 additions and 0 deletions.
6 changes: 6 additions & 0 deletions contracts/Scarb.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "alexandria_storage"
version = "0.2.0"
source = "git+https://github.com/keep-starknet-strange/alexandria?tag=v0.2.0#bcc4fa417abbc8b067ae6f2d382b127006b5077c"

[[package]]
name = "contracts"
version = "0.1.0"
dependencies = [
"alexandria_storage",
"openzeppelin",
"openzeppelin_testing",
"snforge_std",
Expand Down
89 changes: 89 additions & 0 deletions contracts/src/NFTDutchAuction.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#[starknet::contract]
pub mod NFTDutchAuction {
use core::integer::u256;
use contracts::interfaces::INFTDutchAuction::INFTDutchAuction;
use contracts::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};

#[storage]
struct Storage {
erc20_token: ContractAddress,
erc721_token: ContractAddress,
starting_price: u64,
seller: ContractAddress,
duration: u64,
discount_rate: u64,
start_at: u64,
expires_at: u64,
purchase_count: u128,
total_supply: u128,
}

mod Errors {
pub const AUCTION_ENDED: felt252 = 'auction has ended';
pub const LOW_STARTING_PRICE: felt252 = 'low starting price';
pub const INSUFFICIENT_BALANCE: felt252 = 'insufficient balance';
}

#[constructor]
fn constructor(
ref self: ContractState,
erc20_token: ContractAddress,
erc721_token: ContractAddress,
starting_price: u64,
seller: ContractAddress,
duration: u64,
discount_rate: u64,
total_supply: u128,
) {
assert(starting_price >= discount_rate * duration, Errors::LOW_STARTING_PRICE);

self.erc20_token.write(erc20_token);
self.erc721_token.write(erc721_token);
self.starting_price.write(starting_price);
self.seller.write(seller);
self.duration.write(duration);
self.discount_rate.write(discount_rate);
self.start_at.write(get_block_timestamp());
self.expires_at.write(get_block_timestamp() + duration * 1000);
self.total_supply.write(total_supply);
}

#[abi(embed_v0)]
impl NFTDutchAuction of INFTDutchAuction<ContractState> {
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();
println!("Caller inside the buy: {:?}", caller);
// 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);
}
}
}
2 changes: 2 additions & 0 deletions contracts/src/interfaces.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod IDefiVault;
pub mod IStarkIdentity;
pub mod timelock;
pub mod INFTDutchAuction;
pub mod IERC721;
20 changes: 20 additions & 0 deletions contracts/src/interfaces/IERC721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use starknet::ContractAddress;

#[starknet::interface]
pub trait IERC721<TContractState> {
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);
}
5 changes: 5 additions & 0 deletions contracts/src/interfaces/INFTDutchAuction.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[starknet::interface]
pub trait INFTDutchAuction<TContractState> {
fn buy(ref self: TContractState, token_id: u256);
fn get_price(self: @TContractState) -> u64;
}
2 changes: 2 additions & 0 deletions contracts/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ pub mod merkle;
pub mod starkfinder;
pub mod starkidentity;
pub mod mock_erc20;
pub mod mock_erc721;
pub mod NFTDutchAuction;
pub mod timelock;
221 changes: 221 additions & 0 deletions contracts/src/mock_erc721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use starknet::ContractAddress;

#[starknet::interface]
pub trait IERC721<TContractState> {
fn get_name(self: @TContractState) -> felt252;
fn get_symbol(self: @TContractState) -> felt252;
fn get_token_uri(self: @TContractState, token_id: u256) -> felt252;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress;
fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress;
fn is_approved_for_all(
self: @TContractState, owner: ContractAddress, operator: ContractAddress,
) -> bool;
fn approve(ref self: TContractState, to: ContractAddress, token_id: u256);
fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool);
fn transfer_from(
ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256,
);
fn mint(ref self: TContractState, to: ContractAddress, token_id: u256);
}

#[starknet::contract]
mod MockERC721 {

use starknet::{ContractAddress, get_caller_address};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use core::num::traits::Zero;

#[storage]
struct Storage {
name: felt252,
symbol: felt252,
owners: Map::<u256, ContractAddress>,
balances: Map::<ContractAddress, u256>,
token_approvals: Map::<u256, ContractAddress>,
operator_approvals: Map::<(ContractAddress, ContractAddress), bool>,
token_uri: Map<u256, felt252>,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Approval: Approval,
Transfer: Transfer,
ApprovalForAll: ApprovalForAll,
}

#[derive(Drop, starknet::Event)]
struct Approval {
owner: ContractAddress,
to: ContractAddress,
token_id: u256,
}

#[derive(Drop, starknet::Event)]
struct Transfer {
from: ContractAddress,
to: ContractAddress,
token_id: u256,
}

#[derive(Drop, starknet::Event)]
struct ApprovalForAll {
owner: ContractAddress,
operator: ContractAddress,
approved: bool,
}

#[constructor]
fn constructor(ref self: ContractState, _name: felt252, _symbol: felt252) {
self.name.write(_name);
self.symbol.write(_symbol);
}

#[abi(embed_v0)]
impl IERC721Impl of super::IERC721<ContractState> {
fn get_name(self: @ContractState) -> felt252 {
self.name.read()
}
fn get_symbol(self: @ContractState) -> felt252 {
self.symbol.read()
}
fn get_token_uri(self: @ContractState, token_id: u256) -> felt252 {
assert(self._exists(token_id), 'ERC721: invalid token ID');
self.token_uri.read(token_id)
}
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
assert(account.is_non_zero(), 'ERC721: address zero');
self.balances.read(account)
}
fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress {
let owner = self.owners.read(token_id);
owner
}
fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress {
assert(self._exists(token_id), 'ERC721: invalid token ID');
self.token_approvals.read(token_id)
}
fn is_approved_for_all(
self: @ContractState, owner: ContractAddress, operator: ContractAddress,
) -> bool {
self.operator_approvals.read((owner, operator))
}

fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) {
let owner = self.owner_of(token_id);
assert(to != owner, 'Approval to current owner');
assert(
get_caller_address() == owner
|| self.is_approved_for_all(owner, get_caller_address()),
'Not token owner',
);
self.token_approvals.write(token_id, to);
self.emit(Approval { owner: self.owner_of(token_id), to: to, token_id: token_id });
}

fn set_approval_for_all(
ref self: ContractState, operator: ContractAddress, approved: bool,
) {
let owner = get_caller_address();
assert(owner != operator, 'ERC721: approve to caller');
self.operator_approvals.write((owner, operator), approved);
self.emit(ApprovalForAll { owner: owner, operator: operator, approved: approved });
}

fn transfer_from(
ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256,
) {
assert(
self._is_approved_or_owner(get_caller_address(), token_id),
'neither owner nor approved',
);
self._transfer(from, to, token_id);
}

fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) {
self._mint(to, token_id);
}
}

#[generate_trait]
impl ERC721HelperImpl of ERC721HelperTrait {

fn _exists(self: @ContractState, token_id: u256) -> bool {
// check that owner of token is not zero
self.owner_of(token_id).is_non_zero()
}

fn _is_approved_or_owner(
self: @ContractState, spender: ContractAddress, token_id: u256,
) -> bool {
let owner = self.owners.read(token_id);
spender == owner
|| self.is_approved_for_all(owner, spender)
|| self.get_approved(token_id) == spender
}

fn _set_token_uri(ref self: ContractState, token_id: u256, token_uri: felt252) {
assert(self._exists(token_id), 'ERC721: invalid token ID');
self.token_uri.write(token_id, token_uri);
}

fn _transfer(
ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256,
) {
// check that from address is equal to owner of token
assert(from == self.owner_of(token_id), 'ERC721: Caller is not owner');
// check that to address is not zero
assert(to.is_non_zero(), 'ERC721: transfer to 0 address');

// remove previously made approvals
self.token_approvals.write(token_id, Zero::zero());

// increase balance of to address, decrease balance of from address
self.balances.write(from, self.balances.read(from) - 1.into());
self.balances.write(to, self.balances.read(to) + 1.into());

// update token_id owner
self.owners.write(token_id, to);

// emit the Transfer event
self.emit(Transfer { from: from, to: to, token_id: token_id });
}

fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) {
assert(to.is_non_zero(), 'TO_IS_ZERO_ADDRESS');

// Ensures token_id is unique
assert(!self.owner_of(token_id).is_non_zero(), 'ERC721: Token already minted');

// Increase receiver balance
let receiver_balance = self.balances.read(to);
self.balances.write(to, receiver_balance + 1.into());

// Update token_id owner
self.owners.write(token_id, to);

// emit Transfer event
self.emit(Transfer { from: Zero::zero(), to: to, token_id: token_id });
}

fn _burn(ref self: ContractState, token_id: u256) {
let owner = self.owner_of(token_id);

// Clear approvals
self.token_approvals.write(token_id, Zero::zero());

// Decrease owner balance
let owner_balance = self.balances.read(owner);
self.balances.write(owner, owner_balance - 1.into());

// Delete owner
self.owners.write(token_id, Zero::zero());
// emit the Transfer event
self.emit(Transfer { from: owner, to: Zero::zero(), token_id: token_id });
}
}
}
1 change: 1 addition & 0 deletions contracts/tests/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ mod test_defi_contract;
mod test_erc20;
mod test_starkfinder;
mod test_starkidentity;
mod test_nft_dutch;
#[feature("safe_dispatcher")]
mod test_timelock;
Loading

0 comments on commit 70bec42

Please sign in to comment.