Skip to content

Commit

Permalink
Deploy smart contracts to Starknet Sepolia (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
bitfalt authored Dec 21, 2024
1 parent bcd5bdf commit 01ea770
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 73 deletions.
1 change: 1 addition & 0 deletions apps/snfoundry/contracts/src/cofi_collection.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ mod CofiCollection {
self.accesscontrol._grant_role(MINTER_ROLE, minter);
self.accesscontrol._grant_role(URI_SETTER_ROLE, uri_setter);
self.accesscontrol._grant_role(UPGRADER_ROLE, upgrader);
self.erc1155._set_base_uri("ipfs://");
}

impl ERC1155HooksImpl of ERC1155Component::ERC1155HooksTrait<ContractState> {
Expand Down
157 changes: 100 additions & 57 deletions apps/snfoundry/contracts/src/marketplace.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub trait IMarketplace<ContractState> {
fn assign_admin_role(ref self: ContractState, assignee: ContractAddress);
fn buy_product(ref self: ContractState, token_id: u256, token_amount: u256);
fn buy_products(ref self: ContractState, token_ids: Span<u256>, token_amount: Span<u256>);
fn create_product(ref self: ContractState, initial_stock: u256, price: u256, data: Span<felt252>);
fn create_product(
ref self: ContractState, initial_stock: u256, price: u256, data: Span<felt252>
);
fn create_products(ref self: ContractState, initial_stock: Span<u256>, price: Span<u256>);
fn delete_product(ref self: ContractState, token_id: u256);
fn delete_products(ref self: ContractState, token_ids: Span<u256>);
Expand All @@ -19,18 +21,22 @@ pub trait IMarketplace<ContractState> {

#[starknet::contract]
mod Marketplace {
use starknet::event::EventEmitter;
use openzeppelin::access::accesscontrol::AccessControlComponent;
use contracts::cofi_collection::{ICofiCollectionDispatcher, ICofiCollectionDispatcherTrait};
use openzeppelin::access::accesscontrol::AccessControlComponent;
use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent;
use openzeppelin::introspection::src5::SRC5Component;
use openzeppelin::token::erc1155::erc1155_receiver::ERC1155ReceiverComponent;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use openzeppelin::upgrades::UpgradeableComponent;
use contracts::cofi_collection::{ICofiCollectionDispatcher, ICofiCollectionDispatcherTrait};
use starknet::{ContractAddress, get_caller_address, get_contract_address, contract_address_const};
use starknet::event::EventEmitter;
use starknet::storage::Map;
use starknet::{
ContractAddress, get_caller_address, get_contract_address, contract_address_const
};

component!(path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent);
component!(
path: ERC1155ReceiverComponent, storage: erc1155_receiver, event: ERC1155ReceiverEvent
);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);
component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);
Expand All @@ -41,7 +47,8 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;

// ERC1155Receiver
#[abi(embed_v0)]
impl ERC1155ReceiverImpl = ERC1155ReceiverComponent::ERC1155ReceiverImpl<ContractState>;
impl ERC1155ReceiverImpl =
ERC1155ReceiverComponent::ERC1155ReceiverImpl<ContractState>;
impl ERC1155ReceiverInternalImpl = ERC1155ReceiverComponent::InternalImpl<ContractState>;

// SRC5
Expand All @@ -50,13 +57,15 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;

// Access Control
#[abi(embed_v0)]
impl AccessControlImpl = AccessControlComponent::AccessControlImpl<ContractState>;
impl AccessControlImpl =
AccessControlComponent::AccessControlImpl<ContractState>;
impl AccessControlInternalImpl = AccessControlComponent::InternalImpl<ContractState>;

// Upgradeable
impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl<ContractState>;

const STRK_TOKEN_ADDRESS: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d;
const STRK_TOKEN_ADDRESS: felt252 =
0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d;

#[storage]
struct Storage {
Expand Down Expand Up @@ -94,8 +103,6 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
BuyProduct: BuyProduct,
BuyBatchProducts: BuyBatchProducts,
PaymentSeller: PaymentSeller,


}
// Emitted when a product is unlisted from the Marketplace
#[derive(Drop, PartialEq, starknet::Event)]
Expand Down Expand Up @@ -147,7 +154,8 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
/// * `cofi_collection_address` - The address of the CofiCollection contract
/// * `admin` - The address of the admin role
/// * `market_fee` - The fee that the marketplace will take from the sales
/// * `base_uri` - The base uri for the NFTs metadata. Should contain `{id}` so that metadata gets
/// * `base_uri` - The base uri for the NFTs metadata. Should contain `{id}` so that metadata
/// gets
/// replace per each token id. Example: https://example.com/metadata/{id}.json
///
#[constructor]
Expand All @@ -156,16 +164,13 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
cofi_collection_address: ContractAddress,
admin: ContractAddress,
market_fee: u256,
base_uri: ByteArray,
) {
self.erc1155_receiver.initializer();
self.accesscontrol.initializer();
self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, admin);
self.cofi_collection_address.write(cofi_collection_address);
self.market_fee.write(market_fee);
self.current_token_id.write(1);

ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()}.set_base_uri(base_uri);
}

