diff --git a/Cargo.lock b/Cargo.lock index 2ec936c1..e63b5f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,6 +1126,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "soroban-token-contract" +version = "0.0.6" +dependencies = [ + "soroban-sdk", + "soroban-token-sdk", +] + +[[package]] +name = "soroban-token-sdk" +version = "20.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8ed0ae2e5d5e67b7939200bba3712b4c81dcf87b2ccd68bba049bec64c780f" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "soroban-wasmi" version = "0.31.1-soroban.20.0.1" diff --git a/Makefile b/Makefile index 0c1802c8..3db46a6d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SUBDIRS := contracts/collections contracts/deployer +SUBDIRS := contracts/token contracts/collections contracts/deployer contracts/auctions BUILD_FLAGS ?= default: build diff --git a/contracts/auctions/Cargo.toml b/contracts/auctions/Cargo.toml index 8a08a19b..a84e9a95 100644 --- a/contracts/auctions/Cargo.toml +++ b/contracts/auctions/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "phoenix-nft-auctions" version = { workspace = true } -authors = ["Jakub "] +authors = ["Jakub ", "Kaloyan "] repository = { workspace = true } edition = { workspace = true } license = { workspace = true } diff --git a/contracts/auctions/src/contract.rs b/contracts/auctions/src/contract.rs new file mode 100644 index 00000000..03fbce3a --- /dev/null +++ b/contracts/auctions/src/contract.rs @@ -0,0 +1,455 @@ +use soroban_sdk::{contract, contractimpl, log, vec, Address, BytesN, Env, Vec}; + +use crate::{ + collection, + error::ContractError, + storage::{ + generate_auction_id, get_admin, get_auction_by_id, get_auction_token, get_auctions, + get_auctions_by_seller_id, get_highest_bid, is_initialized, save_admin, save_auction_by_id, + save_auction_by_seller, save_auction_token, set_highest_bid, set_initialized, update_admin, + validate_input_params, Auction, AuctionStatus, HighestBid, ItemInfo, + }, + token, +}; + +#[contract] +pub struct MarketplaceContract; + +#[contractimpl] +impl MarketplaceContract { + #[allow(dead_code)] + pub fn initialize( + env: Env, + admin: Address, + auction_token: Address, + ) -> Result<(), ContractError> { + admin.require_auth(); + + if is_initialized(&env) { + log!(&env, "Auction: Initialize: Already initialized"); + return Err(ContractError::AlreadyInitialized); + } + + save_admin(&env, &admin); + save_auction_token(&env, auction_token); + + set_initialized(&env); + + env.events().publish(("initialize", "admin: "), admin); + + Ok(()) + } + + #[allow(dead_code)] + pub fn create_auction( + env: Env, + item_info: ItemInfo, + seller: Address, + duration: u64, + ) -> Result { + seller.require_auth(); + + let input_values = [ + &duration, + &item_info.item_id, + // we want to valid only valid input, in case of `None` we will simply use 1 as + // placeholder + &item_info.buy_now_price.unwrap_or(1), + &item_info.minimum_price.unwrap_or(1), + ]; + validate_input_params(&env, &input_values[..])?; + + let auction_token = get_auction_token(&env)?; + let nft_client = collection::Client::new(&env, &item_info.collection_addr); + let item_balance = nft_client.balance_of(&seller, &item_info.item_id); + + nft_client.set_approval_for_transfer( + &env.current_contract_address(), + &item_info.item_id, + &true, + ); + + // we need at least one item to start an auction + if item_balance < 1 { + log!( + &env, + "Auction: Create Auction: Not enough balance of the item to sell" + ); + return Err(ContractError::NotEnoughBalance); + } + + let id = generate_auction_id(&env)?; + let end_time = env.ledger().timestamp() + duration; + + let auction = Auction { + id, + item_info, + seller: seller.clone(), + highest_bid: None, + end_time, + status: AuctionStatus::Active, + auction_token, + }; + + save_auction(&env, &auction)?; + + env.events() + .publish(("create auction", "auction id: "), auction.id); + env.events().publish(("create auction", "seller: "), seller); + env.events().publish(("initialize", "duration: "), duration); + + Ok(auction) + } + + #[allow(dead_code)] + pub fn place_bid( + env: Env, + auction_id: u64, + bidder: Address, + bid_amount: u64, + ) -> Result<(), ContractError> { + bidder.require_auth(); + + let mut auction = get_auction_by_id(&env, auction_id)?; + + if env.ledger().timestamp() > auction.end_time { + log!(&env, "Auction: Place Bid: Auction not active: ", auction_id); + return Err(ContractError::AuctionNotActive); + } + + if auction.status != AuctionStatus::Active { + log!( + &env, + "Auction: Place Bid: Trying to place a bid for inactive/cancelled auction with id: ", auction_id + ); + return Err(ContractError::AuctionNotActive); + } + + if bidder == auction.seller { + log!(&env, "Auction Place Bid: Seller cannot place bids."); + return Err(ContractError::InvalidBidder); + } + + let token_client = token::Client::new(&env, &auction.auction_token); + + match auction.highest_bid { + Some(current_highest_bid) if bid_amount > current_highest_bid => { + // refund the previous highest bidder + let old_bid_info = get_highest_bid(&env, auction_id)?; + token_client.transfer( + &env.current_contract_address(), + &old_bid_info.bidder, + &(old_bid_info.bid as i128), + ); + } + Some(_) => { + log!( + &env, + "Auction: Place Bid: Bid not enough. Amount bid: ", + bid_amount + ); + return Err(ContractError::BidNotEnough); + } + None => {} + }; + + token_client.transfer( + &bidder, + &env.current_contract_address(), + &(bid_amount as i128), + ); + + set_highest_bid(&env, auction_id, bid_amount, bidder.clone())?; + + auction.highest_bid = Some(bid_amount); + save_auction(&env, &auction)?; + + env.events() + .publish(("place bid", "auction id"), auction_id); + env.events().publish(("place bid", "bidder"), bidder); + env.events().publish(("place bid", "bid"), bid_amount); + + Ok(()) + } + + #[allow(dead_code)] + pub fn finalize_auction(env: Env, auction_id: u64) -> Result<(), ContractError> { + let mut auction = get_auction_by_id(&env, auction_id)?; + + // Check if the auction can be finalized + if auction.status != AuctionStatus::Active { + log!( + env, + "Auction: Finalize auction: Cannot finalize an inactive/ended auction." + ); + return Err(ContractError::AuctionNotActive); + } + if env.ledger().timestamp() < auction.end_time { + log!( + env, + "Auction: Finalize auction: Auction cannot be ended early" + ); + return Err(ContractError::AuctionNotFinished); + } + + let token_client = token::Client::new(&env, &auction.auction_token); + let highest_bid = get_highest_bid(&env, auction_id)?; + + // check if minimum price has been reached + if auction.item_info.minimum_price.map_or(true, |min_price| { + auction + .highest_bid + .map_or(false, |highest_bid| highest_bid >= min_price) + }) { + token_client.transfer( + &env.current_contract_address(), + &auction.seller, + &(highest_bid.bid as i128), + ); + + let nft_client = collection::Client::new(&env, &auction.item_info.collection_addr); + nft_client.safe_transfer_from( + &env.current_contract_address(), + &auction.seller, + &highest_bid.bidder, + &auction.item_info.item_id, + &1, + ); + + auction.status = AuctionStatus::Ended; + save_auction(&env, &auction)?; + env.events() + .publish(("finalize auction", "highest bidder: "), highest_bid.bidder); + env.events() + .publish(("finalize auction", "highest bid: "), highest_bid.bid); + } else if auction.highest_bid.is_none() { + auction.status = AuctionStatus::Ended; + save_auction(&env, &auction)?; + + env.events().publish(("finalize auction", "no bids"), ()); + } else { + token_client.transfer( + &env.current_contract_address(), + &highest_bid.bidder, + &(highest_bid.bid as i128), + ); + auction.status = AuctionStatus::Ended; + save_auction(&env, &auction)?; + log!( + env, + "Auction: Finalize auction: Miniminal price not reached" + ); + + env.events() + .publish(("finalize auction", "auction id: "), auction_id); + env.events() + .publish(("finalize auction", "highest bid: "), auction.highest_bid); + env.events().publish( + ("finalize auction", "minimum price: "), + auction.item_info.minimum_price, + ); + }; + + Ok(()) + } + + #[allow(dead_code)] + pub fn buy_now(env: Env, auction_id: u64, buyer: Address) -> Result<(), ContractError> { + buyer.require_auth(); + + let mut auction = get_auction_by_id(&env, auction_id)?; + + if env.ledger().timestamp() > auction.end_time || auction.status != AuctionStatus::Active { + log!(&env, "Auction: Buy Now: Auction not active: ", auction_id); + return Err(ContractError::AuctionNotActive); + } + + if auction.item_info.buy_now_price.is_none() { + log!( + env, + "Auction: Buy Now: trying to buy an item that does not allow `buy now`" + ); + return Err(ContractError::NoBuyNowOption); + } + + let old_highest_bid = get_highest_bid(&env, auction_id)?; + + let token = token::Client::new(&env, &auction.auction_token); + + // refund only when there is some previous highest bid + if old_highest_bid.bid > 0 { + token.transfer( + &env.current_contract_address(), + &old_highest_bid.bidder, + &(old_highest_bid.bid as i128), + ); + } + + // pay for the item + token.transfer( + &buyer, + &auction.seller, + &(auction + .item_info + .buy_now_price + .expect("Auction: Buy Now: Buy now price has not been set") as i128), + ); + + let collection_client = collection::Client::new(&env, &auction.item_info.collection_addr); + + collection_client.safe_transfer_from( + &env.current_contract_address(), + &auction.seller, + &buyer, + &auction.item_info.item_id, + &1, + ); + + auction.status = AuctionStatus::Ended; + auction.highest_bid = Some( + auction + .item_info + .buy_now_price + .expect("Auction: Buy Now: Buy now price has not been set"), + ); + + save_auction(&env, &auction)?; + + env.events() + .publish(("buy now", "auction id: "), auction_id); + env.events().publish(("buy now", "buyer: "), buyer); + + Ok(()) + } + + #[allow(dead_code)] + pub fn pause(env: Env, auction_id: u64) -> Result<(), ContractError> { + let mut auction = get_auction_by_id(&env, auction_id)?; + auction.seller.require_auth(); + + if auction.status != AuctionStatus::Active { + log!( + &env, + "Auction: Pause: Cannot pause inactive/ended auction: ", + auction_id + ); + return Err(ContractError::AuctionNotActive); + } + + if env.ledger().timestamp() > auction.end_time { + log!(&env, "Auction: Pause: Auction expired: ", auction_id); + return Err(ContractError::AuctionNotActive); + } + + auction.status = AuctionStatus::Paused; + + save_auction(&env, &auction)?; + + env.events().publish(("pause", "auction id: "), auction_id); + + Ok(()) + } + + #[allow(dead_code)] + pub fn unpause(env: &Env, auction_id: u64) -> Result<(), ContractError> { + let mut auction = get_auction_by_id(env, auction_id)?; + auction.seller.require_auth(); + + if auction.status != AuctionStatus::Paused { + log!( + &env, + "Auction: Unpause: Cannot activate unpaused auction: ", + auction_id + ); + return Err(ContractError::AuctionNotPaused); + } + + if env.ledger().timestamp() > auction.end_time { + log!(env, "Auction: Unpause: Auction expired: ", auction_id); + return Err(ContractError::AuctionNotActive); + } + + auction.status = AuctionStatus::Active; + + save_auction(env, &auction)?; + + env.events() + .publish(("unpause", "auction id: "), auction_id); + + Ok(()) + } + + #[allow(dead_code)] + pub fn get_auction(env: Env, auction_id: u64) -> Result { + let auction = get_auction_by_id(&env, auction_id)?; + + Ok(auction) + } + + #[allow(dead_code)] + pub fn get_active_auctions( + env: Env, + start_index: Option, + limit: Option, + ) -> Result, ContractError> { + let all_auctions = get_auctions(&env, start_index, limit)?; + + let mut filtered_auctions = vec![&env]; + + for auction in all_auctions.iter() { + if auction.status == AuctionStatus::Active { + filtered_auctions.push_back(auction); + } + } + + Ok(filtered_auctions) + } + + #[allow(dead_code)] + pub fn get_auctions_by_seller( + env: Env, + seller: Address, + ) -> Result, ContractError> { + let seller_auction_list = get_auctions_by_seller_id(&env, &seller)?; + + Ok(seller_auction_list) + } + + #[allow(dead_code)] + pub fn get_highest_bid(env: Env, auction_id: u64) -> Result { + let highest_bid_info = get_highest_bid(&env, auction_id)?; + + Ok(highest_bid_info) + } + + #[allow(dead_code)] + pub fn update_admin(env: Env, new_admin: Address) -> Result { + let old_admin = get_admin(&env)?; + old_admin.require_auth(); + + env.events() + .publish(("update admin", "old admin: "), old_admin); + env.events() + .publish(("update admin", "new admin: "), &new_admin); + + Ok(update_admin(&env, &new_admin))? + } + + #[allow(dead_code)] + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), ContractError> { + let admin: Address = get_admin(&env)?; + admin.require_auth(); + + env.deployer().update_current_contract_wasm(new_wasm_hash); + + env.events().publish(("upgrade", "admin: "), admin); + + Ok(()) + } +} + +fn save_auction(env: &Env, auction: &Auction) -> Result<(), ContractError> { + save_auction_by_id(env, auction.id, auction)?; + save_auction_by_seller(env, &auction.seller, auction)?; + Ok(()) +} diff --git a/contracts/auctions/src/error.rs b/contracts/auctions/src/error.rs new file mode 100644 index 00000000..53ec611a --- /dev/null +++ b/contracts/auctions/src/error.rs @@ -0,0 +1,25 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum ContractError { + Unauthorized = 0, + AuctionNotFound = 1, + IDMissmatch = 2, + BidNotEnough = 3, + AuctionNotFinished = 4, + NotEnoughBalance = 5, + InvalidInputs = 6, + AuctionNotActive = 7, + MinPriceNotReached = 8, + MissingHighestBid = 9, + AuctionNotPaused = 10, + PaymentProcessingFailed = 11, + NoBuyNowOption = 12, + AlreadyInitialized = 13, + InvalidBidder = 14, + AdminNotFound = 15, + NoBidFound = 16, + AuctionTokenNotFound = 17, +} diff --git a/contracts/auctions/src/lib.rs b/contracts/auctions/src/lib.rs index 1bb3d393..7a60df06 100644 --- a/contracts/auctions/src/lib.rs +++ b/contracts/auctions/src/lib.rs @@ -1,84 +1,21 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Vec}; - -#[derive(Clone)] -#[contracttype] -pub struct Auction { - pub id: u64, - pub item_address: Address, // Can be an NFT contract address or a collection contract address - pub seller: Address, - pub highest_bid: u64, - pub highest_bidder: Address, - pub buy_now_price: u64, - pub end_time: u64, - pub status: AuctionStatus, -} - -#[derive(Clone, PartialEq)] -#[contracttype] -pub enum AuctionStatus { - Active, - Ended, - Cancelled, +mod contract; +mod error; +mod storage; + +#[cfg(test)] +mod test; + +pub mod collection { + type NftId = u64; + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_nft_collections.wasm" + ); } -#[contract] -pub struct MarketplaceContract; - -#[contractimpl] -impl MarketplaceContract { - fn generate_auction_id(env: &Env) -> u64 { - todo!() - } - - pub fn create_auction( - env: Env, - item_address: Address, - seller: Address, - buy_now_price: u64, - duration: u64, - ) -> Auction { - todo!() - } - - pub fn place_bid(env: Env, auction_id: u64, bidder: Address, bid_amount: u64) { - todo!() - } - - pub fn finalize_auction(env: Env, auction_id: u64) { - todo!() - } - - pub fn buy_now(env: Env, auction_id: u64, buyer: Address) { - todo!() - } - - fn distribute_funds(env: Env, auction: Auction) { - todo!() - } - - pub fn pause(env: Env) { - todo!() - } - - pub fn unpause(env: Env) { - todo!() - } - - pub fn get_auction(env: Env, auction_id: u64) -> Auction { - todo!() - } - - pub fn get_active_auctions(env: Env) -> Vec { - todo!() - } - - pub fn get_auctions_by_seller(env: Env, seller: Address) -> Vec { - todo!() - } - - pub fn get_highest_bid(env: Env, auction_id: u64) -> (u64, Address) { - todo!() - } +pub mod token { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" + ); } diff --git a/contracts/auctions/src/storage.rs b/contracts/auctions/src/storage.rs new file mode 100644 index 00000000..e6335108 --- /dev/null +++ b/contracts/auctions/src/storage.rs @@ -0,0 +1,321 @@ +use soroban_sdk::{contracttype, log, panic_with_error, vec, Address, Env, Vec}; + +use crate::error::ContractError; + +// Values used to extend the TTL of storage +pub const DAY_IN_LEDGERS: u32 = 17280; +pub const BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; +pub const LIFETIME_THRESHOLD: u32 = BUMP_AMOUNT - DAY_IN_LEDGERS; + +// consts for Pagination +// since we start counting from 1, default would be 1 as well +pub const DEFAULT_INDEX: u64 = 1; +pub const DEFAULT_LIMIT: u64 = 10; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + IsInitialized, + AuctionId, + AllAuctions, + HighestBid(u64), + AuctionToken, +} + +#[derive(Clone, Debug, PartialEq)] +#[contracttype] +pub struct ItemInfo { + pub collection_addr: Address, + pub item_id: u64, + pub minimum_price: Option, + pub buy_now_price: Option, +} + +#[derive(Clone, Debug, PartialEq)] +#[contracttype] +pub struct Auction { + pub id: u64, + pub item_info: ItemInfo, + pub seller: Address, + pub highest_bid: Option, + pub end_time: u64, + pub status: AuctionStatus, + pub auction_token: Address, +} + +#[derive(Clone, Debug, PartialEq)] +#[contracttype] +pub struct HighestBid { + pub bid: u64, + pub bidder: Address, +} + +#[derive(Clone, PartialEq, Debug)] +#[contracttype] +pub enum AuctionStatus { + Active, + Ended, + Cancelled, + Paused, +} + +pub fn generate_auction_id(env: &Env) -> Result { + let id = env + .storage() + .instance() + .get::<_, u64>(&DataKey::AuctionId) + .unwrap_or_default() + + 1u64; + env.storage().instance().set(&DataKey::AuctionId, &id); + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT); + + Ok(id) +} + +pub fn get_auctions( + env: &Env, + start_index: Option, + limit: Option, +) -> Result, ContractError> { + let start_index = start_index.unwrap_or(DEFAULT_INDEX); + + // this is a safeguard only for the case when `DEFAULT_LIMIT` is higher than the actually + // saved auctions and we use `None` and `None` for `start_index` and `limit`. + // I.e. we have just 3 auctions and we want to query them + let current_highest_id: u64 = env + .storage() + .instance() + .get(&DataKey::AuctionId) + .expect("no previous value"); + + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(current_highest_id); + + let mut auctions = vec![&env]; + + for id in start_index..=limit { + match get_auction_by_id(env, id) { + Ok(auction) => auctions.push_back(auction), + Err(ContractError::AuctionNotFound) => continue, + Err(e) => return Err(e), + } + } + + Ok(auctions) +} + +pub fn save_auction_by_id( + env: &Env, + auction_id: u64, + auction: &Auction, +) -> Result<(), ContractError> { + env.storage().instance().set(&auction_id, auction); + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT); + + Ok(()) +} + +pub fn save_auction_by_seller( + env: &Env, + seller: &Address, + auction: &Auction, +) -> Result<(), ContractError> { + let mut seller_auctions_list: Vec = + env.storage().instance().get(seller).unwrap_or(vec![&env]); + + match seller_auctions_list.iter().position(|a| a.id == auction.id) { + Some(existing_idx) => seller_auctions_list.set(existing_idx as u32, auction.clone()), + None => seller_auctions_list.push_back(auction.clone()), + }; + + env.storage().instance().set(seller, &seller_auctions_list); + + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT); + + Ok(()) +} + +pub fn get_auction_by_id(env: &Env, auction_id: u64) -> Result { + let auction = env + .storage() + .instance() + .get(&auction_id) + .unwrap_or_else(|| { + log!(env, "Auction: Get auction by id: Auction not present"); + panic_with_error!(&env, ContractError::AuctionNotFound); + }); + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT); + + auction +} + +pub fn get_auctions_by_seller_id( + env: &Env, + seller: &Address, +) -> Result, ContractError> { + let seller_auctions_list = env.storage().instance().get(seller).unwrap_or_else(|| { + log!(env, "Auction: Get auction by seller: No auctions found"); + panic_with_error!(&env, ContractError::AuctionNotFound); + }); + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT); + + Ok(seller_auctions_list) +} + +pub fn validate_input_params(env: &Env, values_to_check: &[&u64]) -> Result<(), ContractError> { + values_to_check.iter().for_each(|i| { + if i < &&1 { + log!( + &env, + "Auction: Validate input: parameters cannot be less than 1" + ); + panic_with_error!(&env, ContractError::InvalidInputs); + } + }); + + Ok(()) +} +pub fn is_initialized(env: &Env) -> bool { + env.storage() + .persistent() + .get(&DataKey::IsInitialized) + .unwrap_or(false) +} + +pub fn set_initialized(env: &Env) { + env.storage() + .persistent() + .set(&DataKey::IsInitialized, &true); +} + +pub fn save_admin(env: &Env, admin: &Address) { + env.storage().persistent().set(&DataKey::Admin, &admin); + env.storage() + .persistent() + .extend_ttl(&DataKey::Admin, LIFETIME_THRESHOLD, BUMP_AMOUNT); +} + +pub fn get_admin(env: &Env) -> Result { + let admin = env + .storage() + .persistent() + .get(&DataKey::Admin) + .unwrap_or_else(|| { + log!(env, "Auction: Get Admin: Admin not found"); + Err(ContractError::AdminNotFound) + })?; + env.storage().persistent().has(&DataKey::Admin).then(|| { + env.storage() + .persistent() + .extend_ttl(&DataKey::Admin, LIFETIME_THRESHOLD, BUMP_AMOUNT); + }); + + Ok(admin) +} + +pub fn update_admin(env: &Env, new_admin: &Address) -> Result { + env.storage().persistent().set(&DataKey::Admin, new_admin); + + Ok(new_admin.clone()) +} + +pub fn get_highest_bid(env: &Env, auction_id: u64) -> Result { + let highest_bid = env + .storage() + .instance() + .get(&DataKey::HighestBid(auction_id)) + .unwrap_or(HighestBid { + bid: 0, + // I know + bidder: get_admin(env)?, + }); + + env.storage() + .instance() + .has(&DataKey::HighestBid(auction_id)) + .then(|| { + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT) + }); + + Ok(highest_bid) +} + +pub fn set_highest_bid( + env: &Env, + auction_id: u64, + bid: u64, + bidder: Address, +) -> Result<(), ContractError> { + env.storage().instance().set( + &DataKey::HighestBid(auction_id), + &HighestBid { bid, bidder }, + ); + env.storage() + .instance() + .extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT); + + Ok(()) +} + +pub fn save_auction_token(env: &Env, auction_token: Address) { + env.storage() + .persistent() + .set(&DataKey::AuctionToken, &auction_token); + + env.storage() + .persistent() + .extend_ttl(&DataKey::AuctionToken, LIFETIME_THRESHOLD, BUMP_AMOUNT); +} + +pub fn get_auction_token(env: &Env) -> Result { + let auction_token = env + .storage() + .persistent() + .get(&DataKey::AuctionToken) + .ok_or(ContractError::AuctionTokenNotFound)?; + + env.storage() + .persistent() + .has(&DataKey::AuctionToken) + .then(|| { + env.storage().persistent().extend_ttl( + &DataKey::AuctionToken, + LIFETIME_THRESHOLD, + BUMP_AMOUNT, + ); + }); + + Ok(auction_token) +} + +#[cfg(test)] +mod test { + use soroban_sdk::Env; + + use super::validate_input_params; + + #[test] + #[should_panic(expected = "Auction: Validate input: parameters cannot be less than 1")] + fn validate_input_params_should_fail_with_invalid_input() { + let env = Env::default(); + let _ = validate_input_params(&env, &[&1, &2, &3, &0]); + } + + #[test] + fn validate_input_params_should_work() { + let env = Env::default(); + assert!(validate_input_params(&env, &[&1, &2, &3]).is_ok()); + } +} diff --git a/contracts/auctions/src/test.rs b/contracts/auctions/src/test.rs new file mode 100644 index 00000000..47ef5ba8 --- /dev/null +++ b/contracts/auctions/src/test.rs @@ -0,0 +1,4 @@ +mod bids; +mod finalize_auction; +mod initialization; +mod setup; diff --git a/contracts/auctions/src/test/bids.rs b/contracts/auctions/src/test/bids.rs new file mode 100644 index 00000000..0cca3ba5 --- /dev/null +++ b/contracts/auctions/src/test/bids.rs @@ -0,0 +1,984 @@ +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + vec, Address, Env, +}; + +use crate::{ + contract::{MarketplaceContract, MarketplaceContractClient}, + error::ContractError, + storage::{Auction, AuctionStatus, HighestBid, ItemInfo}, + test::setup::{ + deploy_token_contract, generate_marketplace_and_collection_client, DAY, FOUR_HOURS, WEEKLY, + }, + token, +}; + +use super::setup::create_and_initialize_collection; + +#[test] +fn should_place_a_bid() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + let bidder_a = Address::generate(&env); + let bidder_b = Address::generate(&env); + let bidder_c = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + token_client.mint(&bidder_a, &10i128); + token_client.mint(&bidder_b, &20i128); + token_client.mint(&bidder_c, &40i128); + + let item_info = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + mp_client.place_bid(&1, &bidder_a, &10); + assert_eq!( + mp_client.get_highest_bid(&1), + HighestBid { + bid: 10u64, + bidder: bidder_a.clone() + } + ); + assert_eq!(token_client.balance(&mp_client.address), 10i128); + assert_eq!(token_client.balance(&bidder_a), 0i128); + + mp_client.place_bid(&1, &bidder_b, &20); + assert_eq!( + mp_client.get_highest_bid(&1), + HighestBid { + bid: 20u64, + bidder: bidder_b.clone() + } + ); + assert_eq!(token_client.balance(&mp_client.address), 20i128); + assert_eq!(token_client.balance(&bidder_a), 10i128); + assert_eq!(token_client.balance(&bidder_b), 0i128); + + //bidder_a tries to place a bid, that's lower than the bid of bidder_b + assert_eq!( + mp_client.try_place_bid(&1, &bidder_a, &15), + Err(Ok(ContractError::BidNotEnough)) + ); + assert_eq!(token_client.balance(&mp_client.address), 20i128); + assert_eq!(token_client.balance(&bidder_a), 10i128); + assert_eq!(token_client.balance(&bidder_b), 0i128); + + assert_eq!( + mp_client.get_highest_bid(&1), + HighestBid { + bid: 20u64, + bidder: bidder_b.clone() + } + ); + + mp_client.place_bid(&1, &bidder_c, &40); + assert_eq!( + mp_client.get_highest_bid(&1), + HighestBid { + bid: 40u64, + bidder: bidder_c.clone() + } + ); + assert_eq!(token_client.balance(&mp_client.address), 40i128); + assert_eq!(token_client.balance(&bidder_a), 10i128); + assert_eq!(token_client.balance(&bidder_b), 20i128); + assert_eq!(token_client.balance(&bidder_c), 0i128); +} + +#[test] +fn fail_to_place_bid_when_auction_inactive() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + let bidder_a = Address::generate(&env); + + let token_client = token::Client::new(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + nft_collection_client.set_approval_for_all(&mp_client.address, &true); + + let item_info = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = WEEKLY + DAY); + + mp_client.finalize_auction(&1); + + assert_eq!( + mp_client.try_place_bid(&1, &bidder_a, &10), + Err(Ok(ContractError::AuctionNotActive)) + ); +} + +#[test] +fn seller_tries_to_place_a_bid_should_fail() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let seller = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + token_client.mint(&seller, &1); + let (mp_client, collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collection_client.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collection_client.address, + item_id: 1, + minimum_price: None, + buy_now_price: None, + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = DAY); + + assert_eq!( + mp_client.try_place_bid(&1, &seller, &1), + Err(Ok(ContractError::InvalidBidder)) + ); +} + +#[test] +fn buy_now_should_fail_when_auction_not_active() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let fomo_buyer = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&fomo_buyer, &50); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: None, + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &DAY); + + env.ledger().with_mut(|li| li.timestamp = WEEKLY); + + assert_eq!( + mp_client.try_buy_now(&1, &fomo_buyer), + Err(Ok(ContractError::AuctionNotActive)) + ); +} + +#[test] +fn buy_now_should_fail_when_no_buy_now_price_has_been_set() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let fomo_buyer = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&fomo_buyer, &50); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collections_client.address, + item_id: 1, + minimum_price: None, + buy_now_price: None, + }; + + mp_client.create_auction(&item_info, &seller, &DAY); + + assert_eq!( + mp_client.try_buy_now(&1, &fomo_buyer), + Err(Ok(ContractError::NoBuyNowOption)) + ); +} + +#[test] +fn buy_now() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let bidder_a = Address::generate(&env); + let bidder_b = Address::generate(&env); + let fomo_buyer = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&fomo_buyer, &100); + token_client.mint(&bidder_a, &100); + token_client.mint(&bidder_b, &100); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + collections_client.set_approval_for_transfer(&mp_client.address, &1u64, &true); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + // 4 hours in and we have a first highest bid + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS); + mp_client.place_bid(&1, &bidder_a, &5); + assert_eq!(token_client.balance(&bidder_a), 95); + assert_eq!(token_client.balance(&mp_client.address), 5); + + // 8 hours in and we have a second highest bid + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS * 2); + mp_client.place_bid(&1, &bidder_b, &10); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 90); + assert_eq!(token_client.balance(&mp_client.address), 10); + + // 16 hours in and we have a third highest bid + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS * 4); + mp_client.place_bid(&1, &fomo_buyer, &25); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&fomo_buyer), 75); + assert_eq!(token_client.balance(&mp_client.address), 25); + + // 24 hours in and we have a 4th highest bid + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS * 6); + mp_client.place_bid(&1, &bidder_b, &30); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 70); + assert_eq!(token_client.balance(&fomo_buyer), 100); + assert_eq!(token_client.balance(&mp_client.address), 30); + + // 36 hours in and we have a 5th highest bid, which is over the buy now price + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS * 9); + mp_client.place_bid(&1, &bidder_a, &60); + assert_eq!(token_client.balance(&bidder_a), 40); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&fomo_buyer), 100); + assert_eq!(token_client.balance(&mp_client.address), 60); + + // 40 hours in and the fomo buyer sees the previous user mistake and buys now + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS * 10); + mp_client.buy_now(&1, &fomo_buyer); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&fomo_buyer), 50); + assert_eq!(token_client.balance(&mp_client.address), 0); + assert_eq!(token_client.balance(&seller), 50); + + assert_eq!( + mp_client.get_auction(&1), + Auction { + id: 1, + item_info, + seller, + highest_bid: Some(50), + end_time: WEEKLY, + status: AuctionStatus::Ended, + auction_token: token_client.address + } + ); + + assert_eq!(collections_client.balance_of(&fomo_buyer, &1), 1); +} + +#[test] +fn pause_changes_status_and_second_attempt_fails_to_pause() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: None, + buy_now_price: None, + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = DAY); + + mp_client.pause(&1); + + assert_eq!(mp_client.get_auction(&1).status, AuctionStatus::Paused); + + assert_eq!( + mp_client.try_pause(&1), + Err(Ok(ContractError::AuctionNotActive)) + ); + + // 4 weeks after the creation, after the auction has already expired the seller tries to + // pause it + env.ledger().with_mut(|li| li.timestamp = WEEKLY * 4); + + assert_eq!( + mp_client.try_pause(&1), + Err(Ok(ContractError::AuctionNotActive)) + ); +} + +#[test] +fn pause_after_enddate_should_fail() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: None, + buy_now_price: None, + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = WEEKLY + DAY); + + assert_eq!( + mp_client.try_pause(&1), + Err(Ok(ContractError::AuctionNotActive)) + ); +} + +#[test] +fn unpause_changes_status_and_second_attempt_fails_to_unpause() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let bidder = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + token_client.mint(&bidder, &100); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: None, + buy_now_price: Some(10), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = DAY); + + assert_eq!( + mp_client.try_unpause(&1), + Err(Ok(ContractError::AuctionNotPaused)) + ); + + mp_client.pause(&1); + assert_eq!(mp_client.get_auction(&1).status, AuctionStatus::Paused); + + assert_eq!( + mp_client.try_place_bid(&1, &bidder, &100), + Err(Ok(ContractError::AuctionNotActive)) + ); + + assert_eq!( + mp_client.try_buy_now(&1, &bidder), + Err(Ok(ContractError::AuctionNotActive)) + ); + + assert_eq!( + mp_client.try_finalize_auction(&1), + Err(Ok(ContractError::AuctionNotActive)) + ); + + mp_client.unpause(&1); + assert_eq!(mp_client.get_auction(&1).status, AuctionStatus::Active); + + mp_client.place_bid(&1, &bidder, &100); + + assert_eq!(token_client.balance(&bidder), 0); + assert_eq!(token_client.balance(&mp_client.address), 100); + assert_eq!( + mp_client.get_highest_bid(&1), + HighestBid { bid: 100, bidder } + ); + + mp_client.pause(&1); + // 4 weeks after the creation, after the auction has already expired the seller tries to + // unpause it + env.ledger().with_mut(|li| li.timestamp = WEEKLY * 4); + + assert_eq!( + mp_client.try_unpause(&1), + Err(Ok(ContractError::AuctionNotActive)) + ); +} + +#[test] +fn multiple_auction_by_multiple_sellers() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + + let seller_a = Address::generate(&env); + let seller_b = Address::generate(&env); + let seller_c = Address::generate(&env); + + let bidder_a = Address::generate(&env); + let bidder_b = Address::generate(&env); + let bidder_c = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&bidder_a, &1_000); + token_client.mint(&bidder_b, &1_000); + token_client.mint(&bidder_c, &1_000); + + let mp_client = + MarketplaceContractClient::new(&env, &env.register_contract(None, MarketplaceContract {})); + + mp_client.initialize(&admin, &token_client.address); + + // ============ Collections client setup ============ + let collection_a_client = + create_and_initialize_collection(&env, &seller_a, "Seller A Collection", "SAC"); + let collection_b_client = + create_and_initialize_collection(&env, &seller_b, "Seller B Collection", "SBC"); + let collection_c_client = + create_and_initialize_collection(&env, &seller_c, "Seller C Collection", "SCC"); + + // ============ Auction item setup ============ + let first_item_info_seller_a = ItemInfo { + collection_addr: collection_a_client.address.clone(), + item_id: 1, + minimum_price: Some(100), + buy_now_price: Some(500), + }; + + collection_a_client.mint(&seller_a, &seller_a, &2, &1); + + mp_client.create_auction(&first_item_info_seller_a, &seller_a, &WEEKLY); + + let second_item_info_seller_a = ItemInfo { + collection_addr: collection_a_client.address.clone(), + item_id: 2, + minimum_price: Some(500), + buy_now_price: Some(900), + }; + + mp_client.create_auction(&second_item_info_seller_a, &seller_a, &WEEKLY); + + let item_info_seller_b = ItemInfo { + collection_addr: collection_b_client.address.clone(), + item_id: 1, + minimum_price: Some(50), + buy_now_price: None, + }; + + mp_client.create_auction(&item_info_seller_b, &seller_b, &WEEKLY); + + let item_info_seller_c = ItemInfo { + collection_addr: collection_c_client.address.clone(), + item_id: 1, + minimum_price: None, + buy_now_price: None, + }; + + mp_client.create_auction(&item_info_seller_c, &seller_c, &DAY); + // ============ Authorized transfer ============================ + collection_a_client.set_approval_for_transfer(&mp_client.address, &1, &true); + collection_a_client.set_approval_for_transfer(&mp_client.address, &2, &true); + collection_b_client.set_approval_for_transfer(&mp_client.address, &1, &true); + collection_c_client.set_approval_for_transfer(&mp_client.address, &1, &true); + + // ============ Assert everything is before bidding ============ + + assert_eq!( + mp_client.get_auctions_by_seller(&seller_a), + vec![ + &env, + Auction { + id: 1, + item_info: first_item_info_seller_a.clone(), + seller: seller_a.clone(), + highest_bid: None, + end_time: WEEKLY, + status: AuctionStatus::Active, + auction_token: token_client.address.clone() + }, + Auction { + id: 2, + item_info: second_item_info_seller_a.clone(), + seller: seller_a.clone(), + highest_bid: None, + end_time: WEEKLY, + status: AuctionStatus::Active, + auction_token: token_client.address.clone() + } + ] + ); + + assert_eq!( + mp_client.get_auctions_by_seller(&seller_b), + vec![ + &env, + Auction { + id: 3, + item_info: item_info_seller_b.clone(), + seller: seller_b.clone(), + highest_bid: None, + end_time: WEEKLY, + status: AuctionStatus::Active, + auction_token: token_client.address.clone() + }, + ] + ); + + assert_eq!( + mp_client.get_auctions_by_seller(&seller_c), + vec![ + &env, + Auction { + id: 4, + item_info: item_info_seller_c.clone(), + seller: seller_c.clone(), + highest_bid: None, + end_time: DAY, + status: AuctionStatus::Active, + auction_token: token_client.address.clone() + }, + ] + ); + // ============ Start bidding ============ + + // within day #1 + env.ledger().with_mut(|li| li.timestamp = DAY / 4); + + mp_client.place_bid(&1, &bidder_a, &50); + mp_client.place_bid(&2, &bidder_a, &50); + // `bidder_b` places a bid, but is immediately outbidden by `bidder_c` + mp_client.place_bid(&3, &bidder_b, &25); + mp_client.place_bid(&3, &bidder_c, &26); + mp_client.place_bid(&4, &bidder_b, &100); + + assert_eq!(token_client.balance(&bidder_a), 900); + assert_eq!(token_client.balance(&bidder_b), 900); + assert_eq!(token_client.balance(&bidder_c), 974); + assert_eq!(token_client.balance(&mp_client.address), 226); + + // within day #2 + // here auction #4 has ended + env.ledger().with_mut(|li| li.timestamp = DAY * 2); + + assert_eq!( + mp_client.try_place_bid(&4, &bidder_a, &200), + Err(Ok(ContractError::AuctionNotActive)) + ); + + mp_client.finalize_auction(&4); + assert_eq!(token_client.balance(&mp_client.address), 126); + assert_eq!(token_client.balance(&bidder_a), 900); + assert_eq!(token_client.balance(&bidder_b), 900); + assert_eq!(token_client.balance(&bidder_c), 974); + assert_eq!(token_client.balance(&seller_c), 100); + assert_eq!(collection_c_client.balance_of(&bidder_b, &1), 1); + + // day #3 + env.ledger().with_mut(|li| li.timestamp = DAY * 3); + + mp_client.place_bid(&1, &bidder_b, &100); + mp_client.place_bid(&2, &bidder_c, &75); + mp_client.place_bid(&3, &bidder_a, &50); + + // `bidder_a` has been outbid in both #1 and #2, so he gets his 100 in total back; then he + // places a 50 bid on #3 leaving his balance with 950 + assert_eq!(token_client.balance(&bidder_a), 950); + // `bidder_b` has won auctoin #4 with a 100 bid; now he placed another bid for a 100 in auction + // #1 + assert_eq!(token_client.balance(&bidder_b), 800); + // `bidder_c` had a bid of 26 for auction #3, but he has been outbid by `bidder_a` and after + // `bidder_c`places a bit for 75 he now has 925 in total + assert_eq!(token_client.balance(&bidder_c), 925); + // total of the assets locked in the contract + assert_eq!(token_client.balance(&mp_client.address), 225); + + // day #4 + env.ledger().with_mut(|li| li.timestamp = DAY * 4); + // `bidder_b` fomos and buys the item in auction #1. Right after that `bidder_a`tries to bid on + // that item but fails to do as the auction has ended. + mp_client.buy_now(&1, &bidder_b); + assert_eq!( + mp_client.try_place_bid(&1, &bidder_a, &150), + Err(Ok(ContractError::AuctionNotActive)) + ); + + // verify the ownership + assert_eq!(collection_a_client.balance_of(&bidder_b, &1), 1); + + // we have 2 auctions remaining: #2 and #3 + // the last highest bid on auction #3 is from `bidder_a` so when `bidder_c` places a bet the + // previous bid of 50 is returned back to `bidder_a` making his total balance to 900 + mp_client.place_bid(&2, &bidder_a, &100); + mp_client.place_bid(&3, &bidder_c, &100); + + // day #5 + env.ledger().with_mut(|li| li.timestamp = DAY * 5); + + // the bid of `bidder_b` for 150 returns the previous bid of `bidder_a`, thus `bidder_a` has a + // total of 1000 again + mp_client.place_bid(&2, &bidder_b, &150); + mp_client.place_bid(&3, &bidder_a, &150); + + // day #6 + // let's count the balances again + + assert_eq!(token_client.balance(&bidder_a), 850); + // `bidder_b` has the lowest balance, due to `buy_now` + assert_eq!(token_client.balance(&bidder_b), 250); + // `bidder_c` has been outbid by `bidder_a` the previous day, thus having his full portfolio of + // 1_000 since he was outbid in the last day and got the last 100 refunded + assert_eq!(token_client.balance(&bidder_c), 1_000); + + // day #15 + // okay, enough bidding, let's close the auctions + env.ledger().with_mut(|li| li.timestamp = WEEKLY + DAY); + + mp_client.finalize_auction(&2); + mp_client.finalize_auction(&3); + + // let's do some assertions + + // assertions of the state of the auctions + assert_eq!( + mp_client.get_auctions_by_seller(&seller_a), + vec![ + &env, + Auction { + id: 1, + item_info: first_item_info_seller_a.clone(), + seller: seller_a.clone(), + highest_bid: Some(500), + end_time: WEEKLY, + status: AuctionStatus::Ended, + auction_token: token_client.address.clone() + }, + Auction { + id: 2, + item_info: second_item_info_seller_a, + seller: seller_a.clone(), + highest_bid: Some(150), + end_time: WEEKLY, + status: AuctionStatus::Ended, + auction_token: token_client.address.clone() + } + ] + ); + + assert_eq!( + mp_client.get_auctions_by_seller(&seller_b), + vec![ + &env, + Auction { + id: 3, + item_info: item_info_seller_b, + seller: seller_b.clone(), + highest_bid: Some(150), + end_time: WEEKLY, + status: AuctionStatus::Ended, + auction_token: token_client.address.clone() + }, + ] + ); + + assert_eq!( + mp_client.get_auctions_by_seller(&seller_c), + vec![ + &env, + Auction { + id: 4, + item_info: item_info_seller_c, + seller: seller_c.clone(), + highest_bid: Some(100), + end_time: DAY, + status: AuctionStatus::Ended, + auction_token: token_client.address.clone() + }, + ] + ); + + // assertions of the token balances + // because `bidder_b` used `buy_now` for auction #1 and no one was able to put at least 500 as + // bid to meet the minimum price + assert_eq!(token_client.balance(&seller_a), 500); + // `bidder_a` placed a bit of 150 for this auction + assert_eq!(token_client.balance(&seller_b), 150); + // `bidder_b` bought it with a bid of a 100 + assert_eq!(token_client.balance(&seller_c), 100); + + // bought the item from `seller_b` + assert_eq!(token_client.balance(&bidder_a), 850); + // bought with `buy_now` for a 500 tokens and another big and since he did not met the minimum + // amount to buy the item and got a refund of 100 + assert_eq!(token_client.balance(&bidder_b), 400); + // `bidder_c` has all the tokens he started with + assert_eq!(token_client.balance(&bidder_c), 1_000); + + // make sure that we don't hold any tokens, as we are just intermediary + assert_eq!(token_client.balance(&mp_client.address), 0); + + // let's check the item info + // auction #1 sold item #1 from `collection_a` and the winner is `bidder_a` + assert_eq!(collection_a_client.balance_of(&bidder_b, &1), 1); + // auction #1 DID NOT SELL item #2 from collection_a; item remains with `seller_a` + assert_eq!(collection_a_client.balance_of(&seller_a, &2), 1); + // auction #3 sold item #1 from `collection_b` and the winner is `bidder_a` + assert_eq!(collection_b_client.balance_of(&bidder_a, &1), 1); + // auction #4 sold item #1 from `collection_c` and the winnder is `bidder_b` + assert_eq!(collection_c_client.balance_of(&bidder_b, &1), 1); +} + +#[test] +fn buy_now_should_fail_when_status_is_different_from_active() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let bidder = Address::generate(&env); + + let token = deploy_token_contract(&env, &admin); + token.mint(&bidder, &10); + + let (mp_client, collection) = + generate_marketplace_and_collection_client(&env, &seller, &token.address, None, None); + + collection.mint(&seller, &seller, &1, &1); + + let item_info = ItemInfo { + collection_addr: collection.address, + item_id: 1, + minimum_price: None, + buy_now_price: Some(10), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = DAY); + + mp_client.pause(&1); + + assert_eq!( + mp_client.try_buy_now(&1, &bidder), + Err(Ok(ContractError::AuctionNotActive)) + ); + assert_eq!(token.balance(&bidder), 10); +} + +#[test] +fn buy_now_should_work_when_no_previous_bid() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let fomo_buyer = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&fomo_buyer, &100); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + collections_client.set_approval_for_transfer(&mp_client.address, &1u64, &true); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS); + + mp_client.buy_now(&1, &fomo_buyer); + + assert_eq!(token_client.balance(&fomo_buyer), 50); + assert_eq!(token_client.balance(&mp_client.address), 0); + assert_eq!(token_client.balance(&seller), 50); +} + +#[test] +fn buy_now_should_refund_previous_buyer() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + let bidder = Address::generate(&env); + let fomo_buyer = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&fomo_buyer, &100); + token_client.mint(&bidder, &100); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + collections_client.mint(&seller, &seller, &1, &1); + + collections_client.set_approval_for_transfer(&mp_client.address, &1u64, &true); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS); + + mp_client.place_bid(&1, &bidder, &40); + assert_eq!(token_client.balance(&bidder), 60); + assert_eq!(token_client.balance(&mp_client.address), 40); + + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS * 2); + + mp_client.buy_now(&1, &fomo_buyer); + + assert_eq!(token_client.balance(&fomo_buyer), 50); + assert_eq!(token_client.balance(&bidder), 100); + assert_eq!(token_client.balance(&mp_client.address), 0); + assert_eq!(token_client.balance(&seller), 50); +} diff --git a/contracts/auctions/src/test/finalize_auction.rs b/contracts/auctions/src/test/finalize_auction.rs new file mode 100644 index 00000000..f1aa88e7 --- /dev/null +++ b/contracts/auctions/src/test/finalize_auction.rs @@ -0,0 +1,294 @@ +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + vec, Address, Env, +}; + +use crate::{ + error::ContractError, + storage::{Auction, AuctionStatus, ItemInfo}, + test::setup::{ + deploy_token_contract, generate_marketplace_and_collection_client, DAY, FOUR_HOURS, WEEKLY, + }, + token, +}; + +#[test] +fn finalize_auction() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + + let bidder_a = Address::generate(&env); + let bidder_b = Address::generate(&env); + let bidder_c = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + + token_client.mint(&bidder_a, &100); + token_client.mint(&bidder_b, &100); + token_client.mint(&bidder_c, &100); + + let (mp_client, collections_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + let item_info = ItemInfo { + collection_addr: collections_client.address.clone(), + item_id: 1, + minimum_price: None, + buy_now_price: None, + }; + + collections_client.set_approval_for_transfer(&mp_client.address, &1, &true); + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + // 4 hours after the start of the auctions `bidder_a` places a bid + env.ledger().with_mut(|li| li.timestamp = FOUR_HOURS); + mp_client.place_bid(&1, &bidder_a, &5); + assert_eq!(token_client.balance(&bidder_a), 95); + assert_eq!(token_client.balance(&mp_client.address), 5); + + // another 4 hours pass by and `bidder_b` places a higher bid + env.ledger().with_mut(|li| { + li.timestamp = FOUR_HOURS * 2; + }); + mp_client.place_bid(&1, &bidder_b, &10); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 90); + assert_eq!(token_client.balance(&mp_client.address), 10); + + // 12 hours in total pass by and `bidder_c` places a higher bid + env.ledger().with_mut(|li| { + li.timestamp = FOUR_HOURS * 3; + }); + mp_client.place_bid(&1, &bidder_c, &50); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&bidder_c), 50); + assert_eq!(token_client.balance(&mp_client.address), 50); + + // 13 hours in total pass by and `bidder_b` tries to place a bid, but that's not enough + env.ledger().with_mut(|li| { + li.timestamp = FOUR_HOURS * 3 + 1; + }); + let _ = mp_client.try_place_bid(&1, &bidder_b, &25); + assert_eq!(token_client.balance(&bidder_a), 100); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&bidder_c), 50); + assert_eq!(token_client.balance(&mp_client.address), 50); + + // 16 hours in total pass by and `bidder_a` places the highest bid + env.ledger().with_mut(|li| { + li.timestamp = FOUR_HOURS * 4; + }); + let _ = mp_client.try_place_bid(&1, &bidder_a, &75); + assert_eq!(token_client.balance(&bidder_a), 25); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&bidder_c), 100); + assert_eq!(token_client.balance(&mp_client.address), 75); + + // we wrap it up and the winner is `bidder_a` with a highest bid of 75 + env.ledger().with_mut(|li| li.timestamp = WEEKLY + DAY); + mp_client.finalize_auction(&1); + + // check if the finances are the same + assert_eq!(token_client.balance(&bidder_a), 25); + assert_eq!(token_client.balance(&bidder_b), 100); + assert_eq!(token_client.balance(&bidder_c), 100); + assert_eq!(token_client.balance(&mp_client.address), 0); + + // check if `bidder_a` has 1 NFT of the item + assert_eq!(collections_client.balance_of(&bidder_a, &1), 1); + // when we initialized the collection client we automatically minted 2 items for the same id + // now the seller should have 1 + assert_eq!(collections_client.balance_of(&seller, &1), 1); +} + +#[test] +fn fail_to_finalyze_auction_when_endtime_not_reached() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + let bidder = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + token_client.mint(&bidder, &50); + + let item_info = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + mp_client.place_bid(&1, &bidder, &50); + + assert_eq!(token_client.balance(&mp_client.address), 50i128); + assert_eq!(token_client.balance(&bidder), 0i128); + env.ledger().with_mut(|li| li.timestamp = DAY); + + assert_eq!( + mp_client.try_finalize_auction(&1,), + Err(Ok(ContractError::AuctionNotFinished)) + ); + + // auction is not yet over, so the bid is still in place + assert_eq!(token_client.balance(&mp_client.address), 50i128); + assert_eq!(token_client.balance(&bidder), 0i128); +} +#[test] +fn finalize_auction_when_minimal_price_not_reached_should_refund_last_bidder() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let seller = Address::generate(&env); + let bidder_a = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + token_client.mint(&bidder_a, &5); + + let item_info = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + // we got the highest bid on day #1 + env.ledger().with_mut(|li| li.timestamp = DAY); + mp_client.place_bid(&1, &bidder_a, &5); + + assert_eq!(token_client.balance(&mp_client.address), 5i128); + assert_eq!(token_client.balance(&bidder_a), 0i128); + + // we try to finalize the auction 2 weeks later + env.ledger().with_mut(|li| li.timestamp = WEEKLY * 2); + + mp_client.finalize_auction(&1); + + assert_eq!( + mp_client.get_auction(&1), + Auction { + id: 1, + item_info, + seller, + highest_bid: Some(5), + end_time: WEEKLY, + status: AuctionStatus::Ended, + auction_token: token_client.address.clone() + } + ); + assert_eq!(token_client.balance(&mp_client.address), 0i128); + assert_eq!(token_client.balance(&bidder_a), 5i128); +} + +#[test] +fn fail_to_finalyze_auction_when_not_correct_state() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + + let token_client = token::Client::new(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + let item_info = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + env.ledger().with_mut(|li| li.timestamp = DAY); + + mp_client.pause(&1); + + assert_eq!( + mp_client.try_finalize_auction(&1,), + Err(Ok(ContractError::AuctionNotActive)) + ); +} + +#[test] +fn get_active_auctions_should_list_correct_number_of_active_auctions() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + + let token_client = token::Client::new(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + nft_collection_client.mint_batch(&seller, &seller, &vec![&env, 1, 2, 3], &vec![&env, 1, 1, 1]); + + let first_item = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + let second_item = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 2u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + let third_item = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 3u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + mp_client.create_auction(&first_item, &seller, &WEEKLY); + mp_client.create_auction(&second_item, &seller, &WEEKLY); + mp_client.create_auction(&third_item, &seller, &WEEKLY); + + assert_eq!(mp_client.get_active_auctions(&None, &None).len(), 3); + + mp_client.pause(&1); + assert_eq!(mp_client.get_active_auctions(&None, &None).len(), 2); +} diff --git a/contracts/auctions/src/test/initialization.rs b/contracts/auctions/src/test/initialization.rs new file mode 100644 index 00000000..0051e646 --- /dev/null +++ b/contracts/auctions/src/test/initialization.rs @@ -0,0 +1,240 @@ +extern crate std; +use soroban_sdk::{testutils::Address as _, token, Address, Env}; + +use crate::{ + collection, + contract::{MarketplaceContract, MarketplaceContractClient}, + error::ContractError, + storage::{Auction, AuctionStatus, ItemInfo}, + test::setup::{create_multiple_auctions, generate_marketplace_and_collection_client, WEEKLY}, +}; + +use super::setup::deploy_token_contract; + +#[test] +fn initialize_and_update_admin_should_work() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let new_admin = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + let mp_client = + MarketplaceContractClient::new(&env, &env.register_contract(None, MarketplaceContract {})); + + mp_client.initialize(&admin, &token_client.address); + mp_client.update_admin(&new_admin); +} + +#[test] +fn mp_should_create_auction() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + + let token_client = token::Client::new(&env, &Address::generate(&env)); + let (mp_client, nft_collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + let item_info = ItemInfo { + collection_addr: nft_collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + // check if we have minted two + assert_eq!(nft_collection_client.balance_of(&seller, &1), 2); + mp_client.create_auction(&item_info, &seller, &WEEKLY); + + assert_eq!( + mp_client.get_auction(&1), + Auction { + id: 1, + item_info, + seller: seller.clone(), + highest_bid: None, + end_time: WEEKLY, + status: AuctionStatus::Active, + auction_token: token_client.address + } + ); +} + +#[test] +fn initialize_twice_should_fail() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let admin = Address::generate(&env); + let seller = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &admin); + let (mp_client, _) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + assert_eq!( + mp_client.try_initialize(&admin, &token_client.address), + Err(Ok(ContractError::AlreadyInitialized)) + ); +} + +#[test] +fn mp_should_fail_to_create_auction_where_not_enought_balance_of_the_item() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + + let token_client = token::Client::new(&env, &Address::generate(&env)); + // we don't want to use the collection from the setup method, as this will automatically + // mint an item for the auction. + let (mp_client, _) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + let collection_addr = env.register_contract_wasm(None, collection::WASM); + + let collection_client = collection::Client::new(&env, &collection_addr); + collection_client.initialize( + &seller, + &soroban_sdk::String::from_str(&env, "Soroban Kitties"), + &soroban_sdk::String::from_str(&env, "SKT"), + ); + + let item_info = ItemInfo { + collection_addr: collection_client.address.clone(), + item_id: 1u64, + minimum_price: Some(10), + buy_now_price: Some(50), + }; + + assert_eq!( + mp_client.try_create_auction(&item_info, &seller, &WEEKLY), + Err(Ok(ContractError::NotEnoughBalance)) + ); +} + +#[test] +fn mp_should_be_able_create_multiple_auctions_and_query_them_with_pagination() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + + let seller = Address::generate(&env); + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + + let (mp_client, collection_client) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + create_multiple_auctions(&mp_client, &seller, &collection_client, 25); + + //we have created 25 auctions and if we don't specify anything the default search would be + //from 1..=10 + let result = mp_client.get_active_auctions(&None, &None); + assert_eq!( + result + .into_iter() + .map(|a| a.id) + .collect::>(), + std::vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ); + + // manual from 1..=10 + let result = mp_client.get_active_auctions(&Some(1), &Some(10)); + assert_eq!( + result + .into_iter() + .map(|a| a.id) + .collect::>(), + std::vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ); + + // manaul from 10..=20 + let result = mp_client.get_active_auctions(&Some(10), &Some(20)); + assert_eq!( + result + .into_iter() + .map(|a| a.id) + .collect::>(), + std::vec![10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + ); + + // manaul from 1..=25 + let result = mp_client.get_active_auctions(&Some(1), &Some(25)); + assert_eq!( + result + .into_iter() + .map(|a| a.id) + .collect::>(), + // I'm lazy kek + (1..=25).collect::>() + ); +} + +#[test] +fn get_auction_by_id_should_return_an_err_when_id_not_found() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + let (mp_client, _) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + assert_eq!( + mp_client.try_get_auction(&5), + Err(Ok(ContractError::AuctionNotFound)) + ) +} + +#[test] +fn get_auction_by_seller_should_return_an_err_when_id_not_found() { + let env = Env::default(); + env.mock_all_auths(); + env.budget().reset_unlimited(); + let seller = Address::generate(&env); + + let token_client = deploy_token_contract(&env, &Address::generate(&env)); + let (mp_client, _) = generate_marketplace_and_collection_client( + &env, + &seller, + &token_client.address, + None, + None, + ); + + assert_eq!( + mp_client.try_get_auctions_by_seller(&Address::generate(&env)), + Err(Ok(ContractError::AuctionNotFound)) + ) +} diff --git a/contracts/auctions/src/test/setup.rs b/contracts/auctions/src/test/setup.rs new file mode 100644 index 00000000..9d1c7f05 --- /dev/null +++ b/contracts/auctions/src/test/setup.rs @@ -0,0 +1,94 @@ +use soroban_sdk::{testutils::Address as _, xdr::ToXdr, Address, Bytes, Env, String}; + +use crate::{ + collection::{self, Client}, + contract::{MarketplaceContract, MarketplaceContractClient}, + storage::ItemInfo, + token, +}; + +pub const WEEKLY: u64 = 604_800u64; +pub const DAY: u64 = 86_400u64; +pub const FOUR_HOURS: u64 = 14_400u64; + +pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token::Client<'a> { + token::Client::new(env, &env.register_stellar_asset_contract(admin.clone())) +} + +pub mod auctions_wasm { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/phoenix_nft_auctions.wasm" + ); +} + +pub fn generate_marketplace_and_collection_client<'a>( + env: &Env, + admin: &Address, + auction_token: &Address, + name: Option, + symbol: Option, +) -> (MarketplaceContractClient<'a>, collection::Client<'a>) { + let mp_client = + MarketplaceContractClient::new(env, &env.register_contract(None, MarketplaceContract {})); + + mp_client.initialize(admin, auction_token); + + let alt_name = String::from_str(env, "Stellar kitties"); + let alt_symbol = String::from_str(env, "STK"); + + let name = name.unwrap_or(alt_name); + let symbol = symbol.unwrap_or(alt_symbol); + let collection_addr = env.register_contract_wasm(None, collection::WASM); + + let collection_client = collection::Client::new(env, &collection_addr); + collection_client.initialize(admin, &name, &symbol); + collection_client.mint(admin, admin, &1, &2); + + (mp_client, collection_client) +} + +pub fn create_multiple_auctions( + mp_client: &crate::contract::MarketplaceContractClient, + seller: &Address, + collection_client: &Client, + number_of_auctions_to_make: usize, +) { + for idx in 1..=number_of_auctions_to_make { + collection_client.mint(seller, seller, &(idx as u64), &2); + + let item_info = ItemInfo { + collection_addr: collection_client.address.clone(), + item_id: idx as u64, + minimum_price: None, + buy_now_price: None, + }; + mp_client.create_auction(&item_info, seller, &WEEKLY); + } +} + +/// This also mints 5 items of id #1 to the owner of the collection +pub fn create_and_initialize_collection<'a>( + env: &Env, + seller: &Address, + collection_name: &str, + collection_symbol: &str, +) -> collection::Client<'a> { + let collection_name = String::from_str(env, collection_name); + let collection_symbol = String::from_str(env, collection_symbol); + + let mut salt = Bytes::new(env); + salt.append(&seller.clone().to_xdr(env)); + let salt = env.crypto().sha256(&salt); + + let collection_addr = env + .deployer() + .with_address(Address::generate(env), salt) + .deploy(env.deployer().upload_contract_wasm(collection::WASM)); + + let collection_client = collection::Client::new(env, &collection_addr); + collection_client.initialize(seller, &collection_name, &collection_symbol); + + collection_client.mint(seller, seller, &1, &5); + + collection_client +} diff --git a/contracts/collections/src/contract.rs b/contracts/collections/src/contract.rs index 1b126139..72249074 100644 --- a/contracts/collections/src/contract.rs +++ b/contracts/collections/src/contract.rs @@ -62,7 +62,14 @@ impl Collections { ids: Vec, ) -> Result, ContractError> { if accounts.len() != ids.len() { - log!(&env, "Collections: Balance of batch: length missmatch"); + log!( + &env, + "Collections: Balance of batch: length missmatch: ", + "accounts length: ", + accounts.len(), + "ids length: ", + ids.len() + ); return Err(ContractError::AccountsIdsLengthMissmatch); } let mut batch_balances: Vec = vec![&env]; @@ -94,7 +101,8 @@ impl Collections { if admin == operator { log!( &env, - "Collection: Set approval for all: Cannot set approval for self" + "Collection: Set approval for all: Cannot set approval for self. Operator: ", + operator ); return Err(ContractError::CannotApproveSelf); } @@ -125,6 +133,7 @@ impl Collections { pub fn set_approval_for_transfer( env: Env, operator: Address, + nft_id: u64, approved: bool, ) -> Result<(), ContractError> { let admin = get_admin(&env)?; @@ -133,7 +142,8 @@ impl Collections { if admin == operator { log!( &env, - "Collection: Set approval for transfer: Trying to authorize self" + "Collection: Set approval for transfer: Trying to authorize admin. Operator: ", + operator ); return Err(ContractError::CannotApproveSelf); } @@ -141,6 +151,7 @@ impl Collections { let data_key = DataKey::TransferApproval(TransferApprovalKey { owner: admin.clone(), operator: operator.clone(), + nft_id, }); env.storage().persistent().set(&data_key, &approved); @@ -154,8 +165,9 @@ impl Collections { ( "Set approval for transfer", "Set approval for operator addr: ", + "Set approval for nft id: ", ), - operator, + (operator, nft_id), ); env.events() .publish(("Set approval for", "New approval: "), approved); @@ -165,11 +177,7 @@ impl Collections { // Returns true if `operator` is approved to manage `owner`'s tokens #[allow(dead_code)] - pub fn is_approved_for_all( - env: Env, - owner: Address, - operator: Address, - ) -> Result { + pub fn is_approved_for_all(env: Env, owner: Address, operator: Address) -> bool { let data_key = DataKey::OperatorApproval(OperatorApprovalKey { owner, operator }); let result = env.storage().persistent().get(&data_key).unwrap_or(false); @@ -180,7 +188,7 @@ impl Collections { .extend_ttl(&data_key, LIFETIME_THRESHOLD, BUMP_AMOUNT) }); - Ok(result) + result } // Returns true if `operator` is approved to manage `owner`'s tokens @@ -189,8 +197,13 @@ impl Collections { env: Env, owner: Address, operator: Address, - ) -> Result { - let data_key = DataKey::TransferApproval(TransferApprovalKey { owner, operator }); + nft_id: u64, + ) -> bool { + let data_key = DataKey::TransferApproval(TransferApprovalKey { + owner, + nft_id, + operator, + }); let result = env.storage().persistent().get(&data_key).unwrap_or(false); @@ -200,7 +213,7 @@ impl Collections { .extend_ttl(&data_key, LIFETIME_THRESHOLD, BUMP_AMOUNT) }); - Ok(result) + result } // Transfers `amount` tokens of token type `id` from `from` to `to` @@ -213,21 +226,39 @@ impl Collections { id: u64, transfer_amount: u64, ) -> Result<(), ContractError> { - Self::is_authorized_for_transfer(&env, &sender)?; + // if the sender is NOT transferring his own tokens and he's not authorized for transfer then we fail + if sender != from && !Self::is_authorized_for_transfer(&env, &sender, id) { + log!( + &env, + "Collection: Safe Transfer From: Unauthorized.", + sender, + " trying to transfer from ", + from + ); + return Err(ContractError::Unauthorized); + } + + sender.require_auth(); - let sender_balance = get_balance_of(&env, &from, id)?; + let from_balance = get_balance_of(&env, &from, id)?; let rcpt_balance = get_balance_of(&env, &to, id)?; - if sender_balance < transfer_amount { - log!(&env, "Collection: Safe transfer from: Insuficient Balance"); + if from_balance < transfer_amount { + log!( + &env, + "Collection: Safe batch transfer from: Insufficient Balance", + "Available balance: ", + from_balance, + "Amount to send: ", + transfer_amount + ); return Err(ContractError::InsufficientBalance); } - //NOTE: checks if we go over the limit of u64::MAX? - // first we reduce the sender's `from` balance - update_balance_of(&env, &from, id, sender_balance - transfer_amount)?; + // first we reduce `from` balance + update_balance_of(&env, &from, id, from_balance - transfer_amount)?; - // next we incrase the recipient's `to` balance + // next we incrase `to` balance update_balance_of(&env, &to, id, rcpt_balance + transfer_amount)?; env.events().publish(("safe transfer from", "from: "), from); @@ -249,19 +280,36 @@ impl Collections { ids: Vec, amounts: Vec, ) -> Result<(), ContractError> { - Self::is_authorized_for_transfer(&env, &sender)?; - if ids.len() != amounts.len() { log!( &env, - "Collection: Safe batch transfer from: length mismatch" + "Collection: Safe batch transfer from: length mismatch", + "ids length: ", + ids.len(), + "amounts length: ", + amounts.len() ); return Err(ContractError::IdsAmountsLengthMismatch); } + for id in 0..ids.len() { + if sender != from && !Self::is_authorized_for_transfer(&env, &sender, id.into()) { + log!( + &env, + "Collection: Safe Transfer From: Unauthorized.", + sender, + " trying to transfer from ", + from + ); + return Err(ContractError::Unauthorized); + } + } + + sender.require_auth(); + for idx in 0..ids.len() { - let id = ids.get(idx).unwrap(); - let amount = amounts.get(idx).unwrap(); + let id = ids.get(idx).ok_or(ContractError::InvalidIdIndex)?; + let amount = amounts.get(idx).ok_or(ContractError::InvalidAmountIndex)?; let sender_balance = get_balance_of(&env, &from, id)?; let rcpt_balance = get_balance_of(&env, &to, id)?; @@ -269,7 +317,12 @@ impl Collections { if sender_balance < amount { log!( &env, - "Collection: Safe batch transfer from: Insufficient Balance" + "Collection: Safe batch transfer from: Insufficient balance for id ", + id, + " Available balance: ", + sender_balance, + " Amount to send: ", + amount ); return Err(ContractError::InsufficientBalance); } @@ -294,7 +347,6 @@ impl Collections { } // Mints `amount` tokens of token type `id` to `to` - // FIXME: currently this doesn't check if we have minted the same ID twice. #[allow(dead_code)] pub fn mint( env: Env, @@ -303,7 +355,12 @@ impl Collections { id: u64, amount: u64, ) -> Result<(), ContractError> { - Self::is_authorized_for_all(&env, &sender)?; + if !Self::is_authorized_for_all(&env, &sender) { + log!(&env, "Collections: Mint: Unauthorized. Sender: ", sender); + return Err(ContractError::Unauthorized); + } + + sender.require_auth(); let current_balance = get_balance_of(&env, &to, id)?; update_balance_of(&env, &to, id, current_balance + amount)?; @@ -325,10 +382,21 @@ impl Collections { ids: Vec, amounts: Vec, ) -> Result<(), ContractError> { - Self::is_authorized_for_all(&env, &sender)?; + if !Self::is_authorized_for_all(&env, &sender) { + log!(&env, "Collections: Mint: Unauthorized. Sender: ", sender); + return Err(ContractError::Unauthorized); + } + sender.require_auth(); if ids.len() != amounts.len() { - log!(&env, "Collection: Mint batch: length mismatch"); + log!( + &env, + "Collections: Mint Batch: Length missmatch: ", + "amounts length: ", + amounts.len(), + "ids length: ", + ids.len() + ); return Err(ContractError::IdsAmountsLengthMismatch); } @@ -357,12 +425,24 @@ impl Collections { id: u64, amount: u64, ) -> Result<(), ContractError> { - Self::is_authorized_for_all(&env, &sender)?; + if sender != from && !Self::is_authorized_for_all(&env, &sender) { + log!(&env, "Collections: Mint: Unauthorized. Sender: ", sender); + return Err(ContractError::Unauthorized); + } + + sender.require_auth(); let current_balance = get_balance_of(&env, &from, id)?; if current_balance < amount { - log!(&env, "Collection: Burn: Insufficient Balance"); + log!( + &env, + "Collection: Burn: Insufficient Balance", + "Available balance: ", + current_balance, + "Amount to transfer: ", + amount + ); return Err(ContractError::InsufficientBalance); } @@ -384,10 +464,22 @@ impl Collections { ids: Vec, amounts: Vec, ) -> Result<(), ContractError> { - Self::is_authorized_for_all(&env, &sender)?; + if sender != from && !Self::is_authorized_for_all(&env, &sender) { + log!(&env, "Collections: Mint: Unauthorized. Sender: ", sender); + return Err(ContractError::Unauthorized); + } + + sender.require_auth(); if ids.len() != amounts.len() { - log!(&env, "Collection: Burn batch: length mismatch"); + log!( + &env, + "Collection: Burn batch: length mismatch", + "ids length: ", + ids.len(), + "amounts length: ", + amounts.len() + ); return Err(ContractError::IdsAmountsLengthMismatch); } @@ -397,7 +489,15 @@ impl Collections { let current_balance = get_balance_of(&env, &from, id)?; if current_balance < amount { - log!(&env, "Collection: Burn batch: Insufficient Balance"); + log!( + &env, + "Collection: Burn batch: Insufficient balance for id ", + id, + " Available balance: ", + current_balance, + " Amount to transfer: ", + amount + ); return Err(ContractError::InsufficientBalance); } update_balance_of(&env, &from, id, current_balance - amount)?; @@ -413,7 +513,11 @@ impl Collections { // Sets a new URI for a token type `id` #[allow(dead_code)] pub fn set_uri(env: Env, sender: Address, id: u64, uri: Bytes) -> Result<(), ContractError> { - Self::is_authorized_for_all(&env, &sender)?; + if !Self::is_authorized_for_all(&env, &sender) { + log!(&env, "Collections: Mint: Unauthorized. Sender: ", sender); + return Err(ContractError::Unauthorized); + } + sender.require_auth(); env.storage() .persistent() @@ -432,7 +536,11 @@ impl Collections { // Sets the main image(logo) for the collection #[allow(dead_code)] pub fn set_collection_uri(env: Env, sender: Address, uri: Bytes) -> Result<(), ContractError> { - Self::is_authorized_for_all(&env, &sender)?; + if !Self::is_authorized_for_all(&env, &sender) { + log!(&env, "Collections: Mint: Unauthorized. Sender: ", sender); + return Err(ContractError::Unauthorized); + } + sender.require_auth(); env.storage() .persistent() @@ -504,36 +612,17 @@ impl Collections { Ok(mabye_config) } - fn is_authorized_for_transfer(env: &Env, sender: &Address) -> Result<(), ContractError> { - let admin = get_admin(env)?; + fn is_authorized_for_transfer(env: &Env, sender: &Address, nft_id: u64) -> bool { + let admin = get_admin(env).expect("no admin found"); - if admin == sender.clone() - || Self::is_approved_for_all(env.clone(), admin.clone(), sender.clone())? - || Self::is_approved_for_transfer(env.clone(), admin.clone(), sender.clone())? - { - sender.require_auth(); - } else { - log!( - &env, - "Collections: Safe Transfer From: Unauthorized to transfer." - ); - return Err(ContractError::Unauthorized); - } - - Ok(()) + admin == sender.clone() + || Self::is_approved_for_all(env.clone(), admin.clone(), sender.clone()) + || Self::is_approved_for_transfer(env.clone(), admin.clone(), sender.clone(), nft_id) } - fn is_authorized_for_all(env: &Env, sender: &Address) -> Result<(), ContractError> { - let admin = get_admin(env)?; - - if admin == sender.clone() || Self::is_approved_for_all(env.clone(), admin, sender.clone())? - { - sender.require_auth(); - } else { - log!(&env, "Collections: Mint batch: Unauthorized"); - return Err(ContractError::Unauthorized); - } + fn is_authorized_for_all(env: &Env, sender: &Address) -> bool { + let admin = get_admin(env).expect("no admin found"); - Ok(()) + admin == sender.clone() || Self::is_approved_for_all(env.clone(), admin, sender.clone()) } } diff --git a/contracts/collections/src/storage.rs b/contracts/collections/src/storage.rs index 80b77512..fbc457a4 100644 --- a/contracts/collections/src/storage.rs +++ b/contracts/collections/src/storage.rs @@ -23,6 +23,7 @@ pub struct OperatorApprovalKey { pub struct TransferApprovalKey { pub owner: Address, pub operator: Address, + pub nft_id: u64, } // Enum to represent different data keys in storage diff --git a/contracts/collections/src/test/tests.rs b/contracts/collections/src/test/tests.rs index 636ee673..c12e5b26 100644 --- a/contracts/collections/src/test/tests.rs +++ b/contracts/collections/src/test/tests.rs @@ -560,7 +560,7 @@ fn should_fail_when_admin_tries_to_set_himself_as_operator_for_approval_for_tran let collectoins_client = initialize_collection_contract(&env, Some(&admin), None, None); assert_eq!( - collectoins_client.try_set_approval_for_transfer(&admin, &true), + collectoins_client.try_set_approval_for_transfer(&admin, &1, &true), Err(Ok(ContractError::CannotApproveSelf)) ); } @@ -603,7 +603,7 @@ fn safe_transfer_from_should_fail_when_user_is_not_authorized() { // admin mints himself a new NFT collections_client.mint(&admin, &admin, &1, &2); // admin sets operator to be able to do as they like with the NFT - collections_client.set_approval_for_transfer(&operator, &true); + collections_client.set_approval_for_transfer(&operator, &1, &true); // rogue user tries to steal, but fails assert_eq!( @@ -622,7 +622,7 @@ fn safe_transfer_from_should_fail_when_user_is_not_authorized() { assert_eq!(collections_client.balance_of(&rcpt, &1), 1); // admin revokes rights - collections_client.set_approval_for_transfer(&operator, &false); + collections_client.set_approval_for_transfer(&operator, &1, &false); assert_eq!( collections_client.try_safe_transfer_from(&operator, &admin, &rcpt, &1, &1), @@ -754,3 +754,127 @@ fn grant_all_permissions_to_user_then_withdraw_them() { Err(Ok(ContractError::Unauthorized)) ) } + +#[test] +fn safe_batch_transfer_should_succeed_when_sender_from_the_same() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user_a = Address::generate(&env); + let rcpt = Address::generate(&env); + + let client = initialize_collection_contract(&env, Some(&admin), None, None); + + let ids = vec![&env, 1, 2, 3, 4, 5]; + let amounts = vec![&env, 5, 4, 3, 2, 1]; + client.mint_batch(&admin, &user_a, &ids, &amounts); + + let accounts = vec![ + &env, + user_a.clone(), + user_a.clone(), + user_a.clone(), + user_a.clone(), + user_a.clone(), + ]; + assert_eq!( + client.balance_of_batch(&accounts, &ids), + vec![&env, 5, 4, 3, 2, 1] + ); + + client.safe_batch_transfer_from(&user_a, &user_a, &rcpt, &ids, &amounts); + // rcpt now has all the tokens + assert_eq!( + client.balance_of_batch( + &vec![ + &env, + rcpt.clone(), + rcpt.clone(), + rcpt.clone(), + rcpt.clone(), + rcpt.clone() + ], + &ids + ), + vec![&env, 5, 4, 3, 2, 1] + ); + + // original owner has 0 for all the ids + assert_eq!( + client.balance_of_batch( + &vec![ + &env, + user_a.clone(), + user_a.clone(), + user_a.clone(), + user_a.clone(), + user_a.clone() + ], + &ids + ), + vec![&env, 0, 0, 0, 0, 0] + ) +} + +#[test] +fn safe_batch_transfer_should_fail_when_sender_is_not_authorized_to_transfer_from_from() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user_a = Address::generate(&env); + + let client = initialize_collection_contract(&env, Some(&admin), None, None); + + let ids = vec![&env, 1, 2, 3, 4, 5]; + let amounts = vec![&env, 5, 4, 3, 2, 1]; + client.mint_batch(&admin, &user_a, &ids, &amounts); + + let accounts = vec![ + &env, + user_a.clone(), + user_a.clone(), + user_a.clone(), + user_a.clone(), + user_a.clone(), + ]; + assert_eq!( + client.balance_of_batch(&accounts, &ids), + vec![&env, 5, 4, 3, 2, 1] + ); + + assert_eq!( + client.try_safe_batch_transfer_from( + &Address::generate(&env), + &user_a, + &Address::generate(&env), + &ids, + &amounts, + ), + Err(Ok(ContractError::Unauthorized)) + ); +} + +#[test] +fn safe_transfer_should_work_when_sender_is_from_and_is_authorized() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user_a = Address::generate(&env); + let user_b = Address::generate(&env); + + let client = initialize_collection_contract(&env, Some(&admin), None, None); + + client.mint(&admin, &user_a, &1, &1); + client.set_approval_for_transfer(&user_a, &1, &true); + + assert_eq!(client.balance_of(&user_a, &1), 1u64); + assert_eq!(client.balance_of(&user_b, &1), 0u64); + + client.safe_transfer_from(&user_a, &user_a, &user_b, &1, &1); + + assert_eq!(client.balance_of(&user_a, &1), 0u64); + assert_eq!(client.balance_of(&user_b, &1), 1u64); +} diff --git a/contracts/token/Cargo.lock b/contracts/token/Cargo.lock new file mode 100644 index 00000000..1630e280 --- /dev/null +++ b/contracts/token/Cargo.lock @@ -0,0 +1,1566 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "serde", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901" +dependencies = [ + "quote", + "syn 2.0.39", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.39", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.39", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "ecdsa" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceca8aaf45b5c46ec7ed39fff75f57290368c1846d33d24a122ca81416ab058" +dependencies = [ + "proc-macro2", + "syn 2.0.39", +] + +[[package]] +name = "primeorder" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + +[[package]] +name = "sec1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.0", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "soroban-builtin-sdk-macros" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44877373b3dc6c662377cb1600e3a62706d75e484b6064f9cd22e467c676b159" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "soroban-env-common" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590add16843a61b01844e19e89bccaaee6aa21dc76809017b0662c17dc139ee9" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8dc43acdd6c7e7b371acf44fc1a7dac24934ae3b2f05fafd618818548176" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e25aaffe0c62eb65e0e349f725b4b8b13ad0764d78a15aab5bbccb5c4797726" +dependencies = [ + "backtrace", + "curve25519-dalek", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e16b761459fdf3c4b62b24df3941498d14e5246e6fadfb4774ed8114d243aa4" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn 2.0.39", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "21.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebb7961fc6d8f47e00d404d9240f51aba85df9d67a4f556ef1c6057b5327a8" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "21.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60cd55eb88cbe1d9e7fe3ab1845c7c10c26b27e2d226e973150e5b55580aa359" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "ed25519-dalek", + "rand", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "21.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1916985d1871aa340d7eec834c387f74f80231c846801193c3252266a60a41e" +dependencies = [ + "crate-git-revision", + "darling", + "itertools", + "proc-macro2", + "quote", + "rustc_version", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn 2.0.39", +] + +[[package]] +name = "soroban-spec" +version = "21.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439faff6a155975a9951f27aaa04ae8ef3fd8fe9413d202e2ea2ff094a593449" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "21.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b2a571055f1ed15427ccb8d34ce4208b4135666eade124cfbecfc010fa2ea3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn 2.0.39", + "thiserror", +] + +[[package]] +name = "soroban-token-contract" +version = "0.0.6" +dependencies = [ + "soroban-sdk", + "soroban-token-sdk", +] + +[[package]] +name = "soroban-token-sdk" +version = "21.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be4a8070dab1ac67007da10e51e0c3366046ead5dbff974b3cec468532250b" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "wasmi_arena" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401c1f35e413fac1846d4843745589d9ec678977ab35a384db8ae7830525d468" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" +dependencies = [ + "indexmap 2.1.0", + "semver", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9157cab83003221bfd385833ab587a039f5d6fa7304854042ba358a3b09e0724" +dependencies = [ + "indexmap-nostd", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/contracts/token/Cargo.toml b/contracts/token/Cargo.toml new file mode 100644 index 00000000..a39a67d9 --- /dev/null +++ b/contracts/token/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "soroban-token-contract" +description = "Soroban standard token contract" +version = "0.0.6" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } +soroban-token-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/token/Makefile b/contracts/token/Makefile new file mode 100644 index 00000000..8ed428c3 --- /dev/null +++ b/contracts/token/Makefile @@ -0,0 +1,18 @@ +default: build + +all: test + +test: build + cargo test + +build: + cargo build --target wasm32-unknown-unknown --release + +fmt: + cargo fmt --all + +clippy: build + cargo clippy --tests -- -D warnings + +clean: + cargo clean diff --git a/contracts/token/README.md b/contracts/token/README.md new file mode 100644 index 00000000..33cbfcc9 --- /dev/null +++ b/contracts/token/README.md @@ -0,0 +1,3 @@ +# TOKEN + +```Follows the example of [Soroban token](https://github.com/stellar/soroban-examples/tree/main/token)``` diff --git a/contracts/token/src/admin.rs b/contracts/token/src/admin.rs new file mode 100644 index 00000000..a820bf04 --- /dev/null +++ b/contracts/token/src/admin.rs @@ -0,0 +1,18 @@ +use soroban_sdk::{Address, Env}; + +use crate::storage_types::DataKey; + +pub fn has_administrator(e: &Env) -> bool { + let key = DataKey::Admin; + e.storage().instance().has(&key) +} + +pub fn read_administrator(e: &Env) -> Address { + let key = DataKey::Admin; + e.storage().instance().get(&key).unwrap() +} + +pub fn write_administrator(e: &Env, id: &Address) { + let key = DataKey::Admin; + e.storage().instance().set(&key, id); +} diff --git a/contracts/token/src/allowance.rs b/contracts/token/src/allowance.rs new file mode 100644 index 00000000..c70e2bca --- /dev/null +++ b/contracts/token/src/allowance.rs @@ -0,0 +1,65 @@ +use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; +use soroban_sdk::{Address, Env}; + +pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue { + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + if let Some(allowance) = e.storage().temporary().get::<_, AllowanceValue>(&key) { + if allowance.expiration_ledger < e.ledger().sequence() { + AllowanceValue { + amount: 0, + expiration_ledger: allowance.expiration_ledger, + } + } else { + allowance + } + } else { + AllowanceValue { + amount: 0, + expiration_ledger: 0, + } + } +} + +pub fn write_allowance( + e: &Env, + from: Address, + spender: Address, + amount: i128, + expiration_ledger: u32, +) { + let allowance = AllowanceValue { + amount, + expiration_ledger, + }; + + if amount > 0 && expiration_ledger < e.ledger().sequence() { + panic!("expiration_ledger is less than ledger seq when amount > 0") + } + + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + e.storage().temporary().set(&key.clone(), &allowance); + + if amount > 0 { + let live_for = expiration_ledger + .checked_sub(e.ledger().sequence()) + .unwrap(); + + e.storage().temporary().extend_ttl(&key, live_for, live_for) + } +} + +pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) { + let allowance = read_allowance(e, from.clone(), spender.clone()); + if allowance.amount < amount { + panic!("insufficient allowance"); + } + if amount > 0 { + write_allowance( + e, + from, + spender, + allowance.amount - amount, + allowance.expiration_ledger, + ); + } +} diff --git a/contracts/token/src/balance.rs b/contracts/token/src/balance.rs new file mode 100644 index 00000000..76134e8d --- /dev/null +++ b/contracts/token/src/balance.rs @@ -0,0 +1,35 @@ +use crate::storage_types::{DataKey, BALANCE_BUMP_AMOUNT, BALANCE_LIFETIME_THRESHOLD}; +use soroban_sdk::{Address, Env}; + +pub fn read_balance(e: &Env, addr: Address) -> i128 { + let key = DataKey::Balance(addr); + if let Some(balance) = e.storage().persistent().get::(&key) { + e.storage() + .persistent() + .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); + balance + } else { + 0 + } +} + +fn write_balance(e: &Env, addr: Address, amount: i128) { + let key = DataKey::Balance(addr); + e.storage().persistent().set(&key, &amount); + e.storage() + .persistent() + .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); +} + +pub fn receive_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + write_balance(e, addr, balance + amount); +} + +pub fn spend_balance(e: &Env, addr: Address, amount: i128) { + let balance = read_balance(e, addr.clone()); + if balance < amount { + panic!("insufficient balance"); + } + write_balance(e, addr, balance - amount); +} diff --git a/contracts/token/src/contract.rs b/contracts/token/src/contract.rs new file mode 100644 index 00000000..3722b3dd --- /dev/null +++ b/contracts/token/src/contract.rs @@ -0,0 +1,176 @@ +//! This contract demonstrates a sample implementation of the Soroban token +//! interface. +use crate::admin::{has_administrator, read_administrator, write_administrator}; +use crate::allowance::{read_allowance, spend_allowance, write_allowance}; +use crate::balance::{read_balance, receive_balance, spend_balance}; +use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata}; +#[cfg(test)] +use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; +use crate::storage_types::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; +use soroban_sdk::token::{self, Interface as _}; +use soroban_sdk::{contract, contractimpl, Address, Env, String}; +use soroban_token_sdk::metadata::TokenMetadata; +use soroban_token_sdk::TokenUtils; + +fn check_nonnegative_amount(amount: i128) { + if amount < 0 { + panic!("negative amount is not allowed: {}", amount) + } +} + +#[contract] +pub struct Token; + +#[contractimpl] +impl Token { + pub fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { + if has_administrator(&e) { + panic!("already initialized") + } + write_administrator(&e, &admin); + if decimal > 18 { + panic!("Decimal must not be greater than 18"); + } + + write_metadata( + &e, + TokenMetadata { + decimal, + name, + symbol, + }, + ) + } + + pub fn mint(e: Env, to: Address, amount: i128) { + check_nonnegative_amount(amount); + let admin = read_administrator(&e); + admin.require_auth(); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + receive_balance(&e, to.clone(), amount); + TokenUtils::new(&e).events().mint(admin, to, amount); + } + + pub fn set_admin(e: Env, new_admin: Address) { + let admin = read_administrator(&e); + admin.require_auth(); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + write_administrator(&e, &new_admin); + TokenUtils::new(&e).events().set_admin(admin, new_admin); + } + + #[cfg_attr(not(test), allow(dead_code))] + #[cfg(test)] + pub fn get_allowance(e: Env, from: Address, spender: Address) -> Option { + let key = DataKey::Allowance(AllowanceDataKey { from, spender }); + e.storage().temporary().get::<_, AllowanceValue>(&key) + } +} + +#[contractimpl] +impl token::Interface for Token { + fn allowance(e: Env, from: Address, spender: Address) -> i128 { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + read_allowance(&e, from, spender).amount + } + + fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) { + from.require_auth(); + + check_nonnegative_amount(amount); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + write_allowance(&e, from.clone(), spender.clone(), amount, expiration_ledger); + TokenUtils::new(&e) + .events() + .approve(from, spender, amount, expiration_ledger); + } + + fn balance(e: Env, id: Address) -> i128 { + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + read_balance(&e, id) + } + + fn transfer(e: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + + check_nonnegative_amount(amount); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + TokenUtils::new(&e).events().transfer(from, to, amount); + } + + fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) { + spender.require_auth(); + + check_nonnegative_amount(amount); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + spend_allowance(&e, from.clone(), spender, amount); + spend_balance(&e, from.clone(), amount); + receive_balance(&e, to.clone(), amount); + TokenUtils::new(&e).events().transfer(from, to, amount) + } + + fn burn(e: Env, from: Address, amount: i128) { + from.require_auth(); + + check_nonnegative_amount(amount); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + spend_balance(&e, from.clone(), amount); + TokenUtils::new(&e).events().burn(from, amount); + } + + fn burn_from(e: Env, spender: Address, from: Address, amount: i128) { + spender.require_auth(); + + check_nonnegative_amount(amount); + + e.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + spend_allowance(&e, from.clone(), spender, amount); + spend_balance(&e, from.clone(), amount); + TokenUtils::new(&e).events().burn(from, amount) + } + + fn decimals(e: Env) -> u32 { + read_decimal(&e) + } + + fn name(e: Env) -> String { + read_name(&e) + } + + fn symbol(e: Env) -> String { + read_symbol(&e) + } +} diff --git a/contracts/token/src/lib.rs b/contracts/token/src/lib.rs new file mode 100644 index 00000000..b5f04e4d --- /dev/null +++ b/contracts/token/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] + +mod admin; +mod allowance; +mod balance; +mod contract; +mod metadata; +mod storage_types; +mod test; + +pub use crate::contract::TokenClient; diff --git a/contracts/token/src/metadata.rs b/contracts/token/src/metadata.rs new file mode 100644 index 00000000..715feeea --- /dev/null +++ b/contracts/token/src/metadata.rs @@ -0,0 +1,22 @@ +use soroban_sdk::{Env, String}; +use soroban_token_sdk::{metadata::TokenMetadata, TokenUtils}; + +pub fn read_decimal(e: &Env) -> u32 { + let util = TokenUtils::new(e); + util.metadata().get_metadata().decimal +} + +pub fn read_name(e: &Env) -> String { + let util = TokenUtils::new(e); + util.metadata().get_metadata().name +} + +pub fn read_symbol(e: &Env) -> String { + let util = TokenUtils::new(e); + util.metadata().get_metadata().symbol +} + +pub fn write_metadata(e: &Env, metadata: TokenMetadata) { + let util = TokenUtils::new(e); + util.metadata().set_metadata(&metadata); +} diff --git a/contracts/token/src/storage_types.rs b/contracts/token/src/storage_types.rs new file mode 100644 index 00000000..038562fc --- /dev/null +++ b/contracts/token/src/storage_types.rs @@ -0,0 +1,30 @@ +use soroban_sdk::{contracttype, Address}; + +pub(crate) const DAY_IN_LEDGERS: u32 = 17280; +pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; +pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub(crate) const BALANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub(crate) const BALANCE_LIFETIME_THRESHOLD: u32 = BALANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +#[derive(Clone)] +#[contracttype] +pub struct AllowanceDataKey { + pub from: Address, + pub spender: Address, +} + +#[contracttype] +pub struct AllowanceValue { + pub amount: i128, + pub expiration_ledger: u32, +} + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Allowance(AllowanceDataKey), + Balance(Address), + State(Address), + Admin, +} diff --git a/contracts/token/src/test.rs b/contracts/token/src/test.rs new file mode 100644 index 00000000..c3718c0e --- /dev/null +++ b/contracts/token/src/test.rs @@ -0,0 +1,266 @@ +#![cfg(test)] +extern crate std; + +use crate::{contract::Token, TokenClient}; +use soroban_sdk::{ + symbol_short, + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, + Address, Env, IntoVal, Symbol, +}; + +fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { + let token = TokenClient::new(e, &e.register_contract(None, Token {})); + token.initialize(admin, &7, &"name".into_val(e), &"symbol".into_val(e)); + token +} + +#[test] +fn test() { + let e = Env::default(); + e.mock_all_auths(); + + let admin1 = Address::generate(&e); + let admin2 = Address::generate(&e); + let user1 = Address::generate(&e); + let user2 = Address::generate(&e); + let user3 = Address::generate(&e); + let token = create_token(&e, &admin1); + + token.mint(&user1, &1000); + assert_eq!( + e.auths(), + std::vec![( + admin1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("mint"), + (&user1, 1000_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user1), 1000); + + token.approve(&user2, &user3, &500, &200); + assert_eq!( + e.auths(), + std::vec![( + user2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("approve"), + (&user2, &user3, 500_i128, 200_u32).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.allowance(&user2, &user3), 500); + + token.transfer(&user1, &user2, &600); + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("transfer"), + (&user1, &user2, 600_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user1), 400); + assert_eq!(token.balance(&user2), 600); + + token.transfer_from(&user3, &user2, &user1, &400); + assert_eq!( + e.auths(), + std::vec![( + user3.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + Symbol::new(&e, "transfer_from"), + (&user3, &user2, &user1, 400_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.balance(&user1), 800); + assert_eq!(token.balance(&user2), 200); + + token.transfer(&user1, &user3, &300); + assert_eq!(token.balance(&user1), 500); + assert_eq!(token.balance(&user3), 300); + + token.set_admin(&admin2); + assert_eq!( + e.auths(), + std::vec![( + admin1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("set_admin"), + (&admin2,).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + + // Increase to 500 + token.approve(&user2, &user3, &500, &200); + assert_eq!(token.allowance(&user2, &user3), 500); + token.approve(&user2, &user3, &0, &200); + assert_eq!( + e.auths(), + std::vec![( + user2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("approve"), + (&user2, &user3, 0_i128, 200_u32).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + assert_eq!(token.allowance(&user2, &user3), 0); +} + +#[test] +fn test_burn() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::generate(&e); + let user1 = Address::generate(&e); + let user2 = Address::generate(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.approve(&user1, &user2, &500, &200); + assert_eq!(token.allowance(&user1, &user2), 500); + + token.burn_from(&user2, &user1, &500); + assert_eq!( + e.auths(), + std::vec![( + user2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("burn_from"), + (&user2, &user1, 500_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + + assert_eq!(token.allowance(&user1, &user2), 0); + assert_eq!(token.balance(&user1), 500); + assert_eq!(token.balance(&user2), 0); + + token.burn(&user1, &500); + assert_eq!( + e.auths(), + std::vec![( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token.address.clone(), + symbol_short!("burn"), + (&user1, 500_i128).into_val(&e), + )), + sub_invocations: std::vec![] + } + )] + ); + + assert_eq!(token.balance(&user1), 0); + assert_eq!(token.balance(&user2), 0); +} + +#[test] +#[should_panic(expected = "insufficient balance")] +fn transfer_insufficient_balance() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::generate(&e); + let user1 = Address::generate(&e); + let user2 = Address::generate(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.transfer(&user1, &user2, &1001); +} + +#[test] +#[should_panic(expected = "insufficient allowance")] +fn transfer_from_insufficient_allowance() { + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::generate(&e); + let user1 = Address::generate(&e); + let user2 = Address::generate(&e); + let user3 = Address::generate(&e); + let token = create_token(&e, &admin); + + token.mint(&user1, &1000); + assert_eq!(token.balance(&user1), 1000); + + token.approve(&user1, &user3, &100, &200); + assert_eq!(token.allowance(&user1, &user3), 100); + + token.transfer_from(&user3, &user1, &user2, &101); +} + +#[test] +#[should_panic(expected = "already initialized")] +fn initialize_already_initialized() { + let e = Env::default(); + let admin = Address::generate(&e); + let token = create_token(&e, &admin); + + token.initialize(&admin, &10, &"name".into_val(&e), &"symbol".into_val(&e)); +} + +#[test] +#[should_panic(expected = "Decimal must not be greater than 18")] +fn decimal_is_over_eighteen() { + let e = Env::default(); + let admin = Address::generate(&e); + let token = TokenClient::new(&e, &e.register_contract(None, Token {})); + token.initialize(&admin, &19, &"name".into_val(&e), &"symbol".into_val(&e)); +} + +#[test] +fn test_zero_allowance() { + // Here we test that transfer_from with a 0 amount does not create an empty allowance + let e = Env::default(); + e.mock_all_auths(); + + let admin = Address::generate(&e); + let spender = Address::generate(&e); + let from = Address::generate(&e); + let token = create_token(&e, &admin); + + token.transfer_from(&spender, &from, &spender, &0); + assert!(token.get_allowance(&from, &spender).is_none()); +}