From 860cf4bda1535c28512825a299ed6ced0c5e2d99 Mon Sep 17 00:00:00 2001 From: yomarion Date: Mon, 11 Sep 2023 18:17:35 +0200 Subject: [PATCH 01/20] wip: contract type OK --- conversion_proxy/Cargo.toml | 1 + conversion_proxy/src/lib.rs | 103 +++++++++++++++++++----------------- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/conversion_proxy/Cargo.toml b/conversion_proxy/Cargo.toml index 009641a..73a2361 100644 --- a/conversion_proxy/Cargo.toml +++ b/conversion_proxy/Cargo.toml @@ -3,6 +3,7 @@ name = "conversion_proxy" version = "0.0.2" authors = ["Request Finance", "Request Network"] edition = "2018" +overflow-checks=true [lib] crate-type = ["cdylib", "rlib"] diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index a9004b2..febe6e0 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -16,23 +16,41 @@ const MIN_GAS: Gas = 50_000_000_000_000; const BASIC_GAS: Gas = 10_000_000_000_000; /** - * Flux oracle-related declarations + * Switchboard oracle-related declarations */ -// Return type the Flux price oracle + +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct SwitchboardDecimal { + pub mantissa: i128, + pub scale: u32, +} + #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct PriceEntry { - pub price: U128, // Last reported price - pub decimals: u16, // Amount of decimals (e.g. if 2, 100 = 1.00) - pub last_update: Timestamp, // Time of report + // pub price: U128, // Last reported price + // pub decimals: u16, // Amount of decimals (e.g. if 2, 100 = 1.00) + // pub last_update: Timestamp, // Time of report + pub result: SwitchboardDecimal, + pub num_success: u32, + pub num_error: u32, + pub round_open_timestamp: Timestamp, +} + +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct SwitchboarIx { + pub address: Vec, + pub payer: Vec, } -// Interface of the Flux price oracle -#[near_sdk::ext_contract(fpo_contract)] -trait FPOContract { - fn get_entry(pair: String, provider: AccountId) -> Promise; + +// Interface of the Switchboard price oracle +#[near_sdk::ext_contract(sb_contract)] +trait Switchboard { + fn aggregator_read(ix: SwitchboarIx) -> Promise; } + /// /// This contract /// - oracle_account_id: should be a valid FPO oracle account ID @@ -41,8 +59,9 @@ trait FPOContract { #[near_bindgen] #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct ConversionProxy { - pub oracle_account_id: AccountId, - pub provider_account_id: AccountId, + pub oracle_account: Vec, + pub payer:Vec, + // pub provider_account_id: AccountId, pub owner_id: AccountId, } @@ -112,13 +131,10 @@ impl ConversionProxy { .expect("Payment reference value error"); assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); - let get_rate = fpo_contract::get_entry( - "NEAR/".to_owned() + ¤cy, - self.provider_account_id.clone(), - &self.oracle_account_id, + let get_rate = sb_contract::aggregator_read(SwitchboarIx {address: self.oracle_account.clone(), payer: self.payer.clone()}, + &bs58::encode(self.oracle_account.clone()).into_string(), NO_DEPOSIT, - BASIC_GAS, - ); + BASIC_GAS,); let callback_gas = BASIC_GAS * 3; let process_request_payment = ext_self::rate_callback( to, @@ -137,39 +153,27 @@ impl ConversionProxy { } #[init] - pub fn new(oracle_account_id: AccountId, provider_account_id: AccountId) -> Self { + pub fn new(oracle_account: Vec) -> Self { let owner_id = env::signer_account_id(); + let payer = bs58::decode(owner_id.clone()).into_vec().expect("Could not decode owner"); Self { - oracle_account_id, - provider_account_id, + oracle_account, + payer, owner_id, } } - pub fn set_oracle_account(&mut self, oracle: ValidAccountId) { - let signer_id = env::predecessor_account_id(); - if self.owner_id == signer_id { - self.oracle_account_id = oracle.to_string(); - } else { - panic!("ERR_PERMISSION"); - } - } - - pub fn get_oracle_account(&self) -> AccountId { - return self.oracle_account_id.to_string(); - } - - pub fn set_provider_account(&mut self, oracle: ValidAccountId) { + pub fn set_oracle_account(&mut self, oracle: Vec) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.provider_account_id = oracle.to_string(); + self.oracle_account = oracle; } else { panic!("ERR_PERMISSION"); } } - pub fn get_provider_account(&self) -> AccountId { - return self.provider_account_id.to_string(); + pub fn get_oracle_account(&self) -> Vec { + return self.oracle_account.clone(); } pub fn set_owner(&mut self, owner: ValidAccountId) { @@ -255,26 +259,29 @@ impl ConversionProxy { // Check rate validity assert!( u64::from(max_rate_timespan) == 0 - || rate.last_update >= env::block_timestamp() - u64::from(max_rate_timespan), + || rate.round_open_timestamp >= env::block_timestamp() - u64::from(max_rate_timespan), "Conversion rate too old (Last updated: {})", - rate.last_update, + rate.round_open_timestamp, ); - let conversion_rate = u128::from(rate.price); - let decimals = u32::from(rate.decimals); + let conversion_rate = 0_u128.checked_add_signed(rate.result.mantissa).expect("Negative conversion rate"); + let decimals = u32::from(rate.result.scale); let main_payment = - Balance::from(amount) * ONE_NEAR * 10u128.pow(decimals) / conversion_rate / ONE_FIAT; + (Balance::from(amount) * ONE_NEAR * 10u128.pow(decimals) / conversion_rate / ONE_FIAT) as u128; let fee_payment = Balance::from(fee_amount) * ONE_NEAR * 10u128.pow(decimals) / conversion_rate / ONE_FIAT; let total_payment = main_payment + fee_payment; // Check deposit - assert!( - total_payment <= env::attached_deposit(), - "Deposit too small for payment (Supplied: {}. Demand (incl. fees): {})", - env::attached_deposit(), - total_payment - ); + if total_payment > env::attached_deposit() { + + Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); + panic!( + "Deposit too small for payment (Supplied: {}. Demand (incl. fees): {})", + env::attached_deposit(), + total_payment + ); + } let change = env::attached_deposit() - (total_payment); From 4a1fa50ab96a304904703710b4e27eff8f0d8a35 Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 12 Sep 2023 16:49:29 +0200 Subject: [PATCH 02/20] wip: unit tests and mocks --- Cargo.lock | 20 +++- Cargo.toml | 3 +- README.md | 2 +- conversion_proxy/src/lib.rs | 57 +++++++---- deploy.sh | 1 + mocks/src/fpo_oracle_mock.rs | 2 +- mocks/src/lib.rs | 1 + mocks/src/switchboard_feed_parser_mock.rs | 116 ++++++++++++++++++++++ test.sh | 2 +- 9 files changed, 175 insertions(+), 29 deletions(-) create mode 100644 mocks/src/switchboard_feed_parser_mock.rs diff --git a/Cargo.lock b/Cargo.lock index 39107b6..52b0c9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "tinyvec", +] + [[package]] name = "byte-slice-cast" version = "1.2.1" @@ -1524,7 +1533,7 @@ dependencies = [ "arrayref", "blake2", "borsh", - "bs58", + "bs58 0.4.0", "c2-chacha", "curve25519-dalek", "derive_more", @@ -1571,7 +1580,7 @@ checksum = "75ed2263518ca67a3c158c144813832fd96f48ab239494bb9d7793d315f31417" dependencies = [ "base64", "borsh", - "bs58", + "bs58 0.4.0", "byteorder", "chrono", "derive_more", @@ -1603,7 +1612,7 @@ checksum = "c2b3fb5acf3a494aed4e848446ef2d6ebb47dbe91c681105d4d1786c2ee63e52" dependencies = [ "base64", "borsh", - "bs58", + "bs58 0.4.0", "derive_more", "hex", "lazy_static", @@ -1686,7 +1695,7 @@ checksum = "c7383e242d3e07bf0951e8589d6eebd7f18bb1c1fc5fbec3fad796041a6aebd1" dependencies = [ "base64", "borsh", - "bs58", + "bs58 0.4.0", "near-primitives-core", "near-sdk-macros", "near-vm-logic", @@ -1778,7 +1787,7 @@ checksum = "e11cb28a2d07f37680efdaf860f4c9802828c44fc50c08009e7884de75d982c5" dependencies = [ "base64", "borsh", - "bs58", + "bs58 0.4.0", "byteorder", "near-primitives-core", "near-runtime-utils", @@ -2369,6 +2378,7 @@ dependencies = [ name = "request_conversion_proxy" version = "0.1.0" dependencies = [ + "bs58 0.5.0", "conversion_proxy", "fungible_conversion_proxy", "fungible_proxy", diff --git a/Cargo.toml b/Cargo.toml index 41b88c3..660eabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ near-sdk = "3.1.0" serde = "1.0.118" # near-sdk = "4.0.0-pre.4" hex = "0.4" +bs58 = "0.5.0" [dev-dependencies] near-sdk-sim = "3.2.0" @@ -28,4 +29,4 @@ panic = "abort" overflow-checks = true [workspace] -members = ["conversion_proxy", "fungible_conversion_proxy", "fungible_proxy", "mocks"] \ No newline at end of file +members = ["conversion_proxy", "fungible_conversion_proxy", "fungible_proxy", "mocks"] diff --git a/README.md b/README.md index aa93846..7019348 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ cargo test ## Integration tests ``` -# To test everything +# To test everything (unit tests, sanity checks, simulated tests) ./test.sh # To test contracts one by one: diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index febe6e0..e44de44 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -59,9 +59,9 @@ trait Switchboard { #[near_bindgen] #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct ConversionProxy { - pub oracle_account: Vec, - pub payer:Vec, - // pub provider_account_id: AccountId, + pub feed_parser: AccountId, + pub feed_address: Vec, + pub feed_payer:Vec, pub owner_id: AccountId, } @@ -130,9 +130,10 @@ impl ConversionProxy { let reference_vec: Vec = hex::decode(payment_reference.replace("0x", "")) .expect("Payment reference value error"); assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); + assert_eq!(currency, "USD", "Only payments denominated in USD are implemented for now"); - let get_rate = sb_contract::aggregator_read(SwitchboarIx {address: self.oracle_account.clone(), payer: self.payer.clone()}, - &bs58::encode(self.oracle_account.clone()).into_string(), + let get_rate = sb_contract::aggregator_read(SwitchboarIx {address: self.feed_address.clone(), payer: self.feed_payer.clone()}, + &self.feed_parser, NO_DEPOSIT, BASIC_GAS,); let callback_gas = BASIC_GAS * 3; @@ -153,27 +154,41 @@ impl ConversionProxy { } #[init] - pub fn new(oracle_account: Vec) -> Self { + pub fn new(feed_parser: AccountId, feed_address: Vec) -> Self { let owner_id = env::signer_account_id(); - let payer = bs58::decode(owner_id.clone()).into_vec().expect("Could not decode owner"); + let feed_payer = bs58::decode(owner_id.clone()).into_vec().expect("Could not decode owner"); Self { - oracle_account, - payer, + feed_parser, + feed_address, + feed_payer, owner_id, } } - pub fn set_oracle_account(&mut self, oracle: Vec) { + pub fn set_feed_parser(&mut self, feed_parser: AccountId) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.oracle_account = oracle; + self.feed_parser = feed_parser; } else { panic!("ERR_PERMISSION"); } } - pub fn get_oracle_account(&self) -> Vec { - return self.oracle_account.clone(); + pub fn get_feed_parser(&self) -> AccountId { + return self.feed_parser.clone(); + } + + pub fn set_feed_address(&mut self, oracle: Vec) { + let signer_id = env::predecessor_account_id(); + if self.owner_id == signer_id { + self.feed_address = oracle; + } else { + panic!("ERR_PERMISSION"); + } + } + + pub fn get_feed_address(&self) -> Vec { + return self.feed_address.clone(); } pub fn set_owner(&mut self, owner: ValidAccountId) { @@ -256,6 +271,8 @@ impl ConversionProxy { } PromiseResult::Failed => panic!("ERR_FAILED_ORACLE_FETCH"), }; + // Check rate errors + assert!(rate.num_error == 0 && rate.num_success == 1, "Conversion errors: {}, successes: {}", rate.num_error, rate.num_success); // Check rate validity assert!( u64::from(max_rate_timespan) == 0 @@ -455,37 +472,37 @@ mod tests { testing_env!(context); let mut contract = ConversionProxy::default(); let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_oracle_account(to); + contract.set_feed_address(bs58::decode("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz").into_vec().expect("WRONG TEST FEED ADDRESS FORMAT")); } #[test] - fn admin_oracle() { + fn admin_feed_address() { let owner = ConversionProxy::default().owner_id; let mut contract = ConversionProxy::default(); let context = get_context(owner, ntoy(1), 10u64.pow(14), false); testing_env!(context); let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_oracle_account(to); + contract.set_feed_address(bs58::decode("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz").into_vec().expect("WRONG TEST FEED ADDRESS FORMAT")); } #[test] #[should_panic(expected = r#"ERR_PERMISSION"#)] - fn admin_provider_no_permission() { + fn admin_feed_parser_no_permission() { let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); testing_env!(context); let mut contract = ConversionProxy::default(); let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_provider_account(to); + contract.set_feed_parser(to.into()); } #[test] - fn admin_provider() { + fn admin_feed_parser() { let owner = ConversionProxy::default().owner_id; let mut contract = ConversionProxy::default(); let context = get_context(owner, ntoy(1), 10u64.pow(14), false); testing_env!(context); let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_provider_account(to); + contract.set_feed_parser(to.into()); } #[test] diff --git a/deploy.sh b/deploy.sh index 7937fd0..e3fb7f7 100755 --- a/deploy.sh +++ b/deploy.sh @@ -4,6 +4,7 @@ # testnet deployment and values (default) NEAR_ENV="testnet" +# TODO oracle_account_id="fpo.opfilabs.testnet" provider_account_id="opfilabs.testnet" contract_name="conversion_proxy"; diff --git a/mocks/src/fpo_oracle_mock.rs b/mocks/src/fpo_oracle_mock.rs index d3b2af5..908f515 100644 --- a/mocks/src/fpo_oracle_mock.rs +++ b/mocks/src/fpo_oracle_mock.rs @@ -51,7 +51,7 @@ impl FPOContract { mod tests { use super::*; - use crate::fpo_oracle_mock::AccountId; + // use crate::fpo_oracle_mock::AccountId; use near_sdk::{testing_env, Balance, Gas, MockedBlockchain, VMContext}; use near_sdk_sim::to_yocto; diff --git a/mocks/src/lib.rs b/mocks/src/lib.rs index 1a14e5b..61d845e 100644 --- a/mocks/src/lib.rs +++ b/mocks/src/lib.rs @@ -1,2 +1,3 @@ pub mod fpo_oracle_mock; pub mod fungible_token_mock; +pub mod switchboard_feed_parser_mock; \ No newline at end of file diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs new file mode 100644 index 0000000..d67a711 --- /dev/null +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -0,0 +1,116 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{Timestamp, env, near_bindgen, bs58}; + +/** + * Mocking the Switchboard feed parser contract for tests + */ + + + #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] + pub struct SwitchboardDecimal { + pub mantissa: i128, + pub scale: u32, + } + + #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] + pub struct PriceEntry { + pub result: SwitchboardDecimal, + pub num_success: u32, + pub num_error: u32, + pub round_open_timestamp: Timestamp, + } + + #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] + pub struct SwitchboarIx { + pub address: Vec, + pub payer: Vec, + } + +// For mocks: state of Switchboard feed parser +#[near_bindgen] +#[derive(Default, BorshDeserialize, BorshSerialize)] +pub struct SwitchboardFeedParser {} + +#[near_bindgen] +impl SwitchboardFeedParser { + #[allow(unused_variables)] + pub fn aggregator_read(&self, ix: SwitchboarIx) -> Option { + match &*bs58::encode(ix.address).into_string() { + "testNEARtoUSD" => Some(PriceEntry { + result: SwitchboardDecimal { + mantissa: i128::from(1234000), + scale: u8::from(6).into() + }, + num_success: 1, + num_error: 0, + round_open_timestamp: env::block_timestamp() -10, + }), + _ => { + panic!("InvalidAggregator") + }, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + // use crate::switchboard_feed_parser_mock::SwitchboardFeedParser; + use near_sdk::{testing_env, Balance, Gas, MockedBlockchain, VMContext, AccountId}; + use near_sdk_sim::to_yocto; + + fn get_context( + predecessor_account_id: AccountId, + attached_deposit: Balance, + prepaid_gas: Gas, + is_view: bool, + ) -> VMContext { + VMContext { + current_account_id: predecessor_account_id.clone(), + signer_account_id: predecessor_account_id.clone(), + signer_account_pk: vec![0, 1, 2], + predecessor_account_id, + input: vec![], + block_index: 1, + block_timestamp: 10, + epoch_height: 1, + account_balance: 0, + account_locked_balance: 0, + storage_usage: 10u64.pow(6), + attached_deposit, + prepaid_gas, + random_seed: vec![0, 1, 2], + is_view, + output_data_receivers: vec![], + } + } + #[test] + fn aggregator_read() { + let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); + testing_env!(context); + let contract = SwitchboardFeedParser::default(); + if let Some(result) = contract.aggregator_read(SwitchboarIx { + address: bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC"), + payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC") + }) { + assert_eq!(result.result.mantissa, i128::from(1234000)); + assert_eq!(result.result.scale, 6); + // TODO + } else { + panic!("NEAR/USD mock returned None") + } + } + #[test] + #[should_panic(expected = r#"InvalidAggregator"#)] + fn missing_aggregator_read() { + let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); + testing_env!(context); + let contract = SwitchboardFeedParser::default(); + contract.aggregator_read(SwitchboarIx { + address: bs58::decode("wrongAggregator").into_vec().expect("WRONG VEC"), + payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC") + }); + } +} diff --git a/test.sh b/test.sh index 84d1849..6e7be30 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ cd mocks/ ./build.sh cd .. -cargo test --all \ No newline at end of file +cargo test --all From e0fb9c3a48f670f6f0cf3000f93d4b9e97a7ef3f Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 13 Sep 2023 17:03:38 +0200 Subject: [PATCH 03/20] wip working simulated tests --- conversion_proxy/Cargo.toml | 1 - conversion_proxy/src/lib.rs | 3 +- tests/sim/conversion_proxy.rs | 64 +++++++++++++++++++++++++++++------ tests/sim/utils.rs | 2 +- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/conversion_proxy/Cargo.toml b/conversion_proxy/Cargo.toml index 73a2361..009641a 100644 --- a/conversion_proxy/Cargo.toml +++ b/conversion_proxy/Cargo.toml @@ -3,7 +3,6 @@ name = "conversion_proxy" version = "0.0.2" authors = ["Request Finance", "Request Network"] edition = "2018" -overflow-checks=true [lib] crate-type = ["cdylib", "rlib"] diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index e44de44..bb62ba5 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -293,11 +293,12 @@ impl ConversionProxy { if total_payment > env::attached_deposit() { Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); - panic!( + log!( "Deposit too small for payment (Supplied: {}. Demand (incl. fees): {})", env::attached_deposit(), total_payment ); + return 0_u128; } let change = env::attached_deposit() - (total_payment); diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 632a210..045ecfb 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -1,6 +1,6 @@ use crate::utils::*; use conversion_proxy::ConversionProxyContract; -use mocks::fpo_oracle_mock::FPOContractContract; +use mocks::switchboard_feed_parser_mock::SwitchboardFeedParserContract; use near_sdk::json_types::{U128, U64}; use near_sdk::Balance; use near_sdk_sim::init_simulator; @@ -34,8 +34,8 @@ fn init() -> ( let root = init_simulator(Some(genesis)); deploy!( - contract: FPOContractContract, - contract_id: "mockedfpo".to_string(), + contract: SwitchboardFeedParserContract, + contract_id: "mockedswitchboard".to_string(), bytes: &MOCKED_BYTES, signer_account: root, deposit: to_yocto("7") @@ -52,16 +52,16 @@ fn init() -> ( contract_id: PROXY_ID, bytes: &PROXY_BYTES, signer_account: root, - deposit: to_yocto("10"), - init_method: new("mockedfpo".into(), "any".into()) + deposit: to_yocto("5"), + init_method: new("mockedswitchboard".into(), bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC")) ); - let get_oracle_result = call!(root, proxy.get_oracle_account()); - get_oracle_result.assert_success(); + let get_parser_result = call!(root, proxy.get_feed_parser()); + get_parser_result.assert_success(); debug_assert_eq!( - &get_oracle_result.unwrap_json_value(), - &"mockedfpo".to_string() + &get_parser_result.unwrap_json_value().to_owned(), + &"mockedswitchboard".to_string() ); (account, empty_account_1, empty_account_2, proxy) @@ -188,7 +188,51 @@ fn test_transfer_with_wrong_currency() { ), deposit = transfer_amount ); - assert_one_promise_error(result, "ERR_INVALID_ORACLE_RESPONSE"); + assert_one_promise_error(result, "Only payments denominated in USD are implemented for now"); +} + +#[test] +fn test_transfer_with_low_deposit() { + let (alice, bob, builder, proxy) = init(); + let initial_alice_balance = alice.account().unwrap().amount; + let initial_contract_balance = proxy.account().unwrap().amount; + let transfer_amount = to_yocto("1000"); + let payment_address = bob.account_id().try_into().unwrap(); + let fee_address = builder.account_id().try_into().unwrap(); + + let result = call!( + alice, + proxy.transfer_with_reference( + "0x1122334455667788".to_string(), + payment_address, + U128::from(2000000), + String::from("USD"), + fee_address, + U128::from(0), + U64::from(0) + ), + deposit = transfer_amount + ); + result.assert_success(); + assert_eq!(result.logs().len(), 1, "Wrong number of logs"); + assert!(result.logs()[0].contains("Deposit too small for payment")); + + let alice_balance = alice.account().unwrap().amount; + assert!(alice_balance < initial_alice_balance); + let spent_amount = initial_alice_balance - alice_balance; + assert!( + spent_amount < to_yocto("0.005"), + "Alice should not spend NEAR on a 0 USD payment", + ); + + assert!( + proxy.account().unwrap().amount / to_yocto("1") == initial_contract_balance / to_yocto("1"), + "Contract's balance should be unchanged" + ); + // assert!( + // builder.account().unwrap().amount == initial_bob_balance, + // "Builder's balance should be unchanged" + // ); } #[test] diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index c350763..7e8f0be 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -83,7 +83,7 @@ pub fn assert_one_promise_error(promise_result: ExecutionResult, expected_error_ .outcome() .status { - assert!(execution_error.to_string().contains(expected_error_message)); + assert!(execution_error.to_string().contains(expected_error_message), "Expected error containing: '{}'. Got: '{}'", expected_error_message, execution_error.to_string()); } else { unreachable!(); } From a9e22f7ace09e973eec30e776f957aa590828af2 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 13 Sep 2023 17:04:36 +0200 Subject: [PATCH 04/20] minor --- conversion_proxy/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index bb62ba5..db08eed 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -472,7 +472,7 @@ mod tests { let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); testing_env!(context); let mut contract = ConversionProxy::default(); - let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); + let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); contract.set_feed_address(bs58::decode("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz").into_vec().expect("WRONG TEST FEED ADDRESS FORMAT")); } @@ -482,7 +482,7 @@ mod tests { let mut contract = ConversionProxy::default(); let context = get_context(owner, ntoy(1), 10u64.pow(14), false); testing_env!(context); - let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); + let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); contract.set_feed_address(bs58::decode("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz").into_vec().expect("WRONG TEST FEED ADDRESS FORMAT")); } From 000d427506a6f089b7f1a59ff867d8345fa95ba1 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 14 Sep 2023 15:17:05 +0200 Subject: [PATCH 05/20] wip: deploy.sh simple init param update --- deploy.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/deploy.sh b/deploy.sh index e3fb7f7..9103228 100755 --- a/deploy.sh +++ b/deploy.sh @@ -4,9 +4,8 @@ # testnet deployment and values (default) NEAR_ENV="testnet" -# TODO -oracle_account_id="fpo.opfilabs.testnet" -provider_account_id="opfilabs.testnet" +feed_parser="switchboard-v2.testnet" +feed_address=[251, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18] contract_name="conversion_proxy"; patch=false; @@ -68,7 +67,7 @@ else if ! $patch ; then initParams=" --initFunction new \ - --initArgs '{"oracle_account_id": "'$oracle_account_id'", "provider_account_id": "'$provider_account_id'"}'"; + --initArgs '{"feed_parser": "'$feed_parser'", "feed_address": "'$feed_address'"}'"; fi set -x near deploy -f --wasmFile ./target/wasm32-unknown-unknown/release/$contract_name.wasm \ From 1ede1a22e33375b76a6b5fb8af31ce068d3cd9da Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 14 Sep 2023 18:33:12 +0200 Subject: [PATCH 06/20] Tested onchain. Missing: deploy.sh updates --- conversion_proxy/src/lib.rs | 105 ++++++++++++++++++---- mocks/src/switchboard_feed_parser_mock.rs | 5 +- tests/sim/conversion_proxy.rs | 20 ++++- 3 files changed, 111 insertions(+), 19 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index db08eed..0a0ec59 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -28,9 +28,6 @@ pub struct SwitchboardDecimal { #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct PriceEntry { - // pub price: U128, // Last reported price - // pub decimals: u16, // Amount of decimals (e.g. if 2, 100 = 1.00) - // pub last_update: Timestamp, // Time of report pub result: SwitchboardDecimal, pub num_success: u32, pub num_error: u32, @@ -39,12 +36,11 @@ pub struct PriceEntry { #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct SwitchboarIx { - pub address: Vec, + pub address: Vec, // This feed address reference a specific price feed, see https://app.switchboard.xyz pub payer: Vec, } - -// Interface of the Switchboard price oracle +// Interface of the Switchboard feed parser #[near_sdk::ext_contract(sb_contract)] trait Switchboard { fn aggregator_read(ix: SwitchboarIx) -> Promise; @@ -53,8 +49,9 @@ trait Switchboard { /// /// This contract -/// - oracle_account_id: should be a valid FPO oracle account ID -/// - provider_account_id: should be a valid FPO provider account ID +/// - feed_parser: should be a valid Switchboard feed parser +/// - feed_address: should be a valid NEAR/USD price feed +/// - feed_payer: pays for feeds not sponsored by Switchboard /// - owner_id: only the owner can edit the contract state values above (default = deployer) #[near_bindgen] #[derive(Default, BorshDeserialize, BorshSerialize)] @@ -105,7 +102,7 @@ impl ConversionProxy { /// - `payment_reference`: used for indexing and matching the payment with a request /// - `payment_address`: `amount` in `currency` of NEAR will be paid to this address /// - `amount`: in `currency` with 2 decimals (eg. 1000 is 10.00) - /// - `currency`: ticker, most likely fiat (eg. 'USD') + /// - `currency`: ticker, only "USD" implemented for now /// - `fee_payment_address`: `fee_amount` in `currency` of NEAR will be paid to this address /// - `fee_amount`: in `currency` /// - `max_rate_timespan`: in nanoseconds, the maximum validity for the oracle rate response (or 0 if none) @@ -126,11 +123,11 @@ impl ConversionProxy { env::prepaid_gas(), MIN_GAS ); + assert_eq!(currency, "USD", "Only payments denominated in USD are implemented for now"); let reference_vec: Vec = hex::decode(payment_reference.replace("0x", "")) .expect("Payment reference value error"); assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); - assert_eq!(currency, "USD", "Only payments denominated in USD are implemented for now"); let get_rate = sb_contract::aggregator_read(SwitchboarIx {address: self.feed_address.clone(), payer: self.feed_payer.clone()}, &self.feed_parser, @@ -156,7 +153,7 @@ impl ConversionProxy { #[init] pub fn new(feed_parser: AccountId, feed_address: Vec) -> Self { let owner_id = env::signer_account_id(); - let feed_payer = bs58::decode(owner_id.clone()).into_vec().expect("Could not decode owner"); + let feed_payer = env::signer_account_pk(); Self { feed_parser, feed_address, @@ -178,19 +175,23 @@ impl ConversionProxy { return self.feed_parser.clone(); } - pub fn set_feed_address(&mut self, oracle: Vec) { + pub fn set_feed_address(&mut self, feed_address: String) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.feed_address = oracle; + self.feed_address = bs58::decode(feed_address).into_vec().expect("Wrong feed address format"); } else { panic!("ERR_PERMISSION"); } } - + pub fn get_feed_address(&self) -> Vec { return self.feed_address.clone(); } + pub fn get_encoded_feed_address(&self) -> String { + return bs58::encode(self.feed_address.clone()).into_string(); + } + pub fn set_owner(&mut self, owner: ValidAccountId) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { @@ -199,6 +200,36 @@ impl ConversionProxy { panic!("ERR_PERMISSION"); } } + pub fn set_feed_payer(&mut self) { + let signer_id = env::predecessor_account_id(); + println!("signer_id: {}", signer_id); + if self.owner_id == signer_id { + let feed_payer = env::signer_account_pk(); + let vec_length = feed_payer.len(); + println!("feed_payer: {}, len: {}", feed_payer.clone().into_iter().map(|c| c.to_string()).collect::>().join(","), vec_length); + if vec_length == 32 { + self.feed_payer = env::signer_account_pk(); + return; + } + // For some reason the VM prepends a 0 in front of the 32-long vector + if vec_length > 32 { + log!("Trimming the feed_payer pk to fit length 32 from length {}", vec_length); + self.feed_payer = feed_payer[vec_length-32..].to_vec(); + return + } + panic!("ERR_OWNER_PK_LENGTH"); + } else { + panic!("ERR_PERMISSION"); + } + } + + pub fn get_feed_payer(&self) -> Vec { + return self.feed_payer.clone(); + } + + pub fn get_encoded_feed_payer(&self) -> String { + return bs58::encode(self.feed_payer.clone()).into_string(); + } #[private] pub fn on_transfer_with_reference( @@ -356,7 +387,7 @@ mod tests { VMContext { current_account_id: predecessor_account_id.clone(), signer_account_id: predecessor_account_id.clone(), - signer_account_pk: vec![0, 1, 2], + signer_account_pk: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], predecessor_account_id, input: vec![], block_index: 1, @@ -447,6 +478,26 @@ mod tests { ); } + #[test] + #[should_panic(expected = r#"Only payments denominated in USD are implemented for now"#)] + fn transfer_with_wrong_currency() { + let context = get_context(alice_account(), ntoy(100), 10u64.pow(14), false); + testing_env!(context); + let mut contract = ConversionProxy::default(); + let payment_reference = "0x11223344556677".to_string(); + let currency = "HKD".to_string(); + let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); + contract.transfer_with_reference( + payment_reference, + to, + amount, + currency, + fee_address, + fee_amount, + max_rate_timespan, + ); + } + #[test] fn transfer_with_reference() { let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); @@ -473,7 +524,7 @@ mod tests { testing_env!(context); let mut contract = ConversionProxy::default(); let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_feed_address(bs58::decode("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz").into_vec().expect("WRONG TEST FEED ADDRESS FORMAT")); + contract.set_feed_address("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz".into()); } #[test] @@ -483,7 +534,27 @@ mod tests { let context = get_context(owner, ntoy(1), 10u64.pow(14), false); testing_env!(context); let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_feed_address(bs58::decode("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz").into_vec().expect("WRONG TEST FEED ADDRESS FORMAT")); + contract.set_feed_address("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz".into()); + } + + #[test] + #[should_panic(expected = r#"ERR_PERMISSION"#)] + fn admin_feed_payer_no_permission() { + let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); + testing_env!(context); + let mut contract = ConversionProxy::default(); + let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); + contract.set_feed_payer(); + } + + #[test] + fn admin_feed_payer() { + let owner = ConversionProxy::default().owner_id; + let mut contract = ConversionProxy::default(); + let context = get_context(owner, ntoy(1), 10u64.pow(14), false); + testing_env!(context); + let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); + contract.set_feed_payer(); } #[test] diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index d67a711..82cb42b 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -88,6 +88,8 @@ mod tests { } #[test] fn aggregator_read() { + let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx").into_vec().expect("!!").into_iter().map(|c| c.to_string()).collect::>().join(","); + println!("RESULT: {}", disp_vec); let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); testing_env!(context); let contract = SwitchboardFeedParser::default(); @@ -97,7 +99,6 @@ mod tests { }) { assert_eq!(result.result.mantissa, i128::from(1234000)); assert_eq!(result.result.scale, 6); - // TODO } else { panic!("NEAR/USD mock returned None") } @@ -105,6 +106,8 @@ mod tests { #[test] #[should_panic(expected = r#"InvalidAggregator"#)] fn missing_aggregator_read() { + let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx").into_vec().expect("!!").into_iter().map(|c| c.to_string()).collect::>().join(","); + println!("RESULT: {}", disp_vec); let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); testing_env!(context); let contract = SwitchboardFeedParser::default(); diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 045ecfb..a8b9d0e 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -169,6 +169,24 @@ fn test_transfer_with_invalid_reference_length() { #[test] fn test_transfer_with_wrong_currency() { + let working_vec = bs58::encode([252, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18]).into_string(); + println!("WORKING VEC: {}", working_vec.clone()); + let vec = bs58::decode( working_vec.clone()).into_vec().expect("!!"); + let disp_vec = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); + println!("RESULT WORKING VEC: ['{}']", disp_vec.clone()); + + let vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx").into_vec().expect("!!"); + let disp_vec1 = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); + // println!("RESULT feed-payer: [{}] {}", disp_vec.clone(), vec.len()); + + let vec = bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb").into_vec().expect("!!"); + let disp_vec2 = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); + println!("RESULT \"feed-address\":['{}'],\"payer\":['{}']", disp_vec2.clone(), disp_vec1.clone()); + + let vec = &bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb").into_vec().expect("!!")[1..]; + let disp_vec2 = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); + println!("SLICE \"feed-address\":['{}'],\"length\":['{}']", disp_vec2.clone(), vec.len()); + let (alice, bob, builder, proxy) = init(); let transfer_amount = to_yocto("100"); let payment_address = bob.account_id().try_into().unwrap(); @@ -188,7 +206,7 @@ fn test_transfer_with_wrong_currency() { ), deposit = transfer_amount ); - assert_one_promise_error(result, "Only payments denominated in USD are implemented for now"); + assert_one_promise_error(result, &disp_vec); } #[test] From 392c5522042bdd55001817cc85f5c4d3ad1fbec5 Mon Sep 17 00:00:00 2001 From: yomarion Date: Fri, 15 Sep 2023 03:07:28 +0200 Subject: [PATCH 07/20] stash --- deploy.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/deploy.sh b/deploy.sh index 9103228..cd84b83 100755 --- a/deploy.sh +++ b/deploy.sh @@ -3,9 +3,9 @@ # Run with -h for documentation and help # testnet deployment and values (default) -NEAR_ENV="testnet" -feed_parser="switchboard-v2.testnet" -feed_address=[251, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18] +NEAR_ENV="testnet"; +feed_parser="switchboard-v2.testnet"; +feed_address="[251, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18]"; contract_name="conversion_proxy"; patch=false; @@ -63,11 +63,13 @@ if [ "$contract_name" = "fungible_proxy" ]; then near deploy -f --wasmFile ./target/wasm32-unknown-unknown/release/$contract_name.wasm \ --accountId $ACCOUNT_ID else + initArgs="{"feed_parser":$feed_parser,"feed_address":[251, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18]}"; + echo $initArgs; initParams=""; if ! $patch ; then initParams=" --initFunction new \ - --initArgs '{"feed_parser": "'$feed_parser'", "feed_address": "'$feed_address'"}'"; + --initArgs $initArgs"; fi set -x near deploy -f --wasmFile ./target/wasm32-unknown-unknown/release/$contract_name.wasm \ From 54798c5266dc6bfcea6c4941a16a9505ec5aab30 Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 19 Sep 2023 17:04:58 +0200 Subject: [PATCH 08/20] lint and minor README, test.sh --- README.md | 11 +-- conversion_proxy/src/lib.rs | 80 +++++++++++++------ mocks/src/fpo_oracle_mock.rs | 1 - mocks/src/lib.rs | 2 +- mocks/src/switchboard_feed_parser_mock.rs | 97 +++++++++++++---------- test.sh | 3 +- tests/sim/conversion_proxy.rs | 64 ++++++++++++--- tests/sim/utils.rs | 7 +- 8 files changed, 173 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 7019348..f5356ea 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,9 @@ Smart contracts on NEAR used by the Run all contracts unit tests like this: ``` -cd near-contracts/conversion_proxy -cargo test -cd near-contracts/fungible_conversion_proxy -cargo test -cd near-contracts/fungible_proxy -cargo test -cd near-contracts/mocks -cargo test +cargo test -p conversion_proxy +cargo test -p fungible_conversion_proxy +cargo test -p fungible_proxy ``` ## Integration tests diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index 0a0ec59..d81f981 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -3,7 +3,8 @@ use near_sdk::json_types::{ValidAccountId, U128, U64}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::serde_json::json; use near_sdk::{ - env, log, near_bindgen, serde_json, AccountId, Balance, Gas, Promise, PromiseResult, Timestamp, + bs58, env, log, near_bindgen, serde_json, AccountId, Balance, Gas, Promise, PromiseResult, + Timestamp, }; near_sdk::setup_alloc!(); @@ -19,7 +20,6 @@ const BASIC_GAS: Gas = 10_000_000_000_000; * Switchboard oracle-related declarations */ - #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct SwitchboardDecimal { pub mantissa: i128, @@ -36,7 +36,7 @@ pub struct PriceEntry { #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct SwitchboarIx { - pub address: Vec, // This feed address reference a specific price feed, see https://app.switchboard.xyz + pub address: Vec, // This feed address reference a specific price feed, see https://app.switchboard.xyz pub payer: Vec, } @@ -46,7 +46,6 @@ trait Switchboard { fn aggregator_read(ix: SwitchboarIx) -> Promise; } - /// /// This contract /// - feed_parser: should be a valid Switchboard feed parser @@ -58,7 +57,7 @@ trait Switchboard { pub struct ConversionProxy { pub feed_parser: AccountId, pub feed_address: Vec, - pub feed_payer:Vec, + pub feed_payer: Vec, pub owner_id: AccountId, } @@ -123,16 +122,24 @@ impl ConversionProxy { env::prepaid_gas(), MIN_GAS ); - assert_eq!(currency, "USD", "Only payments denominated in USD are implemented for now"); + assert_eq!( + currency, "USD", + "Only payments denominated in USD are implemented for now" + ); let reference_vec: Vec = hex::decode(payment_reference.replace("0x", "")) .expect("Payment reference value error"); assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); - let get_rate = sb_contract::aggregator_read(SwitchboarIx {address: self.feed_address.clone(), payer: self.feed_payer.clone()}, + let get_rate = sb_contract::aggregator_read( + SwitchboarIx { + address: self.feed_address.clone(), + payer: self.feed_payer.clone(), + }, &self.feed_parser, NO_DEPOSIT, - BASIC_GAS,); + BASIC_GAS, + ); let callback_gas = BASIC_GAS * 3; let process_request_payment = ext_self::rate_callback( to, @@ -178,12 +185,14 @@ impl ConversionProxy { pub fn set_feed_address(&mut self, feed_address: String) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.feed_address = bs58::decode(feed_address).into_vec().expect("Wrong feed address format"); + self.feed_address = bs58::decode(feed_address) + .into_vec() + .expect("Wrong feed address format"); } else { panic!("ERR_PERMISSION"); } } - + pub fn get_feed_address(&self) -> Vec { return self.feed_address.clone(); } @@ -191,7 +200,7 @@ impl ConversionProxy { pub fn get_encoded_feed_address(&self) -> String { return bs58::encode(self.feed_address.clone()).into_string(); } - + pub fn set_owner(&mut self, owner: ValidAccountId) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { @@ -206,23 +215,35 @@ impl ConversionProxy { if self.owner_id == signer_id { let feed_payer = env::signer_account_pk(); let vec_length = feed_payer.len(); - println!("feed_payer: {}, len: {}", feed_payer.clone().into_iter().map(|c| c.to_string()).collect::>().join(","), vec_length); + println!( + "feed_payer: {}, len: {}", + feed_payer + .clone() + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join(","), + vec_length + ); if vec_length == 32 { self.feed_payer = env::signer_account_pk(); return; } // For some reason the VM prepends a 0 in front of the 32-long vector if vec_length > 32 { - log!("Trimming the feed_payer pk to fit length 32 from length {}", vec_length); - self.feed_payer = feed_payer[vec_length-32..].to_vec(); - return + log!( + "Trimming the feed_payer pk to fit length 32 from length {}", + vec_length + ); + self.feed_payer = feed_payer[vec_length - 32..].to_vec(); + return; } panic!("ERR_OWNER_PK_LENGTH"); - } else { + } else { panic!("ERR_PERMISSION"); } } - + pub fn get_feed_payer(&self) -> Vec { return self.feed_payer.clone(); } @@ -303,18 +324,27 @@ impl ConversionProxy { PromiseResult::Failed => panic!("ERR_FAILED_ORACLE_FETCH"), }; // Check rate errors - assert!(rate.num_error == 0 && rate.num_success == 1, "Conversion errors: {}, successes: {}", rate.num_error, rate.num_success); + assert!( + rate.num_error == 0 && rate.num_success == 1, + "Conversion errors: {}, successes: {}", + rate.num_error, + rate.num_success + ); // Check rate validity assert!( u64::from(max_rate_timespan) == 0 - || rate.round_open_timestamp >= env::block_timestamp() - u64::from(max_rate_timespan), + || rate.round_open_timestamp + >= env::block_timestamp() - u64::from(max_rate_timespan), "Conversion rate too old (Last updated: {})", rate.round_open_timestamp, ); - let conversion_rate = 0_u128.checked_add_signed(rate.result.mantissa).expect("Negative conversion rate"); + let conversion_rate = 0_u128 + .checked_add_signed(rate.result.mantissa) + .expect("Negative conversion rate"); let decimals = u32::from(rate.result.scale); - let main_payment = - (Balance::from(amount) * ONE_NEAR * 10u128.pow(decimals) / conversion_rate / ONE_FIAT) as u128; + let main_payment = (Balance::from(amount) * ONE_NEAR * 10u128.pow(decimals) + / conversion_rate + / ONE_FIAT) as u128; let fee_payment = Balance::from(fee_amount) * ONE_NEAR * 10u128.pow(decimals) / conversion_rate / ONE_FIAT; @@ -322,7 +352,6 @@ impl ConversionProxy { let total_payment = main_payment + fee_payment; // Check deposit if total_payment > env::attached_deposit() { - Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); log!( "Deposit too small for payment (Supplied: {}. Demand (incl. fees): {})", @@ -387,7 +416,10 @@ mod tests { VMContext { current_account_id: predecessor_account_id.clone(), signer_account_id: predecessor_account_id.clone(), - signer_account_pk: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], + signer_account_pk: vec![ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, + ], predecessor_account_id, input: vec![], block_index: 1, diff --git a/mocks/src/fpo_oracle_mock.rs b/mocks/src/fpo_oracle_mock.rs index 908f515..2326953 100644 --- a/mocks/src/fpo_oracle_mock.rs +++ b/mocks/src/fpo_oracle_mock.rs @@ -51,7 +51,6 @@ impl FPOContract { mod tests { use super::*; - // use crate::fpo_oracle_mock::AccountId; use near_sdk::{testing_env, Balance, Gas, MockedBlockchain, VMContext}; use near_sdk_sim::to_yocto; diff --git a/mocks/src/lib.rs b/mocks/src/lib.rs index 61d845e..4c11b90 100644 --- a/mocks/src/lib.rs +++ b/mocks/src/lib.rs @@ -1,3 +1,3 @@ pub mod fpo_oracle_mock; pub mod fungible_token_mock; -pub mod switchboard_feed_parser_mock; \ No newline at end of file +pub mod switchboard_feed_parser_mock; diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index 82cb42b..cb8a920 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -1,31 +1,30 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{Timestamp, env, near_bindgen, bs58}; +use near_sdk::{bs58, env, near_bindgen, Timestamp}; /** * Mocking the Switchboard feed parser contract for tests */ +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct SwitchboardDecimal { + pub mantissa: i128, + pub scale: u32, +} - #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] - pub struct SwitchboardDecimal { - pub mantissa: i128, - pub scale: u32, - } - - #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] - pub struct PriceEntry { - pub result: SwitchboardDecimal, - pub num_success: u32, - pub num_error: u32, - pub round_open_timestamp: Timestamp, - } +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct PriceEntry { + pub result: SwitchboardDecimal, + pub num_success: u32, + pub num_error: u32, + pub round_open_timestamp: Timestamp, +} - #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] - pub struct SwitchboarIx { - pub address: Vec, - pub payer: Vec, - } +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct SwitchboarIx { + pub address: Vec, + pub payer: Vec, +} // For mocks: state of Switchboard feed parser #[near_bindgen] @@ -36,20 +35,20 @@ pub struct SwitchboardFeedParser {} impl SwitchboardFeedParser { #[allow(unused_variables)] pub fn aggregator_read(&self, ix: SwitchboarIx) -> Option { - match &*bs58::encode(ix.address).into_string() { - "testNEARtoUSD" => Some(PriceEntry { - result: SwitchboardDecimal { - mantissa: i128::from(1234000), - scale: u8::from(6).into() - }, - num_success: 1, - num_error: 0, - round_open_timestamp: env::block_timestamp() -10, - }), - _ => { - panic!("InvalidAggregator") - }, - } + match &*bs58::encode(ix.address).into_string() { + "testNEARtoUSD" => Some(PriceEntry { + result: SwitchboardDecimal { + mantissa: i128::from(1234000), + scale: u8::from(6).into(), + }, + num_success: 1, + num_error: 0, + round_open_timestamp: env::block_timestamp() - 10, + }), + _ => { + panic!("InvalidAggregator") + } + } } } @@ -58,7 +57,7 @@ mod tests { use super::*; // use crate::switchboard_feed_parser_mock::SwitchboardFeedParser; - use near_sdk::{testing_env, Balance, Gas, MockedBlockchain, VMContext, AccountId}; + use near_sdk::{testing_env, AccountId, Balance, Gas, MockedBlockchain, VMContext}; use near_sdk_sim::to_yocto; fn get_context( @@ -88,17 +87,23 @@ mod tests { } #[test] fn aggregator_read() { - let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx").into_vec().expect("!!").into_iter().map(|c| c.to_string()).collect::>().join(","); + let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx") + .into_vec() + .expect("!!") + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join(","); println!("RESULT: {}", disp_vec); let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); testing_env!(context); let contract = SwitchboardFeedParser::default(); if let Some(result) = contract.aggregator_read(SwitchboarIx { - address: bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC"), - payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC") + address: bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC"), + payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC"), }) { - assert_eq!(result.result.mantissa, i128::from(1234000)); - assert_eq!(result.result.scale, 6); + assert_eq!(result.result.mantissa, i128::from(1234000)); + assert_eq!(result.result.scale, 6); } else { panic!("NEAR/USD mock returned None") } @@ -106,14 +111,22 @@ mod tests { #[test] #[should_panic(expected = r#"InvalidAggregator"#)] fn missing_aggregator_read() { - let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx").into_vec().expect("!!").into_iter().map(|c| c.to_string()).collect::>().join(","); + let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx") + .into_vec() + .expect("!!") + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join(","); println!("RESULT: {}", disp_vec); let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); testing_env!(context); let contract = SwitchboardFeedParser::default(); contract.aggregator_read(SwitchboarIx { - address: bs58::decode("wrongAggregator").into_vec().expect("WRONG VEC"), - payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC") + address: bs58::decode("wrongAggregator") + .into_vec() + .expect("WRONG VEC"), + payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC"), }); } } diff --git a/test.sh b/test.sh index 6e7be30..aba5388 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,3 @@ -cd mocks/ +./mocks/build.sh ./build.sh -cd .. cargo test --all diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index a8b9d0e..aabd809 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -169,23 +169,61 @@ fn test_transfer_with_invalid_reference_length() { #[test] fn test_transfer_with_wrong_currency() { - let working_vec = bs58::encode([252, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18]).into_string(); + let working_vec = bs58::encode([ + 252, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, + 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18, + ]) + .into_string(); println!("WORKING VEC: {}", working_vec.clone()); - let vec = bs58::decode( working_vec.clone()).into_vec().expect("!!"); - let disp_vec = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); + let vec = bs58::decode(working_vec.clone()).into_vec().expect("!!"); + let disp_vec = vec + .clone() + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join("','"); println!("RESULT WORKING VEC: ['{}']", disp_vec.clone()); - let vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx").into_vec().expect("!!"); - let disp_vec1 = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); + let vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx") + .into_vec() + .expect("!!"); + let disp_vec1 = vec + .clone() + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join("','"); // println!("RESULT feed-payer: [{}] {}", disp_vec.clone(), vec.len()); - - let vec = bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb").into_vec().expect("!!"); - let disp_vec2 = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); - println!("RESULT \"feed-address\":['{}'],\"payer\":['{}']", disp_vec2.clone(), disp_vec1.clone()); - - let vec = &bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb").into_vec().expect("!!")[1..]; - let disp_vec2 = vec.clone().into_iter().map(|c| c.to_string()).collect::>().join("','"); - println!("SLICE \"feed-address\":['{}'],\"length\":['{}']", disp_vec2.clone(), vec.len()); + + let vec = bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb") + .into_vec() + .expect("!!"); + let disp_vec2 = vec + .clone() + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join("','"); + println!( + "RESULT \"feed-address\":['{}'],\"payer\":['{}']", + disp_vec2.clone(), + disp_vec1.clone() + ); + + let vec = &bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb") + .into_vec() + .expect("!!")[1..]; + let disp_vec2 = vec + .clone() + .into_iter() + .map(|c| c.to_string()) + .collect::>() + .join("','"); + println!( + "SLICE \"feed-address\":['{}'],\"length\":['{}']", + disp_vec2.clone(), + vec.len() + ); let (alice, bob, builder, proxy) = init(); let transfer_amount = to_yocto("100"); diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index 7e8f0be..3f1cae4 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -83,7 +83,12 @@ pub fn assert_one_promise_error(promise_result: ExecutionResult, expected_error_ .outcome() .status { - assert!(execution_error.to_string().contains(expected_error_message), "Expected error containing: '{}'. Got: '{}'", expected_error_message, execution_error.to_string()); + assert!( + execution_error.to_string().contains(expected_error_message), + "Expected error containing: '{}'. Got: '{}'", + expected_error_message, + execution_error.to_string() + ); } else { unreachable!(); } From a02f633e0f9b19d13654c448a354ef4cce4ee8f2 Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 19 Sep 2023 17:21:07 +0200 Subject: [PATCH 09/20] fix: wrong_currency test --- tests/sim/conversion_proxy.rs | 58 +---------------------------------- 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index aabd809..045ecfb 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -169,62 +169,6 @@ fn test_transfer_with_invalid_reference_length() { #[test] fn test_transfer_with_wrong_currency() { - let working_vec = bs58::encode([ - 252, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, - 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18, - ]) - .into_string(); - println!("WORKING VEC: {}", working_vec.clone()); - let vec = bs58::decode(working_vec.clone()).into_vec().expect("!!"); - let disp_vec = vec - .clone() - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join("','"); - println!("RESULT WORKING VEC: ['{}']", disp_vec.clone()); - - let vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx") - .into_vec() - .expect("!!"); - let disp_vec1 = vec - .clone() - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join("','"); - // println!("RESULT feed-payer: [{}] {}", disp_vec.clone(), vec.len()); - - let vec = bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb") - .into_vec() - .expect("!!"); - let disp_vec2 = vec - .clone() - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join("','"); - println!( - "RESULT \"feed-address\":['{}'],\"payer\":['{}']", - disp_vec2.clone(), - disp_vec1.clone() - ); - - let vec = &bs58::decode("7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb") - .into_vec() - .expect("!!")[1..]; - let disp_vec2 = vec - .clone() - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join("','"); - println!( - "SLICE \"feed-address\":['{}'],\"length\":['{}']", - disp_vec2.clone(), - vec.len() - ); - let (alice, bob, builder, proxy) = init(); let transfer_amount = to_yocto("100"); let payment_address = bob.account_id().try_into().unwrap(); @@ -244,7 +188,7 @@ fn test_transfer_with_wrong_currency() { ), deposit = transfer_amount ); - assert_one_promise_error(result, &disp_vec); + assert_one_promise_error(result, "Only payments denominated in USD are implemented for now"); } #[test] From 5a506cc124894e5b4b67550d805b9073ca4445ec Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 19 Sep 2023 17:49:48 +0200 Subject: [PATCH 10/20] fix: tests with higher ft deposit --- mocks/src/fpo_oracle_mock.rs | 1 + tests/sim/fungible_conversion_proxy.rs | 2 +- tests/sim/fungible_proxy.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mocks/src/fpo_oracle_mock.rs b/mocks/src/fpo_oracle_mock.rs index 2326953..d3b2af5 100644 --- a/mocks/src/fpo_oracle_mock.rs +++ b/mocks/src/fpo_oracle_mock.rs @@ -51,6 +51,7 @@ impl FPOContract { mod tests { use super::*; + use crate::fpo_oracle_mock::AccountId; use near_sdk::{testing_env, Balance, Gas, MockedBlockchain, VMContext}; use near_sdk_sim::to_yocto; diff --git a/tests/sim/fungible_conversion_proxy.rs b/tests/sim/fungible_conversion_proxy.rs index 7b3b2eb..6d9b9a6 100644 --- a/tests/sim/fungible_conversion_proxy.rs +++ b/tests/sim/fungible_conversion_proxy.rs @@ -47,7 +47,7 @@ fn init_fungible() -> ( contract_id: "mockedft".to_string(), bytes: &MOCKED_BYTES, signer_account: root, - deposit: to_yocto("7") + deposit: to_yocto("8") ); let account = root.create_user("alice".to_string(), to_yocto(DEFAULT_BALANCE)); diff --git a/tests/sim/fungible_proxy.rs b/tests/sim/fungible_proxy.rs index ce11ab6..4fb1a07 100644 --- a/tests/sim/fungible_proxy.rs +++ b/tests/sim/fungible_proxy.rs @@ -41,7 +41,7 @@ fn init_fungible() -> ( contract_id: "mockedft".to_string(), bytes: &MOCKED_BYTES, signer_account: root, - deposit: to_yocto("7") + deposit: to_yocto("8") ); let account = root.create_user("alice".to_string(), to_yocto(DEFAULT_BALANCE)); From d39eafffe9f66ccd543538f8502ef1d6c6f99ce0 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 12:55:40 +0200 Subject: [PATCH 11/20] code cleaning, doc --- README.md | 9 ++- conversion_proxy/src/lib.rs | 139 ++++++++++++++++------------------ tests/sim/conversion_proxy.rs | 29 +++---- tests/sim/utils.rs | 11 ++- 4 files changed, 98 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index f5356ea..28d916e 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,21 @@ cargo test -p fungible_conversion_proxy cargo test -p fungible_proxy ``` -## Integration tests +## Integration tests (on a simulated VM with mocked 3rd party contracts) + +Integration tests are located in [tests/sim](tests/sim). ``` # To test everything (unit tests, sanity checks, simulated tests) +# Requires building contracts (release) and mocks (debug) for simulated tests. ./test.sh -# To test contracts one by one: +# To run integration tests on contracts one by one: cargo test conversion_proxy cargo test fungible_conversionproxy cargo test fungible_proxy -# To run integration tests one by one (examples with main transfers): +# To run any tests one by one (examples with main transfers on simulated VM): cargo test conversion_proxy::test_transfer -- --exact cargo test fungible_conversionproxy::test_transfer -- --exact cargo test fungible_proxy::test_transfer -- --exact diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index d81f981..54460d0 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -211,31 +211,16 @@ impl ConversionProxy { } pub fn set_feed_payer(&mut self) { let signer_id = env::predecessor_account_id(); - println!("signer_id: {}", signer_id); if self.owner_id == signer_id { let feed_payer = env::signer_account_pk(); let vec_length = feed_payer.len(); - println!( - "feed_payer: {}, len: {}", - feed_payer - .clone() - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join(","), - vec_length - ); if vec_length == 32 { self.feed_payer = env::signer_account_pk(); return; } - // For some reason the VM prepends a 0 in front of the 32-long vector - if vec_length > 32 { - log!( - "Trimming the feed_payer pk to fit length 32 from length {}", - vec_length - ); - self.feed_payer = feed_payer[vec_length - 32..].to_vec(); + // For some reason, the VM sometimes prepends a 0 in front of the 32-long vector + if vec_length == 33 && feed_payer[0] == 0_u8 { + self.feed_payer = feed_payer[1..].to_vec(); return; } panic!("ERR_OWNER_PK_LENGTH"); @@ -450,20 +435,27 @@ mod tests { ) } + pub(crate) const CURRENCY_USD: &str = "USD"; + pub(crate) const PAYMENT_REF: &str = "0x1122334455667788"; + pub(crate) const FEED_ADDRESS: &str = "HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz"; + #[test] #[should_panic(expected = r#"Incorrect payment reference length"#)] fn transfer_with_invalid_reference_length() { - let context = get_context(alice_account(), ntoy(100), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context( + alice_account(), + ntoy(100), + 10u64.pow(14), + false + )); let mut contract = ConversionProxy::default(); let payment_reference = "0x11223344556677".to_string(); - let currency = "USD".to_string(); let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); contract.transfer_with_reference( payment_reference, to, amount, - currency, + CURRENCY_USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -473,17 +465,20 @@ mod tests { #[test] #[should_panic(expected = r#"Payment reference value error"#)] fn transfer_with_invalid_reference_value() { - let context = get_context(alice_account(), ntoy(100), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context( + alice_account(), + ntoy(100), + 10u64.pow(14), + false + )); let mut contract = ConversionProxy::default(); let payment_reference = "0x123".to_string(); - let currency = "USD".to_string(); let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); contract.transfer_with_reference( payment_reference, to, amount, - currency, + CURRENCY_USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -491,16 +486,19 @@ mod tests { } #[test] - #[should_panic(expected = r#"Not enough attached Gas to call this method"#)] - fn transfer_with_not_enough_gas() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(13), false); - testing_env!(context); + #[should_panic(expected = r#"Only payments denominated in USD are implemented for now"#)] + fn transfer_with_invalid_currency() { + testing_env!(get_context( + alice_account(), + ntoy(100), + 10u64.pow(14), + false + )); let mut contract = ConversionProxy::default(); - let payment_reference = "0x1122334455667788".to_string(); - let currency = "USD".to_string(); + let currency = "HKD".to_string(); let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); contract.transfer_with_reference( - payment_reference, + PAYMENT_REF.into(), to, amount, currency, @@ -511,19 +509,17 @@ mod tests { } #[test] - #[should_panic(expected = r#"Only payments denominated in USD are implemented for now"#)] - fn transfer_with_wrong_currency() { - let context = get_context(alice_account(), ntoy(100), 10u64.pow(14), false); + #[should_panic(expected = r#"Not enough attached Gas to call this method"#)] + fn transfer_with_not_enough_gas() { + let context = get_context(alice_account(), ntoy(1), 10u64.pow(13), false); testing_env!(context); let mut contract = ConversionProxy::default(); - let payment_reference = "0x11223344556677".to_string(); - let currency = "HKD".to_string(); let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); contract.transfer_with_reference( - payment_reference, + PAYMENT_REF.into(), to, amount, - currency, + CURRENCY_USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -532,17 +528,14 @@ mod tests { #[test] fn transfer_with_reference() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - let payment_reference = "0x1122334455667788".to_string(); - let currency = "USD".to_string(); let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); contract.transfer_with_reference( - payment_reference, + PAYMENT_REF.into(), to, amount, - currency, + CURRENCY_USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -551,51 +544,48 @@ mod tests { #[test] #[should_panic(expected = r#"ERR_PERMISSION"#)] - fn admin_oracle_no_permission() { + fn admin_feed_address_no_permission() { let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); testing_env!(context); let mut contract = ConversionProxy::default(); - let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_feed_address("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz".into()); + contract.set_feed_address(FEED_ADDRESS.into()); } #[test] fn admin_feed_address() { let owner = ConversionProxy::default().owner_id; + testing_env!(get_context(owner, ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - let context = get_context(owner, ntoy(1), 10u64.pow(14), false); - testing_env!(context); - let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_feed_address("HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz".into()); + contract.set_feed_address(FEED_ADDRESS.into()); + assert_eq!( + contract.get_encoded_feed_address(), + FEED_ADDRESS.to_string() + ); } #[test] #[should_panic(expected = r#"ERR_PERMISSION"#)] fn admin_feed_payer_no_permission() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); contract.set_feed_payer(); } #[test] fn admin_feed_payer() { let owner = ConversionProxy::default().owner_id; + testing_env!(get_context(owner, ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - let context = get_context(owner, ntoy(1), 10u64.pow(14), false); - testing_env!(context); - let (_to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); contract.set_feed_payer(); + assert_eq!(contract.get_feed_payer(), env::signer_account_pk()); } #[test] #[should_panic(expected = r#"ERR_PERMISSION"#)] fn admin_feed_parser_no_permission() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); + let (to, _, _, _, _) = default_values(); contract.set_feed_parser(to.into()); } @@ -603,19 +593,18 @@ mod tests { fn admin_feed_parser() { let owner = ConversionProxy::default().owner_id; let mut contract = ConversionProxy::default(); - let context = get_context(owner, ntoy(1), 10u64.pow(14), false); - testing_env!(context); - let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_feed_parser(to.into()); + testing_env!(get_context(owner, ntoy(1), 10u64.pow(14), false)); + let (to, _, _, _, _) = default_values(); + contract.set_feed_parser(to.clone().into()); + assert_eq!(contract.get_feed_parser(), Into::::into(to)); } #[test] #[should_panic(expected = r#"ERR_PERMISSION"#)] fn admin_owner_no_permission() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); + let (to, _, _, _, _) = default_values(); contract.set_owner(to); } @@ -623,9 +612,13 @@ mod tests { fn admin_owner() { let owner = ConversionProxy::default().owner_id; let mut contract = ConversionProxy::default(); - let context = get_context(owner, ntoy(1), 10u64.pow(14), false); - testing_env!(context); - let (to, _amount, _fee_address, _fee_amount, _max_rate_timespan) = default_values(); - contract.set_owner(to); + testing_env!(get_context(owner, ntoy(1), 10u64.pow(14), false)); + let (to, _, _, _, _) = default_values(); + contract.set_owner(to.clone()); + testing_env!(get_context(to.into(), ntoy(1), 10u64.pow(14), false)); + assert!(contract.owner_id == env::signer_account_id()); + assert!(contract.get_feed_payer() != env::signer_account_pk()); + contract.set_feed_payer(); + assert_eq!(contract.get_feed_payer(), env::signer_account_pk()); } } diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 045ecfb..bbbebf4 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -23,12 +23,13 @@ lazy_static_include::lazy_static_include_bytes! { const DEFAULT_BALANCE: &str = "400000"; -// Initialize test environment with 3 accounts (alice, bob, builder) and a conversion mock. +// Initialize test environment with 3 accounts (alice, bob, builder), a conversion mock, and its owner account. fn init() -> ( UserAccount, UserAccount, UserAccount, ContractAccount, + UserAccount, ) { let genesis = GenesisConfig::default(); let root = init_simulator(Some(genesis)); @@ -56,6 +57,8 @@ fn init() -> ( init_method: new("mockedswitchboard".into(), bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC")) ); + let set_feed_payer_result = call!(root, proxy.set_feed_payer()); + set_feed_payer_result.assert_success(); let get_parser_result = call!(root, proxy.get_feed_parser()); get_parser_result.assert_success(); @@ -64,12 +67,12 @@ fn init() -> ( &"mockedswitchboard".to_string() ); - (account, empty_account_1, empty_account_2, proxy) + (account, empty_account_1, empty_account_2, proxy, root) } #[test] fn test_transfer() { - let (alice, bob, builder, proxy) = init(); + let (alice, bob, builder, proxy, _) = init(); let initial_alice_balance = alice.account().unwrap().amount; let initial_bob_balance = bob.account().unwrap().amount; let initial_builder_balance = builder.account().unwrap().amount; @@ -135,7 +138,7 @@ fn test_transfer() { fn test_transfer_with_invalid_reference_length() { let transfer_amount = to_yocto("500"); - let (alice, bob, builder, proxy) = init(); + let (alice, bob, builder, proxy, _) = init(); let payment_address = bob.account_id().try_into().unwrap(); let fee_address = builder.account_id().try_into().unwrap(); @@ -153,23 +156,20 @@ fn test_transfer_with_invalid_reference_length() { ), deposit = transfer_amount ); - // No successful outcome is expected - assert!(!result.is_ok()); + assert_one_promise_error(result.clone(), "Incorrect payment reference length"); println!( "test_transfer_with_invalid_parameter_length > TeraGas burnt: {}", result.gas_burnt() as f64 / 1e12 ); - assert_one_promise_error(result, "Incorrect payment reference length"); - // Check Alice balance assert_eq_with_gas(to_yocto(DEFAULT_BALANCE), alice.account().unwrap().amount); } #[test] fn test_transfer_with_wrong_currency() { - let (alice, bob, builder, proxy) = init(); + let (alice, bob, builder, proxy, _) = init(); let transfer_amount = to_yocto("100"); let payment_address = bob.account_id().try_into().unwrap(); let fee_address = builder.account_id().try_into().unwrap(); @@ -188,12 +188,15 @@ fn test_transfer_with_wrong_currency() { ), deposit = transfer_amount ); - assert_one_promise_error(result, "Only payments denominated in USD are implemented for now"); + assert_one_promise_error( + result, + "Only payments denominated in USD are implemented for now", + ); } #[test] fn test_transfer_with_low_deposit() { - let (alice, bob, builder, proxy) = init(); + let (alice, bob, builder, proxy, _) = init(); let initial_alice_balance = alice.account().unwrap().amount; let initial_contract_balance = proxy.account().unwrap().amount; let transfer_amount = to_yocto("1000"); @@ -237,7 +240,7 @@ fn test_transfer_with_low_deposit() { #[test] fn test_transfer_zero_usd() { - let (alice, bob, builder, proxy) = init(); + let (alice, bob, builder, proxy, _) = init(); let initial_alice_balance = alice.account().unwrap().amount; let initial_bob_balance = bob.account().unwrap().amount; let transfer_amount = to_yocto("100"); @@ -279,7 +282,7 @@ fn test_transfer_zero_usd() { #[test] fn test_outdated_rate() { - let (alice, bob, builder, proxy) = init(); + let (alice, bob, builder, proxy, _) = init(); let transfer_amount = to_yocto("100"); let payment_address = bob.account_id().try_into().unwrap(); let fee_address = builder.account_id().try_into().unwrap(); diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index 3f1cae4..ae83d00 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -74,7 +74,16 @@ pub fn assert_received( } pub fn assert_one_promise_error(promise_result: ExecutionResult, expected_error_message: &str) { - assert_eq!(promise_result.promise_errors().len(), 1); + assert!( + !promise_result.is_ok(), + "Promise succeeded, expected to fail." + ); + assert_eq!( + promise_result.promise_errors().len(), + 1, + "Expected 1 error, got {}", + promise_result.promise_errors().len() + ); if let ExecutionStatus::Failure(execution_error) = &promise_result .promise_errors() From 1e36fa984f8ffd5b5a1730ce7c6b984363c7296e Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 13:30:07 +0200 Subject: [PATCH 12/20] code cleanup --- conversion_proxy/src/lib.rs | 10 ++--- mocks/src/switchboard_feed_parser_mock.rs | 1 - tests/sim/conversion_proxy.rs | 53 ++++++++++++----------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index 54460d0..d8f9084 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -435,7 +435,7 @@ mod tests { ) } - pub(crate) const CURRENCY_USD: &str = "USD"; + pub(crate) const USD: &str = "USD"; pub(crate) const PAYMENT_REF: &str = "0x1122334455667788"; pub(crate) const FEED_ADDRESS: &str = "HeS3xrDqHA2CSHTmN9osstz8vbXfgh2mzzzzzzzzzzzz"; @@ -455,7 +455,7 @@ mod tests { payment_reference, to, amount, - CURRENCY_USD.into(), + USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -478,7 +478,7 @@ mod tests { payment_reference, to, amount, - CURRENCY_USD.into(), + USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -519,7 +519,7 @@ mod tests { PAYMENT_REF.into(), to, amount, - CURRENCY_USD.into(), + USD.into(), fee_address, fee_amount, max_rate_timespan, @@ -535,7 +535,7 @@ mod tests { PAYMENT_REF.into(), to, amount, - CURRENCY_USD.into(), + USD.into(), fee_address, fee_amount, max_rate_timespan, diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index cb8a920..beadf92 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -56,7 +56,6 @@ impl SwitchboardFeedParser { mod tests { use super::*; - // use crate::switchboard_feed_parser_mock::SwitchboardFeedParser; use near_sdk::{testing_env, AccountId, Balance, Gas, MockedBlockchain, VMContext}; use near_sdk_sim::to_yocto; diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index bbbebf4..f5a5b6a 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -22,6 +22,8 @@ lazy_static_include::lazy_static_include_bytes! { } const DEFAULT_BALANCE: &str = "400000"; +const USD: &str = "USD"; +const PAYMENT_REF: &str = "0x1122334455667788"; // Initialize test environment with 3 accounts (alice, bob, builder), a conversion mock, and its owner account. fn init() -> ( @@ -79,17 +81,15 @@ fn test_transfer() { let transfer_amount = to_yocto("200000"); let payment_address = bob.account_id().try_into().unwrap(); let fee_address = builder.account_id().try_into().unwrap(); - const ONE_NEAR: Balance = 1_000_000_000_000_000_000_000_000; - // Token transfer failed let result = call!( alice, proxy.transfer_with_reference( - "0x1122334455667788".to_string(), + PAYMENT_REF.into(), payment_address, // 12000.00 USD (main) U128::from(1200000), - String::from("USD"), + USD.into(), fee_address, // 1.00 USD (fee) U128::from(100), @@ -111,9 +111,8 @@ fn test_transfer() { let expected_spent = to_yocto("12001") * 1000 / 1234; assert!( spent_amount - expected_spent < to_yocto("0.005"), - "Alice should spend 12'000 + 1 USD worth of NEAR (+ gas)", + "Alice should spend 12'000 + 1 USD worth of NEAR (+ gas). Spent {spent_amount}, expected: {expected_spent}.", ); - println!("diff: {}", (spent_amount - expected_spent) / ONE_NEAR); assert!(bob.account().unwrap().amount > initial_bob_balance); let received_amount = bob.account().unwrap().amount - initial_bob_balance; @@ -121,14 +120,14 @@ fn test_transfer() { received_amount, // 12'000 USD / rate mocked to_yocto("12000") * 1000 / 1234, - "Bob should receive exactly 12000 USD worth of NEAR" + "Bob should receive exactly 12'000 USD worth of NEAR." ); assert!(builder.account().unwrap().amount > initial_builder_balance); let received_amount = builder.account().unwrap().amount - initial_builder_balance; assert_eq!( received_amount, - // 1 USD + // 1 USD / rate mocked to_yocto("1") * 1000 / 1234, "Builder should receive exactly 1 USD worth of NEAR" ); @@ -149,7 +148,7 @@ fn test_transfer_with_invalid_reference_length() { "0x11223344556677".to_string(), payment_address, U128::from(12), - String::from("USD"), + USD.into(), fee_address, U128::from(1), U64::from(0) @@ -178,7 +177,7 @@ fn test_transfer_with_wrong_currency() { let result = call!( alice, proxy.transfer_with_reference( - "0x1122334455667788".to_string(), + PAYMENT_REF.into(), payment_address, U128::from(1200), String::from("WRONG"), @@ -198,18 +197,23 @@ fn test_transfer_with_wrong_currency() { fn test_transfer_with_low_deposit() { let (alice, bob, builder, proxy, _) = init(); let initial_alice_balance = alice.account().unwrap().amount; + let initial_bob_balance = bob.account().unwrap().amount; let initial_contract_balance = proxy.account().unwrap().amount; let transfer_amount = to_yocto("1000"); let payment_address = bob.account_id().try_into().unwrap(); let fee_address = builder.account_id().try_into().unwrap(); + assert!( + transfer_amount / to_yocto("1") > 0, + "Sanity check: this test is only relevant with high transfer amounts" + ); let result = call!( alice, proxy.transfer_with_reference( - "0x1122334455667788".to_string(), + PAYMENT_REF.into(), payment_address, U128::from(2000000), - String::from("USD"), + USD.into(), fee_address, U128::from(0), U64::from(0) @@ -220,22 +224,21 @@ fn test_transfer_with_low_deposit() { assert_eq!(result.logs().len(), 1, "Wrong number of logs"); assert!(result.logs()[0].contains("Deposit too small for payment")); - let alice_balance = alice.account().unwrap().amount; - assert!(alice_balance < initial_alice_balance); - let spent_amount = initial_alice_balance - alice_balance; + // Alice's balance is slightly impacted by gas, hence we divide by 1 NEAR assert!( - spent_amount < to_yocto("0.005"), - "Alice should not spend NEAR on a 0 USD payment", + alice.account().unwrap().amount / to_yocto("1") == initial_alice_balance / to_yocto("1"), + "Alice should not spend NEAR on a failed payment", ); + // The contract's balance is slightly impacted by execution, hence we divide by 1 NEAR assert!( proxy.account().unwrap().amount / to_yocto("1") == initial_contract_balance / to_yocto("1"), "Contract's balance should be unchanged" ); - // assert!( - // builder.account().unwrap().amount == initial_bob_balance, - // "Builder's balance should be unchanged" - // ); + assert!( + builder.account().unwrap().amount == initial_bob_balance, + "Builder's balance should be unchanged" + ); } #[test] @@ -250,10 +253,10 @@ fn test_transfer_zero_usd() { let result = call!( alice, proxy.transfer_with_reference( - "0x1122334455667788".to_string(), + PAYMENT_REF.into(), payment_address, U128::from(0), - String::from("USD"), + USD.into(), fee_address, U128::from(0), U64::from(0) @@ -290,10 +293,10 @@ fn test_outdated_rate() { let result = call!( alice, proxy.transfer_with_reference( - "0x1122334455667788".to_string(), + PAYMENT_REF.into(), payment_address, U128::from(0), - String::from("USD"), + USD.into(), fee_address, U128::from(0), // The mocked rate is 10 nanoseconds old From a17a99ab9d379bd682549659946a7b31c724f1d4 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 15:00:11 +0200 Subject: [PATCH 13/20] set gas_cost = 0 --- conversion_proxy/src/lib.rs | 7 ++--- tests/sim/conversion_proxy.rs | 58 +++++++++++++++-------------------- tests/sim/utils.rs | 17 +++------- 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index d8f9084..a926d5a 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -209,6 +209,7 @@ impl ConversionProxy { panic!("ERR_PERMISSION"); } } + pub fn set_feed_payer(&mut self) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { @@ -511,8 +512,7 @@ mod tests { #[test] #[should_panic(expected = r#"Not enough attached Gas to call this method"#)] fn transfer_with_not_enough_gas() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(13), false); - testing_env!(context); + testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(13), false)); let mut contract = ConversionProxy::default(); let (to, amount, fee_address, fee_amount, max_rate_timespan) = default_values(); contract.transfer_with_reference( @@ -545,8 +545,7 @@ mod tests { #[test] #[should_panic(expected = r#"ERR_PERMISSION"#)] fn admin_feed_address_no_permission() { - let context = get_context(alice_account(), ntoy(1), 10u64.pow(14), false); - testing_env!(context); + testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); contract.set_feed_address(FEED_ADDRESS.into()); } diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index f5a5b6a..b4d5829 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -2,7 +2,6 @@ use crate::utils::*; use conversion_proxy::ConversionProxyContract; use mocks::switchboard_feed_parser_mock::SwitchboardFeedParserContract; use near_sdk::json_types::{U128, U64}; -use near_sdk::Balance; use near_sdk_sim::init_simulator; use near_sdk_sim::runtime::GenesisConfig; use near_sdk_sim::ContractAccount; @@ -15,10 +14,10 @@ near_sdk::setup_alloc!(); const PROXY_ID: &str = "conversion_proxy"; lazy_static_include::lazy_static_include_bytes! { - PROXY_BYTES => "target/wasm32-unknown-unknown/release/conversion_proxy.wasm" + pub PROXY_BYTES => "target/wasm32-unknown-unknown/release/conversion_proxy.wasm" } lazy_static_include::lazy_static_include_bytes! { - MOCKED_BYTES => "target/wasm32-unknown-unknown/debug/mocks.wasm" + pub MOCKED_BYTES => "target/wasm32-unknown-unknown/debug/mocks.wasm" } const DEFAULT_BALANCE: &str = "400000"; @@ -33,7 +32,8 @@ fn init() -> ( ContractAccount, UserAccount, ) { - let genesis = GenesisConfig::default(); + let mut genesis = GenesisConfig::default(); + genesis.gas_price = 0; let root = init_simulator(Some(genesis)); deploy!( @@ -99,19 +99,14 @@ fn test_transfer() { ); result.assert_success(); - println!( - "test_transfer_usd_near ==> TeraGas burnt: {}", - result.gas_burnt() as f64 / 1e12 - ); - let alice_balance = alice.account().unwrap().amount; assert!(alice_balance < initial_alice_balance); let spent_amount = initial_alice_balance - alice_balance; // 12'001.00 USD worth of NEAR / 1.234 let expected_spent = to_yocto("12001") * 1000 / 1234; assert!( - spent_amount - expected_spent < to_yocto("0.005"), - "Alice should spend 12'000 + 1 USD worth of NEAR (+ gas). Spent {spent_amount}, expected: {expected_spent}.", + yocto_almost_eq(spent_amount, expected_spent), + "Alice should spend 12'000 + 1 USD worth of NEAR. \nSpent: {spent_amount}. \nExpected: {expected_spent}.", ); assert!(bob.account().unwrap().amount > initial_bob_balance); @@ -157,13 +152,12 @@ fn test_transfer_with_invalid_reference_length() { ); assert_one_promise_error(result.clone(), "Incorrect payment reference length"); - println!( - "test_transfer_with_invalid_parameter_length > TeraGas burnt: {}", - result.gas_burnt() as f64 / 1e12 - ); - // Check Alice balance - assert_eq_with_gas(to_yocto(DEFAULT_BALANCE), alice.account().unwrap().amount); + assert_eq!( + to_yocto(DEFAULT_BALANCE), + alice.account().unwrap().amount, + "Alice should not spend NEAR on invalid payment.", + ); } #[test] @@ -203,10 +197,6 @@ fn test_transfer_with_low_deposit() { let payment_address = bob.account_id().try_into().unwrap(); let fee_address = builder.account_id().try_into().unwrap(); - assert!( - transfer_amount / to_yocto("1") > 0, - "Sanity check: this test is only relevant with high transfer amounts" - ); let result = call!( alice, proxy.transfer_with_reference( @@ -224,19 +214,21 @@ fn test_transfer_with_low_deposit() { assert_eq!(result.logs().len(), 1, "Wrong number of logs"); assert!(result.logs()[0].contains("Deposit too small for payment")); - // Alice's balance is slightly impacted by gas, hence we divide by 1 NEAR - assert!( - alice.account().unwrap().amount / to_yocto("1") == initial_alice_balance / to_yocto("1"), - "Alice should not spend NEAR on a failed payment", + assert_eq!( + alice.account().unwrap().amount, + initial_alice_balance, + "Alice should not spend NEAR on a failed payment.", ); // The contract's balance is slightly impacted by execution, hence we divide by 1 NEAR - assert!( - proxy.account().unwrap().amount / to_yocto("1") == initial_contract_balance / to_yocto("1"), + assert_eq!( + proxy.account().unwrap().amount, + initial_contract_balance, "Contract's balance should be unchanged" ); - assert!( - builder.account().unwrap().amount == initial_bob_balance, + assert_eq!( + builder.account().unwrap().amount, + initial_bob_balance, "Builder's balance should be unchanged" ); } @@ -266,11 +258,9 @@ fn test_transfer_zero_usd() { result.assert_success(); let alice_balance = alice.account().unwrap().amount; - assert!(alice_balance < initial_alice_balance); - let spent_amount = initial_alice_balance - alice_balance; - assert!( - spent_amount < to_yocto("0.005"), - "Alice should not spend NEAR on a 0 USD payment", + assert_eq!( + initial_alice_balance, alice_balance, + "Alice should not spend NEAR on a 0 USD payment.", ); assert!( diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index ae83d00..9cf8336 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -1,18 +1,11 @@ use mocks::fungible_token_mock::FungibleTokenContractContract; use near_sdk::json_types::U128; use near_sdk_sim::transaction::ExecutionStatus; -use near_sdk_sim::{call, to_yocto, ContractAccount, ExecutionResult, UserAccount}; +use near_sdk_sim::{call, ContractAccount, ExecutionResult, UserAccount}; -pub fn assert_almost_eq_with_max_delta(left: u128, right: u128, max_delta: u128) { - assert!( - std::cmp::max(left, right) - std::cmp::min(left, right) <= max_delta, - "{}", - format!("Left {left} is not even close to Right {right} within delta {max_delta}") - ); -} - -pub fn assert_eq_with_gas(left: u128, right: u128) { - assert_almost_eq_with_max_delta(left, right, to_yocto("0.005")); +/// Util to compare 2 numbers in yocto, +/- 1 yocto to ignore math precision issues +pub fn yocto_almost_eq(left: u128, right: u128) -> bool { + return std::cmp::max(left, right) - std::cmp::min(left, right) <= 1; } /// Util to check a balance is the same as in a previous state @@ -48,7 +41,7 @@ pub fn assert_spent( assert!(current_balance <= previous_balance, "Did not spend."); assert!( current_balance == previous_balance - expected_spent_amount, - "Spent {} instead of {}", + "Spent {}\ninstead of {}", previous_balance - current_balance, expected_spent_amount ); From 820cba3fe14e1f578e33d7dc1887ce6292d10dc9 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 15:02:16 +0200 Subject: [PATCH 14/20] fix: SwitchboardIx typo --- conversion_proxy/src/lib.rs | 6 +++--- mocks/src/switchboard_feed_parser_mock.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index a926d5a..ec8cae9 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -35,7 +35,7 @@ pub struct PriceEntry { } #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub struct SwitchboarIx { +pub struct SwitchboardIx { pub address: Vec, // This feed address reference a specific price feed, see https://app.switchboard.xyz pub payer: Vec, } @@ -43,7 +43,7 @@ pub struct SwitchboarIx { // Interface of the Switchboard feed parser #[near_sdk::ext_contract(sb_contract)] trait Switchboard { - fn aggregator_read(ix: SwitchboarIx) -> Promise; + fn aggregator_read(ix: SwitchboardIx) -> Promise; } /// @@ -132,7 +132,7 @@ impl ConversionProxy { assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); let get_rate = sb_contract::aggregator_read( - SwitchboarIx { + SwitchboardIx { address: self.feed_address.clone(), payer: self.feed_payer.clone(), }, diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index beadf92..2e924b8 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -21,7 +21,7 @@ pub struct PriceEntry { } #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub struct SwitchboarIx { +pub struct SwitchboardIx { pub address: Vec, pub payer: Vec, } @@ -34,7 +34,7 @@ pub struct SwitchboardFeedParser {} #[near_bindgen] impl SwitchboardFeedParser { #[allow(unused_variables)] - pub fn aggregator_read(&self, ix: SwitchboarIx) -> Option { + pub fn aggregator_read(&self, ix: SwitchboardIx) -> Option { match &*bs58::encode(ix.address).into_string() { "testNEARtoUSD" => Some(PriceEntry { result: SwitchboardDecimal { @@ -97,7 +97,7 @@ mod tests { let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); testing_env!(context); let contract = SwitchboardFeedParser::default(); - if let Some(result) = contract.aggregator_read(SwitchboarIx { + if let Some(result) = contract.aggregator_read(SwitchboardIx { address: bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC"), payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC"), }) { @@ -121,7 +121,7 @@ mod tests { let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); testing_env!(context); let contract = SwitchboardFeedParser::default(); - contract.aggregator_read(SwitchboarIx { + contract.aggregator_read(SwitchboardIx { address: bs58::decode("wrongAggregator") .into_vec() .expect("WRONG VEC"), From e21bd2c4879305a9ec598192f78e6d5d710a54f7 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 15:46:43 +0200 Subject: [PATCH 15/20] expect as precondition style and unwraps --- conversion_proxy/src/lib.rs | 6 ++-- mocks/src/switchboard_feed_parser_mock.rs | 42 +++++++++-------------- tests/sim/conversion_proxy.rs | 2 +- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index ec8cae9..dbbdfe2 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -185,9 +185,7 @@ impl ConversionProxy { pub fn set_feed_address(&mut self, feed_address: String) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.feed_address = bs58::decode(feed_address) - .into_vec() - .expect("Wrong feed address format"); + self.feed_address = bs58::decode(feed_address).into_vec().unwrap(); } else { panic!("ERR_PERMISSION"); } @@ -326,7 +324,7 @@ impl ConversionProxy { ); let conversion_rate = 0_u128 .checked_add_signed(rate.result.mantissa) - .expect("Negative conversion rate"); + .expect("The conversion rate should be positive"); let decimals = u32::from(rate.result.scale); let main_payment = (Balance::from(amount) * ONE_NEAR * 10u128.pow(decimals) / conversion_rate diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index 2e924b8..3dc9ac6 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -86,20 +86,16 @@ mod tests { } #[test] fn aggregator_read() { - let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx") - .into_vec() - .expect("!!") - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join(","); - println!("RESULT: {}", disp_vec); - let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); - testing_env!(context); + testing_env!(get_context( + "alice.near".to_string(), + to_yocto("1"), + 10u64.pow(14), + true + )); let contract = SwitchboardFeedParser::default(); if let Some(result) = contract.aggregator_read(SwitchboardIx { - address: bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC"), - payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC"), + address: bs58::decode("testNEARtoUSD").into_vec().unwrap(), + payer: bs58::decode("anynearpayer").into_vec().unwrap(), }) { assert_eq!(result.result.mantissa, i128::from(1234000)); assert_eq!(result.result.scale, 6); @@ -110,22 +106,16 @@ mod tests { #[test] #[should_panic(expected = r#"InvalidAggregator"#)] fn missing_aggregator_read() { - let disp_vec = bs58::decode("E81iAUr7RPDUksAFtZxn7curbUVRy1Gps6sr6JnQALHx") - .into_vec() - .expect("!!") - .into_iter() - .map(|c| c.to_string()) - .collect::>() - .join(","); - println!("RESULT: {}", disp_vec); - let context = get_context("alice.near".to_string(), to_yocto("1"), 10u64.pow(14), true); - testing_env!(context); + testing_env!(get_context( + "alice.near".to_string(), + to_yocto("1"), + 10u64.pow(14), + true + )); let contract = SwitchboardFeedParser::default(); contract.aggregator_read(SwitchboardIx { - address: bs58::decode("wrongAggregator") - .into_vec() - .expect("WRONG VEC"), - payer: bs58::decode("anynearpayer").into_vec().expect("WRONG VEC"), + address: bs58::decode("wrongAggregator").into_vec().unwrap(), + payer: bs58::decode("anynearpayer").into_vec().unwrap(), }); } } diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index b4d5829..b5fb565 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -56,7 +56,7 @@ fn init() -> ( bytes: &PROXY_BYTES, signer_account: root, deposit: to_yocto("5"), - init_method: new("mockedswitchboard".into(), bs58::decode("testNEARtoUSD").into_vec().expect("WRONG VEC")) + init_method: new("mockedswitchboard".into(), bs58::decode("testNEARtoUSD").into_vec().unwrap()) ); let set_feed_payer_result = call!(root, proxy.set_feed_payer()); From 8d56286f2262fe463568e7c51e21d5274ba26d4b Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 18:04:29 +0200 Subject: [PATCH 16/20] num_success >= 1 should work and refund --- conversion_proxy/src/lib.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index dbbdfe2..4430e65 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -308,12 +308,15 @@ impl ConversionProxy { PromiseResult::Failed => panic!("ERR_FAILED_ORACLE_FETCH"), }; // Check rate errors - assert!( - rate.num_error == 0 && rate.num_success == 1, - "Conversion errors: {}, successes: {}", - rate.num_error, - rate.num_success - ); + if rate.num_error != 0 || rate.num_success < 1 { + Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); + log!( + "Conversion errors: {}, successes: {}", + rate.num_error, + rate.num_success + ); + return 0_u128; + } // Check rate validity assert!( u64::from(max_rate_timespan) == 0 From b4da485d1223f1f28c765e182abb320160b64047 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 20 Sep 2023 21:18:55 +0200 Subject: [PATCH 17/20] chore: replace Uuid with [u8; 32] instead of vec --- conversion_proxy/src/lib.rs | 67 +++++++++++++---------- mocks/src/switchboard_feed_parser_mock.rs | 22 +++++--- tests/sim/conversion_proxy.rs | 4 +- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index 4430e65..33f665a 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -1,10 +1,12 @@ +use std::convert::TryInto; + use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::json_types::{ValidAccountId, U128, U64}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::serde_json::json; use near_sdk::{ bs58, env, log, near_bindgen, serde_json, AccountId, Balance, Gas, Promise, PromiseResult, - Timestamp, + PublicKey, Timestamp, }; near_sdk::setup_alloc!(); @@ -34,10 +36,12 @@ pub struct PriceEntry { pub round_open_timestamp: Timestamp, } +pub type Uuid = [u8; 32]; + #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct SwitchboardIx { - pub address: Vec, // This feed address reference a specific price feed, see https://app.switchboard.xyz - pub payer: Vec, + pub address: Uuid, // This feed address reference a specific price feed, see https://app.switchboard.xyz + pub payer: Uuid, } // Interface of the Switchboard feed parser @@ -56,8 +60,8 @@ trait Switchboard { #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct ConversionProxy { pub feed_parser: AccountId, - pub feed_address: Vec, - pub feed_payer: Vec, + pub feed_address: Uuid, + pub feed_payer: Uuid, pub owner_id: AccountId, } @@ -158,9 +162,9 @@ impl ConversionProxy { } #[init] - pub fn new(feed_parser: AccountId, feed_address: Vec) -> Self { + pub fn new(feed_parser: AccountId, feed_address: Uuid) -> Self { let owner_id = env::signer_account_id(); - let feed_payer = env::signer_account_pk(); + let feed_payer: Uuid = Self::get_uuid(env::signer_account_pk()); Self { feed_parser, feed_address, @@ -185,13 +189,17 @@ impl ConversionProxy { pub fn set_feed_address(&mut self, feed_address: String) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.feed_address = bs58::decode(feed_address).into_vec().unwrap(); + self.feed_address = bs58::decode(feed_address) + .into_vec() + .expect("feed_address should be decodable into a vector") + .try_into() + .expect("feed_address should be decodable into [u8; 32]"); } else { panic!("ERR_PERMISSION"); } } - pub fn get_feed_address(&self) -> Vec { + pub fn get_feed_address(&self) -> Uuid { return self.feed_address.clone(); } @@ -211,24 +219,13 @@ impl ConversionProxy { pub fn set_feed_payer(&mut self) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - let feed_payer = env::signer_account_pk(); - let vec_length = feed_payer.len(); - if vec_length == 32 { - self.feed_payer = env::signer_account_pk(); - return; - } - // For some reason, the VM sometimes prepends a 0 in front of the 32-long vector - if vec_length == 33 && feed_payer[0] == 0_u8 { - self.feed_payer = feed_payer[1..].to_vec(); - return; - } - panic!("ERR_OWNER_PK_LENGTH"); + self.feed_payer = Self::get_uuid(env::signer_account_pk()); } else { panic!("ERR_PERMISSION"); } } - pub fn get_feed_payer(&self) -> Vec { + pub fn get_feed_payer(&self) -> Uuid { return self.feed_payer.clone(); } @@ -236,6 +233,21 @@ impl ConversionProxy { return bs58::encode(self.feed_payer.clone()).into_string(); } + /// This method transforms a PublicKey (eg. ed25519:3H8UcosBhKfPcuZj7ffr3QqG5BxiGzJECqPZAZka5fJn) into a Uuid (alias for [u8; 32]) + /// Should be useless onchain. + #[private] + pub fn get_uuid(public_key: PublicKey) -> Uuid { + let vec_length = public_key.len(); + if vec_length == 32 { + return public_key.try_into().unwrap(); + } + // For some reason, the local VM sometimes prepends a 0 in front of the 32-long vector + if vec_length == 33 && public_key[0] == 0_u8 { + return public_key[1..].try_into().unwrap(); + } + panic!("ERR_OWNER_PK_LENGTH {} {:?}", vec_length, public_key); + } + #[private] pub fn on_transfer_with_reference( &self, @@ -403,10 +415,7 @@ mod tests { VMContext { current_account_id: predecessor_account_id.clone(), signer_account_id: predecessor_account_id.clone(), - signer_account_pk: vec![ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, 30, 31, - ], + signer_account_pk: (1..33).collect(), // Public key: Size 32 predecessor_account_id, input: vec![], block_index: 1, @@ -577,7 +586,7 @@ mod tests { testing_env!(get_context(owner, ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); contract.set_feed_payer(); - assert_eq!(contract.get_feed_payer(), env::signer_account_pk()); + assert_eq!(contract.get_feed_payer().to_vec(), env::signer_account_pk()); } #[test] @@ -617,8 +626,8 @@ mod tests { contract.set_owner(to.clone()); testing_env!(get_context(to.into(), ntoy(1), 10u64.pow(14), false)); assert!(contract.owner_id == env::signer_account_id()); - assert!(contract.get_feed_payer() != env::signer_account_pk()); + assert!(contract.get_feed_payer().to_vec() != env::signer_account_pk()); contract.set_feed_payer(); - assert_eq!(contract.get_feed_payer(), env::signer_account_pk()); + assert_eq!(contract.get_feed_payer().to_vec(), env::signer_account_pk()); } } diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index 3dc9ac6..d863fcb 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -1,6 +1,6 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{bs58, env, near_bindgen, Timestamp}; +use near_sdk::{env, near_bindgen, Timestamp}; /** * Mocking the Switchboard feed parser contract for tests @@ -20,10 +20,12 @@ pub struct PriceEntry { pub round_open_timestamp: Timestamp, } +pub type Uuid = [u8; 32]; + #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct SwitchboardIx { - pub address: Vec, - pub payer: Vec, + pub address: Uuid, + pub payer: Uuid, } // For mocks: state of Switchboard feed parser @@ -31,12 +33,14 @@ pub struct SwitchboardIx { #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct SwitchboardFeedParser {} +pub const VALID_FEED_ADDRESS: [u8; 32] = [0; 32]; + #[near_bindgen] impl SwitchboardFeedParser { #[allow(unused_variables)] pub fn aggregator_read(&self, ix: SwitchboardIx) -> Option { - match &*bs58::encode(ix.address).into_string() { - "testNEARtoUSD" => Some(PriceEntry { + match ix.address { + VALID_FEED_ADDRESS => Some(PriceEntry { result: SwitchboardDecimal { mantissa: i128::from(1234000), scale: u8::from(6).into(), @@ -94,8 +98,8 @@ mod tests { )); let contract = SwitchboardFeedParser::default(); if let Some(result) = contract.aggregator_read(SwitchboardIx { - address: bs58::decode("testNEARtoUSD").into_vec().unwrap(), - payer: bs58::decode("anynearpayer").into_vec().unwrap(), + address: [0; 32], + payer: [1; 32], }) { assert_eq!(result.result.mantissa, i128::from(1234000)); assert_eq!(result.result.scale, 6); @@ -114,8 +118,8 @@ mod tests { )); let contract = SwitchboardFeedParser::default(); contract.aggregator_read(SwitchboardIx { - address: bs58::decode("wrongAggregator").into_vec().unwrap(), - payer: bs58::decode("anynearpayer").into_vec().unwrap(), + address: [255; 32], + payer: [1; 32], }); } } diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index b5fb565..4b1c0f1 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -1,6 +1,6 @@ use crate::utils::*; use conversion_proxy::ConversionProxyContract; -use mocks::switchboard_feed_parser_mock::SwitchboardFeedParserContract; +use mocks::switchboard_feed_parser_mock::{SwitchboardFeedParserContract, VALID_FEED_ADDRESS}; use near_sdk::json_types::{U128, U64}; use near_sdk_sim::init_simulator; use near_sdk_sim::runtime::GenesisConfig; @@ -56,7 +56,7 @@ fn init() -> ( bytes: &PROXY_BYTES, signer_account: root, deposit: to_yocto("5"), - init_method: new("mockedswitchboard".into(), bs58::decode("testNEARtoUSD").into_vec().unwrap()) + init_method: new("mockedswitchboard".into(), VALID_FEED_ADDRESS) ); let set_feed_payer_result = call!(root, proxy.set_feed_payer()); From f52cc82ae7e91253c3dfae2a79e44765ddbbc1e8 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 21 Sep 2023 17:41:23 +0200 Subject: [PATCH 18/20] chore: ease contract init with String feed address --- conversion_proxy/src/lib.rs | 78 ++++++++++++++--------- mocks/src/switchboard_feed_parser_mock.rs | 9 ++- tests/sim/conversion_proxy.rs | 4 +- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/conversion_proxy/src/lib.rs b/conversion_proxy/src/lib.rs index 33f665a..12d9ae4 100644 --- a/conversion_proxy/src/lib.rs +++ b/conversion_proxy/src/lib.rs @@ -162,9 +162,10 @@ impl ConversionProxy { } #[init] - pub fn new(feed_parser: AccountId, feed_address: Uuid) -> Self { + pub fn new(feed_parser: AccountId, feed_address_pk: &String) -> Self { let owner_id = env::signer_account_id(); - let feed_payer: Uuid = Self::get_uuid(env::signer_account_pk()); + let feed_payer = Self::get_uuid(env::signer_account_pk()).expect("ERR_OWNER_PK_LENGTH"); + let feed_address = Self::get_uuid_from_string(feed_address_pk); Self { feed_parser, feed_address, @@ -186,14 +187,10 @@ impl ConversionProxy { return self.feed_parser.clone(); } - pub fn set_feed_address(&mut self, feed_address: String) { + pub fn set_feed_address(&mut self, feed_address: &String) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.feed_address = bs58::decode(feed_address) - .into_vec() - .expect("feed_address should be decodable into a vector") - .try_into() - .expect("feed_address should be decodable into [u8; 32]"); + self.feed_address = Self::get_uuid_from_string(feed_address); } else { panic!("ERR_PERMISSION"); } @@ -219,7 +216,8 @@ impl ConversionProxy { pub fn set_feed_payer(&mut self) { let signer_id = env::predecessor_account_id(); if self.owner_id == signer_id { - self.feed_payer = Self::get_uuid(env::signer_account_pk()); + self.feed_payer = + Self::get_uuid(env::signer_account_pk()).expect("ERR_OWNER_PK_LENGTH"); } else { panic!("ERR_PERMISSION"); } @@ -236,16 +234,25 @@ impl ConversionProxy { /// This method transforms a PublicKey (eg. ed25519:3H8UcosBhKfPcuZj7ffr3QqG5BxiGzJECqPZAZka5fJn) into a Uuid (alias for [u8; 32]) /// Should be useless onchain. #[private] - pub fn get_uuid(public_key: PublicKey) -> Uuid { + pub fn get_uuid(public_key: PublicKey) -> Option { let vec_length = public_key.len(); if vec_length == 32 { - return public_key.try_into().unwrap(); + return Some(public_key.try_into().unwrap()); } // For some reason, the local VM sometimes prepends a 0 in front of the 32-long vector if vec_length == 33 && public_key[0] == 0_u8 { - return public_key[1..].try_into().unwrap(); + return Some(public_key[1..].try_into().unwrap()); } - panic!("ERR_OWNER_PK_LENGTH {} {:?}", vec_length, public_key); + return None; + } + + #[private] + pub fn get_uuid_from_string(public_key: &String) -> Uuid { + bs58::decode(public_key) + .into_vec() + .expect("public_key should be decodable into a vector") + .try_into() + .expect("public_key should be decodable into [u8; 32]") } #[private] @@ -294,6 +301,15 @@ impl ConversionProxy { } } + /// This method refunds a payer, then logs an error message. + /// Used as a bandaid until we find a solution for refund.then(panic) + #[private] + pub fn refund_then_log(&mut self, payer: ValidAccountId, error_message: String) -> u128 { + Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); + log!(error_message); + return 0_u128; + } + #[private] #[payable] pub fn rate_callback( @@ -314,20 +330,24 @@ impl ConversionProxy { PromiseResult::Successful(value) => { match serde_json::from_slice::(&value) { Ok(value) => value, - Err(_e) => panic!("ERR_INVALID_ORACLE_RESPONSE"), + Err(_e) => { + return self.refund_then_log(payer, "ERR_INVALID_ORACLE_RESPONSE".into()) + } } } - PromiseResult::Failed => panic!("ERR_FAILED_ORACLE_FETCH"), + PromiseResult::Failed => { + return self.refund_then_log(payer, "ERR_FAILED_ORACLE_FETCH".into()); + } }; // Check rate errors if rate.num_error != 0 || rate.num_success < 1 { - Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); - log!( - "Conversion errors: {}, successes: {}", - rate.num_error, - rate.num_success + return self.refund_then_log( + payer, + "Conversion errors:".to_string() + + &rate.num_error.to_string() + + &", successes: " + + &rate.num_success.to_string(), ); - return 0_u128; } // Check rate validity assert!( @@ -351,13 +371,13 @@ impl ConversionProxy { let total_payment = main_payment + fee_payment; // Check deposit if total_payment > env::attached_deposit() { - Promise::new(payer.clone().to_string()).transfer(env::attached_deposit()); - log!( - "Deposit too small for payment (Supplied: {}. Demand (incl. fees): {})", - env::attached_deposit(), - total_payment + return self.refund_then_log( + payer, + "Deposit too small for payment. Supplied: ".to_string() + + &env::attached_deposit().to_string() + + &". Demand (incl. fees): " + + &total_payment.to_string(), ); - return 0_u128; } let change = env::attached_deposit() - (total_payment); @@ -557,7 +577,7 @@ mod tests { fn admin_feed_address_no_permission() { testing_env!(get_context(alice_account(), ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - contract.set_feed_address(FEED_ADDRESS.into()); + contract.set_feed_address(&FEED_ADDRESS.into()); } #[test] @@ -565,7 +585,7 @@ mod tests { let owner = ConversionProxy::default().owner_id; testing_env!(get_context(owner, ntoy(1), 10u64.pow(14), false)); let mut contract = ConversionProxy::default(); - contract.set_feed_address(FEED_ADDRESS.into()); + contract.set_feed_address(&FEED_ADDRESS.into()); assert_eq!( contract.get_encoded_feed_address(), FEED_ADDRESS.to_string() diff --git a/mocks/src/switchboard_feed_parser_mock.rs b/mocks/src/switchboard_feed_parser_mock.rs index d863fcb..b4d3171 100644 --- a/mocks/src/switchboard_feed_parser_mock.rs +++ b/mocks/src/switchboard_feed_parser_mock.rs @@ -1,6 +1,7 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{env, near_bindgen, Timestamp}; +use near_sdk::{bs58, env, near_bindgen, Timestamp}; +use std::str; /** * Mocking the Switchboard feed parser contract for tests @@ -33,7 +34,11 @@ pub struct SwitchboardIx { #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct SwitchboardFeedParser {} -pub const VALID_FEED_ADDRESS: [u8; 32] = [0; 32]; +const VALID_FEED_ADDRESS: [u8; 32] = [0; 32]; + +pub fn valid_feed_key() -> String { + bs58::encode(&VALID_FEED_ADDRESS).into_string() +} #[near_bindgen] impl SwitchboardFeedParser { diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 4b1c0f1..33711ca 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -1,6 +1,6 @@ use crate::utils::*; use conversion_proxy::ConversionProxyContract; -use mocks::switchboard_feed_parser_mock::{SwitchboardFeedParserContract, VALID_FEED_ADDRESS}; +use mocks::switchboard_feed_parser_mock::{valid_feed_key, SwitchboardFeedParserContract}; use near_sdk::json_types::{U128, U64}; use near_sdk_sim::init_simulator; use near_sdk_sim::runtime::GenesisConfig; @@ -56,7 +56,7 @@ fn init() -> ( bytes: &PROXY_BYTES, signer_account: root, deposit: to_yocto("5"), - init_method: new("mockedswitchboard".into(), VALID_FEED_ADDRESS) + init_method: new("mockedswitchboard".into(), &valid_feed_key()) ); let set_feed_payer_result = call!(root, proxy.set_feed_payer()); From 3c7155cb2ac7e536380cac8de54bc700d97780d5 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:50:42 +0200 Subject: [PATCH 19/20] Update conversion_proxy.rs --- tests/sim/conversion_proxy.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 33711ca..71ef30d 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -220,7 +220,6 @@ fn test_transfer_with_low_deposit() { "Alice should not spend NEAR on a failed payment.", ); - // The contract's balance is slightly impacted by execution, hence we divide by 1 NEAR assert_eq!( proxy.account().unwrap().amount, initial_contract_balance, From 543f1e6798948686bde35697c56dd70b5c63d6b1 Mon Sep 17 00:00:00 2001 From: yomarion Date: Fri, 22 Sep 2023 16:03:12 +0200 Subject: [PATCH 20/20] deploy.sh --- deploy.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/deploy.sh b/deploy.sh index cd84b83..ead65ee 100755 --- a/deploy.sh +++ b/deploy.sh @@ -4,8 +4,8 @@ # testnet deployment and values (default) NEAR_ENV="testnet"; -feed_parser="switchboard-v2.testnet"; -feed_address="[251, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18]"; +oracle_account_id="fpo.opfilabs.testnet" +provider_account_id="opfilabs.testnet" contract_name="conversion_proxy"; patch=false; @@ -63,7 +63,19 @@ if [ "$contract_name" = "fungible_proxy" ]; then near deploy -f --wasmFile ./target/wasm32-unknown-unknown/release/$contract_name.wasm \ --accountId $ACCOUNT_ID else - initArgs="{"feed_parser":$feed_parser,"feed_address":[251, 166, 196, 242, 159, 139, 89, 47, 230, 78, 243, 185, 185, 188, 150, 219, 165, 68, 131, 5, 216, 42, 120, 26, 26, 142, 133, 0, 111, 235, 63, 18]}"; + + if [ "$contract_name" = "conversion_proxy" ]; then + if [ "$NEAR_ENV" = "mainnet" ]; then + feed_parser="switchboard-v2.mainnet"; + feed_address="C3p8SSWQS8j1nx7HrzBBphX5jZcS1EY28EJ5iwjzSix2"; + else + feed_parser="switchboard-v2.testnet"; + feed_address="7igqhpGQ8xPpyjQ4gMHhXRvtZcrKSGJkdKDJYBiPQgcb"; + fi + initArgs='{"feed_parser":"'$feed_parser'","feed_address_pk":"'$feed_address'"}'; + else + initArgs='{"oracle_account_id": "'$oracle_account_id'", "provider_account_id": "'$provider_account_id'"}'; + fi echo $initArgs; initParams=""; if ! $patch ; then