#[abi(embed_v0)]
Expand All @@ -174,7 +179,7 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol._grant_role(PRODUCER, assignee);
}

fn assign_consumer_role(ref self: ContractState, assignee: ContractAddress) {
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol._grant_role(CONSUMER, assignee);
Expand All @@ -183,8 +188,8 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
fn assign_admin_role(ref self: ContractState, assignee: ContractAddress) {
self.accesscontrol.assert_only_role(DEFAULT_ADMIN_ROLE);
self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, assignee);
}
}

fn buy_product(ref self: ContractState, token_id: u256, token_amount: u256) {
let stock = self.listed_product_stock.read(token_id);
assert(stock >= token_amount, 'Not enough stock');
Expand All @@ -196,14 +201,23 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
};
// Get payment from buyer
let mut producer_fee = self.listed_product_price.read(token_id) * token_amount;
let mut total_price = producer_fee + self.calculate_fee(producer_fee, self.market_fee.read());
assert(strk_token_dispatcher.balance_of(get_caller_address()) >= total_price, 'insufficient funds');
let mut total_price = producer_fee
+ self.calculate_fee(producer_fee, self.market_fee.read());
assert(
strk_token_dispatcher.balance_of(get_caller_address()) >= total_price,
'insufficient funds'
);
strk_token_dispatcher.transfer_from(buyer, contract_address, total_price);

// Transfer the nft products
let cofi_collection = ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()};
cofi_collection.safe_transfer_from(contract_address, buyer, token_id, token_amount, array![0].span());

let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read()
};
cofi_collection
.safe_transfer_from(
contract_address, buyer, token_id, token_amount, array![0].span()
);

// Update stock
let new_stock = stock - token_amount;
self.update_stock(token_id, new_stock);
Expand All @@ -215,7 +229,9 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;

// Send payment to the producer
let seller_address = self.seller_products.read(token_id);
self.claim_balances.write(seller_address, self.claim_balances.read(seller_address) + producer_fee);
self
.claim_balances
.write(seller_address, self.claim_balances.read(seller_address) + producer_fee);
let token_ids = array![token_id].span();
self.emit(PaymentSeller { token_ids, seller: seller_address, payment: producer_fee });
}
Expand All @@ -238,7 +254,8 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
assert(stock > 0, 'Product not available');
let token_amount = *token_amount.at(token_idx);
assert(stock >= token_amount, 'Not enough stock');
producer_fee += self.listed_product_price.read(*token_ids.at(token_idx)) * token_amount;
producer_fee += self.listed_product_price.read(*token_ids.at(token_idx))
* token_amount;

let producer = self.seller_products.read(*token_ids.at(token_idx));
if producers_found.len() == 0 {
Expand All @@ -249,18 +266,22 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
};

// Transfer the funds
let total_price = producer_fee + self.calculate_fee(producer_fee, self.market_fee.read());
let total_price = producer_fee
+ self.calculate_fee(producer_fee, self.market_fee.read());
let strk_token_dispatcher = IERC20Dispatcher {
contract_address: contract_address_const::<STRK_TOKEN_ADDRESS>()
};
assert(strk_token_dispatcher.balance_of(buyer) >= total_price, 'insufficient funds');
strk_token_dispatcher.transfer_from(buyer, contract_address, total_price);

// Transfer the nft products
let cofi_collection = ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()};
cofi_collection.safe_batch_transfer_from(
contract_address, buyer, token_ids, token_amount, array![0].span()
);
let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read()
};
cofi_collection
.safe_batch_transfer_from(
contract_address, buyer, token_ids, token_amount, array![0].span()
);

self.emit(BuyBatchProducts { token_ids, token_amount, total_price });
// Update stock for products
Expand All @@ -282,38 +303,42 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;

// Send payment to the producer
let seller_address = *producers_found.at(0);
self.claim_balances.write(seller_address, self.claim_balances.read(seller_address) + producer_fee);
self
.claim_balances
.write(seller_address, self.claim_balances.read(seller_address) + producer_fee);
self.emit(PaymentSeller { token_ids, seller: seller_address, payment: producer_fee });
}

