From 7bc1af117c538d5c7c1a301007ac64850cc45f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Obrok?= Date: Fri, 4 Nov 2022 17:54:22 +0100 Subject: [PATCH 1/6] Add simple_dex to deploy.sh --- contracts/scripts/deploy.sh | 73 ++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/contracts/scripts/deploy.sh b/contracts/scripts/deploy.sh index ea59c266fb..ef8b046726 100755 --- a/contracts/scripts/deploy.sh +++ b/contracts/scripts/deploy.sh @@ -159,6 +159,30 @@ function deploy_marketplace { eval "$__resultvar='$contract_address'" } +function deploy_simple_dex { + local __resultvar=$1 + + # --- CREATE AN INSTANCE OF THE CONTRACT + + cd "$CONTRACTS_PATH"/simple_dex + + local contract_address + contract_address=$(cargo contract instantiate --url "$NODE" --constructor new --suri "$AUTHORITY_SEED" --skip-confirm) + contract_address=$(echo "$contract_address" | grep Contract | tail -1 | cut -c 14-) + + echo "Simple dex contract instance address: $contract_address" + + # --- GRANT PRIVILEGES ON THE CONTRACT + + cd "$CONTRACTS_PATH"/access_control + + cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$AUTHORITY" 'Owner('"$contract_address"')' --suri "$AUTHORITY_SEED" --skip-confirm + cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$AUTHORITY" 'Admin('"$contract_address"')' --suri "$AUTHORITY_SEED" --skip-confirm + cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$AUTHORITY" 'LiquidityProvider('"$contract_address"')' --suri "$AUTHORITY_SEED" --skip-confirm + + eval "$__resultvar='$contract_address'" +} + function link_bytecode() { local contract=$1 local placeholder=$2 @@ -190,6 +214,9 @@ cargo contract build --release cd "$CONTRACTS_PATH"/marketplace cargo contract build --release +cd "$CONTRACTS_PATH"/simple_dex +cargo contract build --release + # --- DEPLOY ACCESS CONTROL CONTRACT cd "$CONTRACTS_PATH"/access_control @@ -209,6 +236,7 @@ upload_contract TICKET_TOKEN_CODE_HASH ticket_token upload_contract GAME_TOKEN_CODE_HASH game_token upload_contract BUTTON_CODE_HASH button upload_contract MARKETPLACE_CODE_HASH marketplace +upload_contract SIMPLE_DEX_CODE_HASH simple_dex start=$(date +%s.%N) @@ -245,6 +273,9 @@ deploy_game_token THE_PRESSIAH_COMETH_TOKEN Lono LON $salt deploy_marketplace THE_PRESSIAH_COMETH_MARKETPLACE "$MARKETPLACE_CODE_HASH" the_pressiah_cometh "$salt" "$THE_PRESSIAH_COMETH_TICKET" "$THE_PRESSIAH_COMETH_TOKEN" deploy_button_game THE_PRESSIAH_COMETH ThePressiahCometh "$THE_PRESSIAH_COMETH_TICKET" "$THE_PRESSIAH_COMETH_TOKEN" "$THE_PRESSIAH_COMETH_MARKETPLACE" "$salt" +echo "Simple Dex" +deploy_simple_dex SIMPLE_DEX + # spit adresses to a JSON file cd "$CONTRACTS_PATH" @@ -266,24 +297,30 @@ jq -n --arg early_bird_special "$EARLY_BIRD_SPECIAL" \ --arg marketplace_code_hash "$MARKETPLACE_CODE_HASH" \ --arg access_control "$ACCESS_CONTROL" \ --arg access_control_code_hash "$ACCESS_CONTROL_CODE_HASH" \ - '{early_bird_special: $early_bird_special, - early_bird_special_marketplace: $early_bird_special_marketplace, - early_bird_special_ticket: $early_bird_special_ticket, - early_bird_special_token: $early_bird_special_token, - back_to_the_future: $back_to_the_future, - back_to_the_future_ticket: $back_to_the_future_ticket, - back_to_the_future_token: $back_to_the_future_token, - back_to_the_future_marketplace: $back_to_the_future_marketplace, - the_pressiah_cometh: $the_pressiah_cometh, - the_pressiah_cometh_ticket: $the_pressiah_cometh_ticket, - the_pressiah_cometh_token: $the_pressiah_cometh_token, - the_pressiah_cometh_marketplace: $the_pressiah_cometh_marketplace, - access_control: $access_control, - button_code_hash: $button_code_hash, - ticket_token_code_hash: $ticket_token_code_hash, - game_token_code_hash: $game_token_code_hash, - marketplace_code_hash: $marketplace_code_hash, - access_control_code_hash: $access_control_code_hash}' > addresses.json + --arg simple_dex "$SIMPLE_DEX" \ + --arg simple_dex_code_hash "$SIMPLE_DEX_CODE_HASH" \ + '{ + early_bird_special: $early_bird_special, + early_bird_special_marketplace: $early_bird_special_marketplace, + early_bird_special_ticket: $early_bird_special_ticket, + early_bird_special_token: $early_bird_special_token, + back_to_the_future: $back_to_the_future, + back_to_the_future_ticket: $back_to_the_future_ticket, + back_to_the_future_token: $back_to_the_future_token, + back_to_the_future_marketplace: $back_to_the_future_marketplace, + the_pressiah_cometh: $the_pressiah_cometh, + the_pressiah_cometh_ticket: $the_pressiah_cometh_ticket, + the_pressiah_cometh_token: $the_pressiah_cometh_token, + the_pressiah_cometh_marketplace: $the_pressiah_cometh_marketplace, + access_control: $access_control, + simple_dex: $simple_dex, + button_code_hash: $button_code_hash, + ticket_token_code_hash: $ticket_token_code_hash, + game_token_code_hash: $game_token_code_hash, + marketplace_code_hash: $marketplace_code_hash, + access_control_code_hash: $access_control_code_hash, + simple_dex_code_hash: $simple_dex_code_hash + }' > addresses.json end=`date +%s.%N` echo "Time elapsed: $( echo "$end - $start" | bc -l )" From 52aa5dce57e71f7d5d196724ab0273c897c2e861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Obrok?= Date: Wed, 9 Nov 2022 12:25:59 +0100 Subject: [PATCH 2/6] Expose out_given_in in simple_dex --- contracts/simple_dex/lib.rs | 95 +++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/contracts/simple_dex/lib.rs b/contracts/simple_dex/lib.rs index b058994c80..71748d217e 100644 --- a/contracts/simple_dex/lib.rs +++ b/contracts/simple_dex/lib.rs @@ -162,19 +162,11 @@ mod simple_dex { return Err(DexError::InsufficientAllowanceOf(token_in)); } - let balance_token_in = self.balance_of(token_in, this)?; - let balance_token_out = self.balance_of(token_out, this)?; - - if balance_token_out < min_amount_token_out { - // throw early if we cannot support this swap anyway due to liquidity being too low - return Err(DexError::NotEnoughLiquidityOf(token_out)); - } - - let amount_token_out = Self::out_given_in( + let amount_token_out = self.out_given_in( + token_in, + token_out, amount_token_in, - balance_token_in, - balance_token_out, - self.swap_fee_percentage, + Some(min_amount_token_out), )?; if amount_token_out < min_amount_token_out { @@ -283,7 +275,7 @@ mod simple_dex { /// Returns current value of the swap_fee_percentage parameter #[ink(message)] - pub fn swap_fee_percentage(&mut self) -> Balance { + pub fn swap_fee_percentage(&self) -> Balance { self.swap_fee_percentage } @@ -365,6 +357,50 @@ mod simple_dex { .map_err(|why| DexError::InkEnv(format!("Can't retrieve own code hash: {:?}", why))) } + /// Swap trade output given a curve with equal token weights + /// + /// swap_fee_percentage (integer) is a percentage of the trade that goes towards the pool + /// B_0 - (100 * B_0 * B_i) / (100 * (B_i + A_i) -A_i * fee) + #[ink(message)] + pub fn out_given_in( + &self, + token_in: AccountId, + token_out: AccountId, + amount_token_in: Balance, + min_amount_token_out: Option, + ) -> Result { + let this = self.env().account_id(); + let balance_token_in = self.balance_of(token_in, this)?; + let balance_token_out = self.balance_of(token_out, this)?; + + if min_amount_token_out.map_or(false, |x| balance_token_out < x) { + // throw early if we cannot support this swap anyway due to liquidity being too low + return Err(DexError::NotEnoughLiquidityOf(token_out)); + } + + let op0 = amount_token_in + .checked_mul(self.swap_fee_percentage) + .ok_or(DexError::Arithmethic)?; + + let op1 = balance_token_in + .checked_add(amount_token_in) + .and_then(|result| result.checked_mul(100)) + .ok_or(DexError::Arithmethic)?; + + let op2 = op1.checked_sub(op0).ok_or(DexError::Arithmethic)?; + + let op3 = balance_token_in + .checked_mul(balance_token_out) + .and_then(|result| result.checked_mul(100)) + .ok_or(DexError::Arithmethic)?; + + let op4 = op3.checked_div(op2).ok_or(DexError::Arithmethic)?; + + balance_token_out + .checked_sub(op4) + .ok_or(DexError::Arithmethic) + } + fn new_init(&mut self) { self.access_control = AccountId::from(ACCESS_CONTROL_PUBKEY); self.swap_fee_percentage = 0; @@ -442,39 +478,6 @@ mod simple_dex { .fire() } - /// Swap trade output given a curve with equal token weights - /// - /// swap_fee_percentage (integer) is a percentage of the trade that goes towards the pool - /// B_0 - (100 * B_0 * B_i) / (100 * (B_i + A_i) -A_i * fee) - fn out_given_in( - amount_token_in: Balance, - balance_token_in: Balance, - balance_token_out: Balance, - swap_fee_percentage: u128, - ) -> Result { - let op0 = amount_token_in - .checked_mul(swap_fee_percentage) - .ok_or(DexError::Arithmethic)?; - - let op1 = balance_token_in - .checked_add(amount_token_in) - .and_then(|result| result.checked_mul(100)) - .ok_or(DexError::Arithmethic)?; - - let op2 = op1.checked_sub(op0).ok_or(DexError::Arithmethic)?; - - let op3 = balance_token_in - .checked_mul(balance_token_out) - .and_then(|result| result.checked_mul(100)) - .ok_or(DexError::Arithmethic)?; - - let op4 = op3.checked_div(op2).ok_or(DexError::Arithmethic)?; - - balance_token_out - .checked_sub(op4) - .ok_or(DexError::Arithmethic) - } - fn access_control_error_handler(role: Role) -> DexError { DexError::MissingRole(role) } From ca84acf8cd3cbd157a593d063486f941a2ecb3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Obrok?= Date: Wed, 9 Nov 2022 12:32:04 +0100 Subject: [PATCH 3/6] Add e2e test for simple_dex --- .../src/contract/convertible_value.rs | 36 ++++- aleph-client/src/contract/mod.rs | 14 +- contracts/scripts/test.sh | 6 +- contracts/simple_dex/Cargo.lock | 74 +++++++++-- e2e-tests/src/cases.rs | 3 +- e2e-tests/src/config.rs | 8 ++ e2e-tests/src/test/button_game/contracts.rs | 123 +++++++++++++++++- e2e-tests/src/test/button_game/helpers.rs | 83 +++++++++++- e2e-tests/src/test/button_game/mod.rs | 107 ++++++++++++++- e2e-tests/src/test/mod.rs | 3 +- 10 files changed, 428 insertions(+), 29 deletions(-) diff --git a/aleph-client/src/contract/convertible_value.rs b/aleph-client/src/contract/convertible_value.rs index 7cbcd58bbe..23efd041d0 100644 --- a/aleph-client/src/contract/convertible_value.rs +++ b/aleph-client/src/contract/convertible_value.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use contract_transcode::Value; use sp_core::crypto::Ss58Codec; @@ -71,3 +71,37 @@ impl TryFrom for AccountId { } } } + +impl TryFrom for Result +where + ConvertibleValue: TryInto, +{ + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result, Self::Error> { + match &value.0 { + Value::Tuple(tuple) => match tuple.ident() { + Some(x) if x == "Ok" => { + if tuple.values().count() == 1 { + let item = + ConvertibleValue(tuple.values().next().unwrap().clone()).try_into()?; + return Ok(Ok(item)); + } else { + bail!("Unexpected number of elements in Ok variant: {:?}", &value) + } + } + Some(x) if x == "Err" => { + if tuple.values().count() == 1 { + return Ok(Err(anyhow!(value.to_string()))); + } else { + bail!("Unexpected number of elements in Err variant: {:?}", &value) + } + } + _ => (), + }, + _ => (), + }; + + bail!("Expected {:?} to be an Ok(_) or Err(_) tuple.", value) + } +} diff --git a/aleph-client/src/contract/mod.rs b/aleph-client/src/contract/mod.rs index 4dd3cbdfe5..e6adce4ee1 100644 --- a/aleph-client/src/contract/mod.rs +++ b/aleph-client/src/contract/mod.rs @@ -98,15 +98,15 @@ impl ContractInstance { conn: &C, message: &str, ) -> Result { - self.contract_read(conn, message, &[]) + self.contract_read::(conn, message, &[]) } /// Reads the value of a read-only call via RPC. - pub fn contract_read( + pub fn contract_read + Debug>( &self, conn: &C, message: &str, - args: &[&str], + args: &[S], ) -> Result { let payload = self.encode(message, args)?; let request = self.contract_read_request(&payload); @@ -123,15 +123,15 @@ impl ContractInstance { /// Executes a 0-argument contract call. pub fn contract_exec0(&self, conn: &SignedConnection, message: &str) -> Result<()> { - self.contract_exec(conn, message, &[]) + self.contract_exec::(conn, message, &[]) } /// Executes a contract call. - pub fn contract_exec( + pub fn contract_exec + Debug>( &self, conn: &SignedConnection, message: &str, - args: &[&str], + args: &[S], ) -> Result<()> { let data = self.encode(message, args)?; let xt = compose_extrinsic!( @@ -166,7 +166,7 @@ impl ContractInstance { }) } - fn encode(&self, message: &str, args: &[&str]) -> Result> { + fn encode + Debug>(&self, message: &str, args: &[S]) -> Result> { ContractMessageTranscoder::new(&self.ink_project).encode(message, args) } diff --git a/contracts/scripts/test.sh b/contracts/scripts/test.sh index 8bfc27de82..96b7061d0f 100755 --- a/contracts/scripts/test.sh +++ b/contracts/scripts/test.sh @@ -7,10 +7,12 @@ CONTRACTS_PATH=$(pwd)/contracts EARLY_BIRD_SPECIAL=$(jq --raw-output ".early_bird_special" < "$CONTRACTS_PATH"/addresses.json) THE_PRESSIAH_COMETH=$(jq --raw-output ".the_pressiah_cometh" < "$CONTRACTS_PATH"/addresses.json) BACK_TO_THE_FUTURE=$(jq --raw-output ".back_to_the_future" < "$CONTRACTS_PATH"/addresses.json) +SIMPLE_DEX=$(jq --raw-output ".simple_dex" < "$CONTRACTS_PATH"/addresses.json) pushd "$E2E_PATH" RUST_LOG="aleph_e2e_client=info" cargo run --release -- \ + --test-cases simple_dex \ --test-cases marketplace \ --test-cases button_game_reset \ --test-cases early_bird_special \ @@ -19,9 +21,11 @@ RUST_LOG="aleph_e2e_client=info" cargo run --release -- \ --early-bird-special "$EARLY_BIRD_SPECIAL" \ --the-pressiah-cometh "$THE_PRESSIAH_COMETH" \ --back-to-the-future "$BACK_TO_THE_FUTURE" \ + --simple-dex "$SIMPLE_DEX" \ --button-game-metadata ../contracts/button/target/ink/metadata.json \ --ticket-token-metadata ../contracts/ticket_token/target/ink/metadata.json \ --reward-token-metadata ../contracts/game_token/target/ink/metadata.json \ - --marketplace-metadata ../contracts/marketplace/target/ink/metadata.json + --marketplace-metadata ../contracts/marketplace/target/ink/metadata.json \ + --simple-dex-metadata ../contracts/simple_dex/target/ink/metadata.json exit $? diff --git a/contracts/simple_dex/Cargo.lock b/contracts/simple_dex/Cargo.lock index 0dd7a6f029..a755d6ca20 100644 --- a/contracts/simple_dex/Cargo.lock +++ b/contracts/simple_dex/Cargo.lock @@ -520,6 +520,45 @@ dependencies = [ "autocfg", ] +[[package]] +name = "obce" +version = "0.1.0" +source = "git+https://github.com/Supercolony-net/obce?branch=polkadot-v0.9.29#9843e2a3e91e7c29a9b21dde7ead3299e9d3c0fb" +dependencies = [ + "ink_engine", + "ink_env", + "ink_lang", + "ink_metadata", + "ink_prelude", + "ink_primitives", + "ink_storage", + "obce-macro", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "obce-codegen" +version = "0.1.0" +source = "git+https://github.com/Supercolony-net/obce?branch=polkadot-v0.9.29#9843e2a3e91e7c29a9b21dde7ead3299e9d3c0fb" +dependencies = [ + "blake2 0.10.4", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "obce-macro" +version = "0.1.0" +source = "git+https://github.com/Supercolony-net/obce?branch=polkadot-v0.9.29#9843e2a3e91e7c29a9b21dde7ead3299e9d3c0fb" +dependencies = [ + "obce-codegen", + "proc-macro2", + "syn", + "synstructure", +] + [[package]] name = "once_cell" version = "1.14.0" @@ -534,8 +573,8 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openbrush" -version = "2.1.0" -source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=8a20f95#8a20f951feaf8c6ea72c4bf113bf115bad9f158f" +version = "2.2.0" +source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=15e6366#15e63664d87124f4cc0e7ba55b21a71222983677" dependencies = [ "ink_env", "ink_lang", @@ -551,8 +590,8 @@ dependencies = [ [[package]] name = "openbrush_contracts" -version = "2.1.0" -source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=8a20f95#8a20f951feaf8c6ea72c4bf113bf115bad9f158f" +version = "2.2.0" +source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=15e6366#15e63664d87124f4cc0e7ba55b21a71222983677" dependencies = [ "ink_env", "ink_lang", @@ -561,19 +600,21 @@ dependencies = [ "ink_primitives", "ink_storage", "openbrush_lang", + "pallet-assets-chain-extension", "parity-scale-codec", "scale-info", ] [[package]] name = "openbrush_lang" -version = "2.1.0" -source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=8a20f95#8a20f951feaf8c6ea72c4bf113bf115bad9f158f" +version = "2.2.0" +source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=15e6366#15e63664d87124f4cc0e7ba55b21a71222983677" dependencies = [ "const_format", "ink_env", "ink_lang", "ink_metadata", + "ink_prelude", "ink_primitives", "ink_storage", "openbrush_lang_macro", @@ -584,8 +625,8 @@ dependencies = [ [[package]] name = "openbrush_lang_codegen" -version = "2.1.0" -source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=8a20f95#8a20f951feaf8c6ea72c4bf113bf115bad9f158f" +version = "2.2.0" +source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=15e6366#15e63664d87124f4cc0e7ba55b21a71222983677" dependencies = [ "blake2 0.9.2", "cargo_metadata", @@ -603,8 +644,8 @@ dependencies = [ [[package]] name = "openbrush_lang_macro" -version = "2.1.0" -source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=8a20f95#8a20f951feaf8c6ea72c4bf113bf115bad9f158f" +version = "2.2.0" +source = "git+https://github.com/Supercolony-net/openbrush-contracts.git?rev=15e6366#15e63664d87124f4cc0e7ba55b21a71222983677" dependencies = [ "openbrush_lang_codegen", "proc-macro2", @@ -612,6 +653,19 @@ dependencies = [ "synstructure", ] +[[package]] +name = "pallet-assets-chain-extension" +version = "0.1.1" +source = "git+https://github.com/Supercolony-net/pallet-assets-chain-extension#83ff37cb0d19d15a881ecab2c922596039c8358d" +dependencies = [ + "ink_metadata", + "ink_primitives", + "ink_storage", + "obce", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "parity-scale-codec" version = "3.1.5" diff --git a/e2e-tests/src/cases.rs b/e2e-tests/src/cases.rs index eaf1f52c35..f77809e166 100644 --- a/e2e-tests/src/cases.rs +++ b/e2e-tests/src/cases.rs @@ -12,7 +12,7 @@ use crate::{ fee_calculation as test_fee_calculation, finalization as test_finalization, force_new_era as test_force_new_era, marketplace as test_marketplace, points_basic as test_points_basic, points_stake_change as test_points_stake_change, - staking_era_payouts as test_staking_era_payouts, + simple_dex as test_simple_dex, staking_era_payouts as test_staking_era_payouts, staking_new_validator as test_staking_new_validator, the_pressiah_cometh as test_the_pressiah_cometh, token_transfer as test_token_transfer, treasury_access as test_treasury_access, validators_rotate as test_validators_rotate, @@ -63,5 +63,6 @@ pub fn possible_test_cases() -> PossibleTestCases { ("back_to_the_future", test_back_to_the_future as TestCase), ("the_pressiah_cometh", test_the_pressiah_cometh as TestCase), ("marketplace", test_marketplace as TestCase), + ("simple_dex", test_simple_dex as TestCase), ] } diff --git a/e2e-tests/src/config.rs b/e2e-tests/src/config.rs index f69aae60e4..aacd02f807 100644 --- a/e2e-tests/src/config.rs +++ b/e2e-tests/src/config.rs @@ -80,6 +80,10 @@ pub struct TestCaseParams { #[clap(long)] pub the_pressiah_cometh: Option, + /// Address of the simple dex contract. + #[clap(long)] + pub simple_dex: Option, + /// Path to the button game metadata file. Only used by button tests. #[clap(long)] pub button_game_metadata: Option, @@ -95,4 +99,8 @@ pub struct TestCaseParams { /// Path to the marketplace metadata file. Only used by button tests. #[clap(long)] pub marketplace_metadata: Option, + + /// Path to the simple_dex metadata file. Only used by button tests. + #[clap(long)] + pub simple_dex_metadata: Option, } diff --git a/e2e-tests/src/test/button_game/contracts.rs b/e2e-tests/src/test/button_game/contracts.rs index 8bcde90b04..8dc6188840 100644 --- a/e2e-tests/src/test/button_game/contracts.rs +++ b/e2e-tests/src/test/button_game/contracts.rs @@ -6,6 +6,121 @@ use sp_core::crypto::{AccountId32 as AccountId, Ss58Codec}; use crate::Config; +/// A wrapper around the simple dex contract. +/// +/// The methods on this type match contract methods. +#[derive(Debug)] +pub(super) struct SimpleDexInstance { + contract: ContractInstance, +} + +impl<'a> From<&'a SimpleDexInstance> for &'a ContractInstance { + fn from(dex: &'a SimpleDexInstance) -> Self { + &dex.contract + } +} + +impl<'a> From<&'a SimpleDexInstance> for AccountId { + fn from(dex: &'a SimpleDexInstance) -> Self { + dex.contract.address().clone() + } +} + +impl SimpleDexInstance { + pub fn new(config: &Config) -> Result { + let dex_address = config + .test_case_params + .simple_dex + .clone() + .context("Simple dex address not set.")?; + let dex_address = AccountId::from_string(&dex_address)?; + let metadata_path = config + .test_case_params + .simple_dex_metadata + .clone() + .context("Simple dex metadata not set")?; + + Ok(Self { + contract: ContractInstance::new(dex_address, &metadata_path)?, + }) + } + + pub fn add_swap_pair( + &self, + conn: &SignedConnection, + from: AccountId, + to: AccountId, + ) -> Result<()> { + self.contract + .contract_exec(conn, "add_swap_pair", &[&from.to_string(), &to.to_string()]) + } + + pub fn deposit( + &self, + conn: &SignedConnection, + amounts: &[(&PSP22TokenInstance, Balance)], + ) -> Result<()> { + let deposits = amounts + .iter() + .map(|(token, amount)| { + let address: AccountId = (*token).try_into()?; + Ok(format!("deposits ({:}, {:})", address, amount)) + }) + .collect::>>()?; + + self.contract + .contract_exec(conn, "deposit", &[format!("[{:}]", deposits.join(","))]) + } + + pub fn out_given_in( + &self, + conn: &C, + token_in: &PSP22TokenInstance, + token_out: &PSP22TokenInstance, + amount_token_in: Balance, + min_amount_token_out: Option, + ) -> Result { + let token_in: AccountId = token_in.into(); + let token_out: AccountId = token_out.into(); + + self.contract + .contract_read( + conn, + "out_given_in", + &[ + token_in.to_string(), + token_out.to_string(), + amount_token_in.to_string(), + min_amount_token_out.map_or("None".to_string(), |x| format!("Some({:})", x)), + ], + )? + .try_into()? + } + + pub fn swap( + &self, + conn: &SignedConnection, + token_in: &PSP22TokenInstance, + amount_token_in: Balance, + token_out: &PSP22TokenInstance, + min_amount_token_out: Balance, + ) -> Result<()> { + let token_in: AccountId = token_in.into(); + let token_out: AccountId = token_out.into(); + + self.contract.contract_exec( + conn, + "swap", + &[ + token_in.to_string(), + token_out.to_string(), + amount_token_in.to_string(), + min_amount_token_out.to_string(), + ], + ) + } +} + /// A wrapper around a button game contract. /// /// The methods on this type match contract methods. @@ -99,7 +214,7 @@ impl PSP22TokenInstance { self.contract.contract_exec( conn, "PSP22::transfer", - &[to.to_string().as_str(), amount.to_string().as_str(), "0x00"], + &[to.to_string(), amount.to_string(), "0x00".to_string()], ) } @@ -107,7 +222,7 @@ impl PSP22TokenInstance { self.contract.contract_exec( conn, "PSP22Mintable::mint", - &[to.to_string().as_str(), amount.to_string().as_str()], + &[to.to_string(), amount.to_string()], ) } @@ -120,13 +235,13 @@ impl PSP22TokenInstance { self.contract.contract_exec( conn, "PSP22::approve", - &[spender.to_string().as_str(), value.to_string().as_str()], + &[spender.to_string(), value.to_string()], ) } pub fn balance_of(&self, conn: &Connection, account: &AccountId) -> Result { self.contract - .contract_read(conn, "PSP22::balance_of", &[account.to_string().as_str()])? + .contract_read(conn, "PSP22::balance_of", &[account.to_string()])? .try_into() } } diff --git a/e2e-tests/src/test/button_game/helpers.rs b/e2e-tests/src/test/button_game/helpers.rs index 2ca2e0ae14..638c25f33c 100644 --- a/e2e-tests/src/test/button_game/helpers.rs +++ b/e2e-tests/src/test/button_game/helpers.rs @@ -19,8 +19,10 @@ use log::{info, warn}; use rand::Rng; use sp_core::Pair; -use super::contracts::{ButtonInstance, PSP22TokenInstance}; -use crate::{test::button_game::contracts::MarketplaceInstance, Config}; +use super::contracts::{ + ButtonInstance, MarketplaceInstance, PSP22TokenInstance, SimpleDexInstance, +}; +use crate::Config; /// A wrapper around a KeyPair for purposes of converting to an account id in tests. pub struct KeyPairWrapper(KeyPair); @@ -103,6 +105,11 @@ pub fn alephs(basic_unit_amount: Balance) -> Balance { basic_unit_amount * 1_000_000_000_000 } +/// Returns the given number multiplied by 10^6. +pub fn mega(x: Balance) -> Balance { + x * 1_000_000 +} + pub(super) struct ButtonTestContext { pub button: Arc, pub ticket_token: Arc, @@ -118,6 +125,78 @@ pub(super) struct ButtonTestContext { pub player: KeyPairWrapper, } +pub(super) struct DexTestContext { + pub conn: Connection, + /// An authority with the power to mint tokens and manage the dex. + pub authority: KeyPairWrapper, + /// A random account with some money for fees. + pub account: KeyPairWrapper, + pub dex: Arc, + pub token1: Arc, + pub token2: Arc, + pub token3: Arc, + /// A [BufferedReceiver] preconfigured to listen for events of `dex`, `token1`, `token2`, and `token3`. + pub events: BufferedReceiver>, +} + +pub(super) fn setup_dex_test(config: &Config) -> Result { + let conn = config.get_first_signed_connection().as_connection(); + let authority = KeyPairWrapper(aleph_client::keypair_from_string(&config.sudo_seed)); + let account = random_account(); + + let dex = Arc::new(SimpleDexInstance::new(config)?); + let token1 = + reward_token_for_button(config, &conn, &config.test_case_params.early_bird_special)?; + let token2 = + reward_token_for_button(config, &conn, &config.test_case_params.the_pressiah_cometh)?; + let token3 = + reward_token_for_button(config, &conn, &config.test_case_params.back_to_the_future)?; + + let c1 = dex.clone(); + let c2 = token1.clone(); + let c3 = token2.clone(); + let c4 = token3.clone(); + + let subscription = subscribe_events(&conn)?; + let (events_tx, events_rx) = channel(); + + thread::spawn(move || { + let contract_metadata = vec![ + c1.as_ref().into(), + c2.as_ref().into(), + c3.as_ref().into(), + c4.as_ref().into(), + ]; + + listen_contract_events(subscription, &contract_metadata, None, |event| { + let _ = events_tx.send(event); + }); + }); + + let events = BufferedReceiver::new(events_rx, Duration::from_secs(3)); + transfer(&conn, &authority, &account, alephs(100)); + + Ok(DexTestContext { + conn, + authority, + account, + dex, + token1, + token2, + token3, + events, + }) +} + +fn reward_token_for_button( + config: &Config, + conn: &Connection, + button_contract_address: &Option, +) -> Result> { + let button = ButtonInstance::new(config, button_contract_address)?; + Ok(Arc::new(reward_token(conn, &button, config)?)) +} + /// Sets up a number of objects commonly used in button game tests. pub(super) fn setup_button_test( config: &Config, diff --git a/e2e-tests/src/test/button_game/mod.rs b/e2e-tests/src/test/button_game/mod.rs index a2857c1667..ef021c24d9 100644 --- a/e2e-tests/src/test/button_game/mod.rs +++ b/e2e-tests/src/test/button_game/mod.rs @@ -5,11 +5,12 @@ use anyhow::Result; use assert2::{assert, let_assert}; use helpers::sign; use log::info; +use sp_core::Pair; use crate::{ test::button_game::helpers::{ - assert_recv, assert_recv_id, refute_recv_id, setup_button_test, wait_for_death, - ButtonTestContext, + assert_recv, assert_recv_id, mega, refute_recv_id, setup_button_test, setup_dex_test, + wait_for_death, ButtonTestContext, DexTestContext, }, Config, }; @@ -17,6 +18,108 @@ use crate::{ mod contracts; mod helpers; +/// Test trading on simple_dex. +/// +/// The scenario does the following (given 3 tokens A, B, C): +/// +/// 1. Enables A <-> B, and A -> C swaps. +/// 2. Adds (A, 2000M), (B, 5000M), (C, 10000M) of liquidity. +/// 3. Makes a swap A -> B and then B -> A for the amount of B received in the first swap. +/// 4. Checks that the price after the two swaps is the same as before (with a dust allowance of 1 for rounding). +/// 5. Checks that it's possible to make an A -> C swap, but impossible to make a C -> A swap. +pub fn simple_dex(config: &Config) -> Result<()> { + let DexTestContext { + conn, + authority, + account, + dex, + token1, + token2, + token3, + mut events, + } = setup_dex_test(config)?; + let authority_conn = &sign(&conn, &authority); + let account_conn = &sign(&conn, &account); + let token1 = token1.as_ref(); + let token2 = token2.as_ref(); + let token3 = token3.as_ref(); + let dex = dex.as_ref(); + + dex.add_swap_pair(authority_conn, token1.into(), token2.into())?; + assert_recv_id(&mut events, "SwapPairAdded"); + + dex.add_swap_pair(authority_conn, token2.into(), token1.into())?; + assert_recv_id(&mut events, "SwapPairAdded"); + + dex.add_swap_pair(authority_conn, token1.into(), token3.into())?; + assert_recv_id(&mut events, "SwapPairAdded"); + + token1.mint(authority_conn, &authority.public().into(), mega(3000))?; + token2.mint(authority_conn, &authority.public().into(), mega(5000))?; + token3.mint(authority_conn, &authority.public().into(), mega(10000))?; + + token1.approve(authority_conn, &dex.into(), mega(3000))?; + token2.approve(authority_conn, &dex.into(), mega(5000))?; + token3.approve(authority_conn, &dex.into(), mega(10000))?; + dex.deposit( + authority_conn, + &[ + (token1, mega(3000)), + (token2, mega(5000)), + (token3, mega(10000)), + ], + )?; + + let more_than_liquidity = mega(1_000_000); + assert!(dex + .out_given_in( + account_conn, + &token1, + &token2, + 100, + Some(more_than_liquidity) + ) + .is_err()); + + let initial_amount = mega(100); + token1.mint(authority_conn, &account.public().into(), initial_amount)?; + let expected_output = dex.out_given_in(account_conn, &token1, &token2, initial_amount, None)?; + assert!(expected_output > 0); + + token1.approve(account_conn, &dex.into(), initial_amount)?; + dex.swap( + account_conn, + &token1, + initial_amount, + &token2, + expected_output * 9 / 10, + )?; + assert_recv_id(&mut events, "Swapped"); + assert!(token2.balance_of(&conn, &account.public().into())? == expected_output); + + token2.approve(account_conn, &dex.into(), expected_output)?; + dex.swap(account_conn, &token2, expected_output, &token1, mega(90))?; + assert_recv_id(&mut events, "Swapped"); + + let balance_after = token1.balance_of(&conn, &account.public().into())?; + assert!(initial_amount.abs_diff(balance_after) <= 1); + assert!( + dex.out_given_in(account_conn, &token1, &token2, initial_amount, None)? + .abs_diff(expected_output) + <= 1 + ); + + token1.approve(account_conn, &dex.into(), balance_after)?; + dex.swap(account_conn, &token1, balance_after, &token3, mega(90))?; + assert_recv_id(&mut events, "Swapped"); + let balance_token3 = token3.balance_of(&conn, &account.public().into())?; + token3.approve(account_conn, &dex.into(), balance_token3)?; + dex.swap(account_conn, &token3, balance_token3, &token1, mega(90))?; + refute_recv_id(&mut events, "Swapped"); + + Ok(()) +} + /// Tests trading on the marketplace. /// /// The scenario: diff --git a/e2e-tests/src/test/mod.rs b/e2e-tests/src/test/mod.rs index 790666f615..6b8cba4d93 100644 --- a/e2e-tests/src/test/mod.rs +++ b/e2e-tests/src/test/mod.rs @@ -1,5 +1,6 @@ pub use button_game::{ - back_to_the_future, button_game_reset, early_bird_special, marketplace, the_pressiah_cometh, + back_to_the_future, button_game_reset, early_bird_special, marketplace, simple_dex, + the_pressiah_cometh, }; pub use electing_validators::authorities_are_staking; pub use era_payout::era_payouts_calculated_correctly; From e34c0064668106e489fc338989e60f89ae05053c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Obrok?= Date: Thu, 10 Nov 2022 13:50:42 +0100 Subject: [PATCH 4/6] Fix clippy --- .../src/contract/convertible_value.rs | 15 ++++++------- e2e-tests/src/test/button_game/mod.rs | 22 +++++++------------ 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/aleph-client/src/contract/convertible_value.rs b/aleph-client/src/contract/convertible_value.rs index 23efd041d0..ff27c894d6 100644 --- a/aleph-client/src/contract/convertible_value.rs +++ b/aleph-client/src/contract/convertible_value.rs @@ -79,29 +79,28 @@ where type Error = anyhow::Error; fn try_from(value: ConvertibleValue) -> Result, Self::Error> { - match &value.0 { - Value::Tuple(tuple) => match tuple.ident() { + if let Value::Tuple(tuple) = &value.0 { + match tuple.ident() { Some(x) if x == "Ok" => { if tuple.values().count() == 1 { let item = ConvertibleValue(tuple.values().next().unwrap().clone()).try_into()?; return Ok(Ok(item)); } else { - bail!("Unexpected number of elements in Ok variant: {:?}", &value) + bail!("Unexpected number of elements in Ok variant: {:?}", &value); } } Some(x) if x == "Err" => { if tuple.values().count() == 1 { return Ok(Err(anyhow!(value.to_string()))); } else { - bail!("Unexpected number of elements in Err variant: {:?}", &value) + bail!("Unexpected number of elements in Err variant: {:?}", &value); } } _ => (), - }, - _ => (), - }; + } + } - bail!("Expected {:?} to be an Ok(_) or Err(_) tuple.", value) + bail!("Expected {:?} to be an Ok(_) or Err(_) tuple.", value); } } diff --git a/e2e-tests/src/test/button_game/mod.rs b/e2e-tests/src/test/button_game/mod.rs index ef021c24d9..ef6c532e7e 100644 --- a/e2e-tests/src/test/button_game/mod.rs +++ b/e2e-tests/src/test/button_game/mod.rs @@ -72,49 +72,43 @@ pub fn simple_dex(config: &Config) -> Result<()> { let more_than_liquidity = mega(1_000_000); assert!(dex - .out_given_in( - account_conn, - &token1, - &token2, - 100, - Some(more_than_liquidity) - ) + .out_given_in(account_conn, token1, token2, 100, Some(more_than_liquidity)) .is_err()); let initial_amount = mega(100); token1.mint(authority_conn, &account.public().into(), initial_amount)?; - let expected_output = dex.out_given_in(account_conn, &token1, &token2, initial_amount, None)?; + let expected_output = dex.out_given_in(account_conn, token1, token2, initial_amount, None)?; assert!(expected_output > 0); token1.approve(account_conn, &dex.into(), initial_amount)?; dex.swap( account_conn, - &token1, + token1, initial_amount, - &token2, + token2, expected_output * 9 / 10, )?; assert_recv_id(&mut events, "Swapped"); assert!(token2.balance_of(&conn, &account.public().into())? == expected_output); token2.approve(account_conn, &dex.into(), expected_output)?; - dex.swap(account_conn, &token2, expected_output, &token1, mega(90))?; + dex.swap(account_conn, token2, expected_output, token1, mega(90))?; assert_recv_id(&mut events, "Swapped"); let balance_after = token1.balance_of(&conn, &account.public().into())?; assert!(initial_amount.abs_diff(balance_after) <= 1); assert!( - dex.out_given_in(account_conn, &token1, &token2, initial_amount, None)? + dex.out_given_in(account_conn, token1, token2, initial_amount, None)? .abs_diff(expected_output) <= 1 ); token1.approve(account_conn, &dex.into(), balance_after)?; - dex.swap(account_conn, &token1, balance_after, &token3, mega(90))?; + dex.swap(account_conn, token1, balance_after, token3, mega(90))?; assert_recv_id(&mut events, "Swapped"); let balance_token3 = token3.balance_of(&conn, &account.public().into())?; token3.approve(account_conn, &dex.into(), balance_token3)?; - dex.swap(account_conn, &token3, balance_token3, &token1, mega(90))?; + dex.swap(account_conn, token3, balance_token3, token1, mega(90))?; refute_recv_id(&mut events, "Swapped"); Ok(()) From e0020f69d025c1a60ded08a12c1cd93a7942555c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Obrok?= Date: Thu, 10 Nov 2022 16:01:13 +0100 Subject: [PATCH 5/6] Leave dust in the dex --- .github/workflows/check-excluded-packages.yml | 1 + contracts/simple_dex/Cargo.lock | 158 ++++++++++++++++++ contracts/simple_dex/Cargo.toml | 5 +- contracts/simple_dex/lib.rs | 44 ++++- 4 files changed, 205 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-excluded-packages.yml b/.github/workflows/check-excluded-packages.yml index 7aa4047f2e..05b161daa1 100644 --- a/.github/workflows/check-excluded-packages.yml +++ b/.github/workflows/check-excluded-packages.yml @@ -72,6 +72,7 @@ jobs: pushd "$p" cargo fmt --all --check cargo clippy --all-features -- --no-deps -D warnings + cargo test popd done diff --git a/contracts/simple_dex/Cargo.lock b/contracts/simple_dex/Cargo.lock index a755d6ca20..73261c0d96 100644 --- a/contracts/simple_dex/Cargo.lock +++ b/contracts/simple_dex/Cargo.lock @@ -40,6 +40,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitvec" version = "1.0.1" @@ -87,6 +108,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "camino" version = "1.1.1" @@ -217,6 +244,21 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fs2" version = "0.4.3" @@ -484,6 +526,15 @@ dependencies = [ "synstructure", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.10.3" @@ -505,6 +556,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.132" @@ -734,6 +791,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.21" @@ -779,12 +868,57 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rlibc" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc874b127765f014d792f16763a81245ab80500e2ad921ed4ee9e82481ee08fe" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.11" @@ -927,6 +1061,7 @@ dependencies = [ "ink_storage", "openbrush", "parity-scale-codec", + "proptest", "scale-info", ] @@ -971,6 +1106,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.34" @@ -1042,6 +1191,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/contracts/simple_dex/Cargo.toml b/contracts/simple_dex/Cargo.toml index 8318177cca..cfca965111 100644 --- a/contracts/simple_dex/Cargo.toml +++ b/contracts/simple_dex/Cargo.toml @@ -21,6 +21,9 @@ openbrush = { git = "https://github.com/Supercolony-net/openbrush-contracts.git" access_control = { path = "../access_control", default-features = false, features = ["ink-as-dependency"] } game_token = { path = "../game_token", default-features = false, features = ["ink-as-dependency"] } +[dev-dependencies] +proptest = "1.0" + [lib] name = "simple_dex" path = "lib.rs" @@ -47,4 +50,4 @@ std = [ ink-as-dependency = [] [profile.dev] -codegen-units = 16 \ No newline at end of file +codegen-units = 16 diff --git a/contracts/simple_dex/lib.rs b/contracts/simple_dex/lib.rs index 71748d217e..abbe8964e2 100644 --- a/contracts/simple_dex/lib.rs +++ b/contracts/simple_dex/lib.rs @@ -372,14 +372,27 @@ mod simple_dex { let this = self.env().account_id(); let balance_token_in = self.balance_of(token_in, this)?; let balance_token_out = self.balance_of(token_out, this)?; - if min_amount_token_out.map_or(false, |x| balance_token_out < x) { // throw early if we cannot support this swap anyway due to liquidity being too low return Err(DexError::NotEnoughLiquidityOf(token_out)); } + Self::_out_given_in( + amount_token_in, + balance_token_in, + balance_token_out, + self.swap_fee_percentage, + ) + } + + fn _out_given_in( + amount_token_in: Balance, + balance_token_in: Balance, + balance_token_out: Balance, + swap_fee_percentage: Balance, + ) -> Result { let op0 = amount_token_in - .checked_mul(self.swap_fee_percentage) + .checked_mul(swap_fee_percentage) .ok_or(DexError::Arithmethic)?; let op1 = balance_token_in @@ -398,6 +411,8 @@ mod simple_dex { balance_token_out .checked_sub(op4) + // If the division is not even, leave the 1 unit of dust in the exchange instead of paying it out. + .and_then(|result| result.checked_sub(if op3 % op2 > 0 { 1 } else { 0 })) .ok_or(DexError::Arithmethic) } @@ -506,4 +521,29 @@ mod simple_dex { emitter.emit_event(event); } } + + #[cfg(test)] + mod test { + use proptest::prelude::*; + + use super::*; + + proptest! { + #[test] + fn rounding_benefits_dex( + balance_token_a in 1..1000u128, + balance_token_b in 1..1000u128, + pay_token_a in 1..1000u128 + ) { + let get_token_b = + SimpleDex::_out_given_in(pay_token_a, balance_token_a, balance_token_b, 0).unwrap(); + let balance_token_a = balance_token_a + pay_token_a; + let balance_token_b = balance_token_b - get_token_b; + let get_token_a = + SimpleDex::_out_given_in(get_token_b, balance_token_b, balance_token_a, 0).unwrap(); + + assert!(get_token_a <= pay_token_a); + } + } + } } From 5c81f48a7f7859443b1dc0a1919cf5574d1fce91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Obrok?= Date: Thu, 17 Nov 2022 12:43:28 +0100 Subject: [PATCH 6/6] Vary fee percentage in dex test --- contracts/simple_dex/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/simple_dex/lib.rs b/contracts/simple_dex/lib.rs index ea9b626e3a..258212cb47 100644 --- a/contracts/simple_dex/lib.rs +++ b/contracts/simple_dex/lib.rs @@ -540,14 +540,15 @@ mod simple_dex { fn rounding_benefits_dex( balance_token_a in 1..1000u128, balance_token_b in 1..1000u128, - pay_token_a in 1..1000u128 + pay_token_a in 1..1000u128, + fee_percentage in 0..10u128 ) { let get_token_b = - SimpleDex::_out_given_in(pay_token_a, balance_token_a, balance_token_b, 0).unwrap(); + SimpleDex::_out_given_in(pay_token_a, balance_token_a, balance_token_b, fee_percentage).unwrap(); let balance_token_a = balance_token_a + pay_token_a; let balance_token_b = balance_token_b - get_token_b; let get_token_a = - SimpleDex::_out_given_in(get_token_b, balance_token_b, balance_token_a, 0).unwrap(); + SimpleDex::_out_given_in(get_token_b, balance_token_b, balance_token_a, fee_percentage).unwrap(); assert!(get_token_a <= pay_token_a); }