Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add nft dutch contract #179

Merged
merged 2 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
88 changes: 88 additions & 0 deletions contracts/src/NFTDutchAuction.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#[starknet::contract]
pub mod NFTDutchAuction {
use core::integer::u256;
use contracts::interfaces::INFTDutchAuction::INFTDutchAuction;
use contracts::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};

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

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

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

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

#[abi(embed_v0)]
impl NFTDutchAuction of INFTDutchAuction<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();
// 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;
220 changes: 220 additions & 0 deletions contracts/src/mock_erc721.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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
Loading