diff --git a/crates/dojo-defi/src/constant_product_market.cairo b/crates/dojo-defi/src/constant_product_market.cairo new file mode 100644 index 0000000000..98d784c920 --- /dev/null +++ b/crates/dojo-defi/src/constant_product_market.cairo @@ -0,0 +1,2 @@ +mod components; +mod systems; diff --git a/crates/dojo-defi/src/constant_product_market/components.cairo b/crates/dojo-defi/src/constant_product_market/components.cairo new file mode 100644 index 0000000000..2fb23a0caf --- /dev/null +++ b/crates/dojo-defi/src/constant_product_market/components.cairo @@ -0,0 +1,79 @@ +use traits::Into; +use traits::TryInto; +use option::OptionTrait; + +const SCALING_FACTOR: u128 = 10000_u128; + +#[derive(Component)] +struct Cash { + amount: u128, +} + +#[derive(Component)] +struct Item { + quantity: usize, +} + +#[derive(Component)] +struct Market { + cash_amount: u128, + item_quantity: usize, +} + +trait MarketTrait { + fn buy(self: @Market, quantity: usize) -> u128; + fn sell(self: @Market, quantity: usize) -> u128; +} + +impl MarketImpl of MarketTrait { + fn buy(self: @Market, quantity: usize) -> u128 { + assert(quantity < *self.item_quantity, 'not enough liquidity'); + let (quantity, available, cash) = normalize(quantity, self); + let k = cash * available; + let cost = (k / (available - quantity)) - cash; + cost + } + + fn sell(self: @Market, quantity: usize) -> u128 { + let (quantity, available, cash) = normalize(quantity, self); + let k = cash * available; + let payout = cash - (k / (available + quantity)); + payout + } +} + +fn normalize(quantity: usize, market: @Market) -> (u128, u128, u128) { + let quantity: u128 = quantity.into().try_into().unwrap() * SCALING_FACTOR; + let available: u128 = (*market.item_quantity).into().try_into().unwrap() * SCALING_FACTOR; + (quantity, available, *market.cash_amount) +} + + +#[test] +#[should_panic(expected: ('not enough liquidity', ))] +fn test_not_enough_quantity() { + let market = Market { + cash_amount: SCALING_FACTOR * 1_u128, item_quantity: 1_usize + }; // pool 1:1 + let cost = market.buy(10_usize); +} + +#[test] +#[available_gas(100000)] +fn test_market_buy() { + let market = Market { + cash_amount: SCALING_FACTOR * 1_u128, item_quantity: 10_usize + }; // pool 1:10 + let cost = market.buy(5_usize); + assert(cost == SCALING_FACTOR * 1_u128, 'wrong cost'); +} + +#[test] +#[available_gas(100000)] +fn test_market_sell() { + let market = Market { + cash_amount: SCALING_FACTOR * 1_u128, item_quantity: 10_usize + }; // pool 1:10 + let payout = market.sell(5_usize); + assert(payout == 3334_u128, 'wrong payout'); +} diff --git a/crates/dojo-defi/src/constant_product_market/systems.cairo b/crates/dojo-defi/src/constant_product_market/systems.cairo new file mode 100644 index 0000000000..92fa481a17 --- /dev/null +++ b/crates/dojo-defi/src/constant_product_market/systems.cairo @@ -0,0 +1,87 @@ +#[system] +mod Buy { + use traits::Into; + use array::ArrayTrait; + use dojo_defi::constant_product_market::components::Item; + use dojo_defi::constant_product_market::components::Cash; + use dojo_defi::constant_product_market::components::Market; + use dojo_defi::constant_product_market::components::MarketTrait; + + fn execute(game_id: felt252, item_id: felt252, quantity: usize) { + let player: felt252 = starknet::get_caller_address().into(); + + let cash_sk: Query = (game_id, (player)).into(); + let player_cash = commands::::entity(cash_sk); + + let market_sk: Query = (game_id, (item_id)).into(); + let market = commands::::entity(market_sk); + + let cost = market.buy(quantity); + assert(cost < player_cash.amount, 'not enough cash'); + + // update market + commands::set_entity( + market_sk, + (Market { + cash_amount: market.cash_amount + cost, + item_quantity: market.item_quantity - quantity, + }) + ); + + // update player cash + commands::set_entity(cash_sk, (Cash { amount: player_cash.amount - cost })); + + // update player item + let item_sk: Query = (game_id, (player, item_id)).into(); + let maybe_item = commands::::try_entity(item_sk); + let player_quantity = match maybe_item { + Option::Some(item) => item.quantity + quantity, + Option::None(_) => quantity, + }; + commands::set_entity(item_sk, (Item { quantity: player_quantity })); + } +} + +#[system] +mod Sell { + use traits::Into; + use array::ArrayTrait; + use dojo_defi::constant_product_market::components::Item; + use dojo_defi::constant_product_market::components::Cash; + use dojo_defi::constant_product_market::components::Market; + use dojo_defi::constant_product_market::components::MarketTrait; + + fn execute(game_id: felt252, item_id: felt252, quantity: usize) { + let player: felt252 = starknet::get_caller_address().into(); + + let item_sk: Query = (game_id, (player, item_id)).into(); + let maybe_item = commands::::try_entity(item_sk); + let player_quantity = match maybe_item { + Option::Some(item) => item.quantity, + Option::None(_) => 0_u32, + }; + assert(player_quantity >= quantity, 'not enough items'); + + let cash_sk: Query = (game_id, (player)).into(); + let player_cash = commands::::entity(cash_sk); + + let market_sk: Query = (game_id, (item_id)).into(); + let market = commands::::entity(market_sk); + let payout = market.sell(quantity); + + // update market + commands::set_entity( + market_sk, + (Market { + cash_amount: market.cash_amount - payout, + item_quantity: market.item_quantity + quantity + }) + ); + + // update player cash + commands::set_entity(cash_sk, (Cash { amount: player_cash.amount + payout })); + + // update player item + commands::set_entity(item_sk, (Item { quantity: player_quantity - quantity })); + } +} \ No newline at end of file diff --git a/crates/dojo-defi/src/lib.cairo b/crates/dojo-defi/src/lib.cairo index e69de29bb2..f9c2c219fb 100644 --- a/crates/dojo-defi/src/lib.cairo +++ b/crates/dojo-defi/src/lib.cairo @@ -0,0 +1 @@ +mod constant_product_market;