///
/// Adds a new product to the marketplace
/// Arguments:
/// * `initial_stock` - The amount of stock that the product will have
/// * `price` - The price of the product per unity expresed in fri (1e-18 strk)
/// * `data` - Additional context or metadata for the token transfer process
fn create_product(ref self: ContractState, initial_stock: u256, price: u256, data: Span<felt252>) {
fn create_product(
ref self: ContractState, initial_stock: u256, price: u256, data: Span<felt252>
) {
self.accesscontrol.assert_only_role(PRODUCER);
let token_id = self.current_token_id.read();
let cofi_collection = ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()};
let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read()
};
cofi_collection.mint(get_contract_address(), token_id, initial_stock, data);

let producer = get_caller_address();
self.seller_products.write(token_id, producer);

self.current_token_id.write(token_id + 1);
self.initialize_product(token_id, producer, initial_stock, price);
}

fn create_products(
ref self: ContractState,
initial_stock: Span<u256>,
price: Span<u256>
) {
fn create_products(ref self: ContractState, initial_stock: Span<u256>, price: Span<u256>) {
assert(initial_stock.len() == price.len(), 'wrong len of arrays');
self.accesscontrol.assert_only_role(PRODUCER);
let producer = get_caller_address();
let cofi_collection = ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()};
let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read()
};

// Create token ids and mint the nfts
let mut token_ids = array![];
Expand All @@ -326,7 +351,10 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
token_ids.append(current_token_id + token_idx.into());
token_idx += 1;
};
cofi_collection.batch_mint(get_contract_address(), token_ids.span(), initial_stock, array![0].span());
cofi_collection
.batch_mint(
get_contract_address(), token_ids.span(), initial_stock, array![0].span()
);
self.current_token_id.write(current_token_id + initial_stock.len().into());

// Initialize the products
Expand All @@ -335,19 +363,25 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
if token_idx == initial_stock.len() {
break;
}
self.initialize_product(
*token_ids.at(token_idx), producer, *initial_stock.at(token_idx), *price.at(token_idx)
);
self
.initialize_product(
*token_ids.at(token_idx),
producer,
*initial_stock.at(token_idx),
*price.at(token_idx)
);
token_idx += 1;
};
}

fn delete_product(ref self: ContractState, token_id: u256) {
self.accesscontrol.assert_only_role(PRODUCER);
let producer = get_caller_address();
assert(self.seller_products.read(token_id) == producer, 'Not your product');

let cofi_collection = ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()};
let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read()
};
let token_holder = get_contract_address();
let amount_tokens = cofi_collection.balance_of(token_holder, token_id);
self.update_stock(token_id, 0);
Expand All @@ -372,7 +406,9 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
// Burn nfts
token_idx = 0;
let token_holder = get_contract_address();
let cofi_collection = ICofiCollectionDispatcher {contract_address: self.cofi_collection_address.read()};
let cofi_collection = ICofiCollectionDispatcher {
contract_address: self.cofi_collection_address.read()
};
loop {
if token_idx == token_ids.len() {
break;
Expand All @@ -394,7 +430,8 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
contract_address: contract_address_const::<STRK_TOKEN_ADDRESS>()
};
strk_token_dispatcher.approve(producer, claim_balance);
let transfer = strk_token_dispatcher.transfer_from(get_contract_address(), producer, claim_balance);
let transfer = strk_token_dispatcher
.transfer_from(get_contract_address(), producer, claim_balance);
assert(transfer, 'Error claiming');

self.claim_balances.write(producer, 0);
Expand All @@ -403,7 +440,13 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;

#[generate_trait]
impl InternalImpl of InternalTrait {
fn initialize_product(ref self: ContractState, token_id: u256, producer: ContractAddress, stock: u256, price: u256) {
fn initialize_product(
ref self: ContractState,
token_id: u256,
producer: ContractAddress,
stock: u256,
price: u256
) {
//let product = Product { stock, price };
//self.seller_products.entry(producer).write(token_id);
self.listed_product_stock.write(token_id, stock);
Expand All @@ -416,14 +459,14 @@ use openzeppelin::access::accesscontrol::AccessControlComponent;
self.emit(UpdateStock { token_id, new_stock });
}

// Amount is the total amount
// Amount is the total amount
// BPS is the percentage you want to calculate. (Example: 2.5% = 250bps, 7,48% = 748bps)
// Use example:
// Use example:
// Calculate the 3% fee of 250 STRK
// calculate_fee(250, 300) = 7.5
fn calculate_fee(ref self: ContractState, amount: u256, bps: u256) -> u256 {
assert((amount * bps) >= 10_000, 'Fee too low');
amount * bps / 10_000
}
}
}
}
1 change: 1 addition & 0 deletions apps/snfoundry/contracts/src/test/test_marketplace.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading

0 comments on commit 01ea770

Please sign in to comment.