From 0281e295834a94351691b7b2319ad3ec208ee113 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 14:04:50 +0100 Subject: [PATCH 01/14] initiated contract and unit test --- Cargo.lock | 10 + Cargo.toml | 2 +- fungible_proxy/Cargo.toml | 16 + fungible_proxy/src/lib.rs | 357 +++++++++++++++++++++ fungible_proxy/test.sh | 2 + tests/sim/{main.rs => conversion_proxy.rs} | 0 6 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 fungible_proxy/Cargo.toml create mode 100644 fungible_proxy/src/lib.rs create mode 100755 fungible_proxy/test.sh rename tests/sim/{main.rs => conversion_proxy.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 97e6438..234e506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -959,6 +959,16 @@ dependencies = [ "serde", ] +[[package]] +name = "fungible_proxy" +version = "0.0.1" +dependencies = [ + "hex", + "near-sdk", + "near-sdk-sim", + "serde", +] + [[package]] name = "funty" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index e3df073..f017c0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,4 +27,4 @@ panic = "abort" overflow-checks = true [workspace] -members = ["conversion_proxy", "fungible_conversion_proxy", "mocks"] \ No newline at end of file +members = ["conversion_proxy", "fungible_conversion_proxy", "fungible_proxy", "mocks"] \ No newline at end of file diff --git a/fungible_proxy/Cargo.toml b/fungible_proxy/Cargo.toml new file mode 100644 index 0000000..08dd04c --- /dev/null +++ b/fungible_proxy/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fungible_proxy" +version = "0.0.1" +authors = ["Request Network Foundation"] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "3.1.0" +serde = "1.0.118" +hex = "0.4" + +[dev-dependencies] +near-sdk-sim = "3.2.0" diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs new file mode 100644 index 0000000..fd5381e --- /dev/null +++ b/fungible_proxy/src/lib.rs @@ -0,0 +1,357 @@ +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::{ + env, near_bindgen, serde_json, AccountId, Balance, Gas, Promise +}; +near_sdk::setup_alloc!(); + +const NO_DEPOSIT: Balance = 0; +const YOCTO_DEPOSIT: Balance = 1; // Fungible token transfers require a deposit of exactly 1 yoctoNEAR +const ONE_FIAT: Balance = 100; // Fiat values with two decimals +const MIN_GAS: Gas = 150_000_000_000_000; +const BASIC_GAS: Gas = 10_000_000_000_000; + +/// Helper struct containing arguments supplied by the caller +/// +/// - `amount`: in `currency` with 2 decimals (eg. 1000 is 10.00) +/// - `fee_address`: `fee_amount` in `currency` of payment token will be paid to this address +/// - `fee_amount`: in `currency` +/// - `payment_reference`: used for indexing and matching the payment with a request +/// - `to`: `amount` in `currency` of payment token will be paid to this address +#[derive(Serialize, Deserialize)] +pub struct PaymentArgs { + pub amount: U128, + pub fee_address: ValidAccountId, + pub fee_amount: U128, + pub payment_reference: String, + pub to: ValidAccountId, +} + +impl Into for String { + fn into(self) -> PaymentArgs { + serde_json::from_str(&self).expect("Incorrect msg format") + } +} + +impl Into for PaymentArgs { + fn into(self) -> String { + serde_json::to_string(&self).unwrap() + } +} + +/** + * Fungible token-related declarations + */ + +// Interface of fungible tokens +#[near_sdk::ext_contract(ft_contract)] +trait FungibleTokenContract { + fn ft_transfer(receiver_id: String, amount: String, memo: Option); +} + +/// +/// This contract +#[near_bindgen] +#[derive(Default, BorshDeserialize, BorshSerialize)] +pub struct FungibleProxy {} + +// Callback methods +#[near_sdk::ext_contract(ext_self)] +pub trait ExtSelfRequestProxy { + fn on_transfer_with_reference( + &self, + args: PaymentArgs, + token_address: AccountId, + payer: AccountId, + amount: U128, + ) -> String; +} + +trait FungibleTokenReceiver { + fn ft_on_transfer(&mut self, sender_id: AccountId, amount: String, msg: String) -> Promise; +} + +#[near_bindgen] +impl FungibleTokenReceiver for FungibleProxy { + /// This is the function that will be called by the fungible token contract's `ft_transfer_call` function. + /// We use the `msg` field to obtain the arguments supplied by the caller specifying the intended payment. + /// `msg` should be a string in JSON format containing all the fields in `PaymentArgs`. + /// Eg. msg = {"payment_reference":"abc7c8bb1234fd12","to":"dummy.payee.near","amount":"1000000","currency":"USD","fee_address":"fee.requestfinance.near","fee_amount":"200","max_rate_timespan":"0"} + /// + /// For more information on the fungible token standard, see https://nomicon.io/Standards/Tokens/FungibleToken/Core + /// + fn ft_on_transfer(&mut self, sender_id: AccountId, amount: String, msg: String) -> Promise { + let token_address = env::predecessor_account_id(); + self.transfer_with_reference( + msg.into(), + token_address, + sender_id, + U128::from(amount.parse::().unwrap()), + ) + } +} + +#[near_bindgen] +impl FungibleProxy { + /// Main function for this contract, transfers fungible tokens to a payment address (to) with a payment reference, as well as a fee. + /// The `amount` is denominated in `currency` with 2 decimals. + /// + /// Due to the way NEAR defines the fungible token standard, this function should NOT be called directly. Instead, the + /// `ft_transfer_call` function in the contract of the fungible token being used for payment should be called with the + /// `msg` argument containing the arguments for this function as a JSON-serialized string. `ft_on_transfer` (defined + /// in this contract) is called by the token contract and deserializes the arguments and calls this function. + /// + /// See https://nomicon.io/Standards/Tokens/FungibleToken/Core for more information on how NEAR handles + /// sending fungible tokens to be used by a contract function. + /// + #[private] + fn transfer_with_reference( + &mut self, + args: PaymentArgs, + token_address: AccountId, + payer: AccountId, + amount: U128, + ) -> Promise { + assert!( + MIN_GAS <= env::prepaid_gas(), + "Not enough attached Gas to call this method (Supplied: {}. Demand: {})", + env::prepaid_gas(), + MIN_GAS + ); + + let reference_vec: Vec = hex::decode(args.payment_reference.replace("0x", "")) + .expect("Payment reference value error"); + assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); + assert!(args.fee_amount.0 <= amount.0, "amount smaller than fee_amount"); + let main_amount = amount.0 - args.fee_amount.0; + + ext_self::on_transfer_with_reference( + args, + token_address, + payer, + main_amount.into(), + &env::current_account_id(), + NO_DEPOSIT, + BASIC_GAS, + ) + } + + /// Convenience function for constructing the `msg` argument for `ft_transfer_call` in the fungible token contract. + /// Constructing the `msg` string could also easily be done on the frontend so is included here just for completeness + /// and convenience. + pub fn get_transfer_with_reference_args( + &self, + amount: U128, + currency: String, + fee_address: ValidAccountId, + fee_amount: U128, + max_rate_timespan: U64, + payment_reference: String, + to: ValidAccountId, + ) -> String { + let args = PaymentArgs { + amount, + fee_address, + fee_amount, + payment_reference, + to, + }; + args.into() + } + + #[private] + pub fn on_transfer_with_reference( + &self, + args: PaymentArgs, + token_address: AccountId, + payer: AccountId, + amount: U128, + ) -> String { + if near_sdk::is_promise_success() { + // Log success for indexing and payment detection + env::log( + &json!({ + "amount": amount, + "token_address": token_address, + "fee_address": args.fee_address, + "fee_amount": args.fee_amount, + "payment_reference": args.payment_reference, + "to": args.to, + }) + .to_string() + .into_bytes(), + ); + 0.to_string() + } else { + env::log( + format!( + "Failed to transfer to account {}. Returning attached amount of {} of token {} to {}", + args.to, amount.0, token_address, payer) + .as_bytes(), + ); + (amount.0 + args.fee_amount.0).to_string() // return full amount for `ft_resolve_transfer` on the token contract + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use near_sdk::{testing_env, VMContext}; + use near_sdk::{AccountId, Balance, MockedBlockchain}; + use std::convert::TryInto; + + fn alice_account() -> AccountId { + "alice.near".to_string() + } + + 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: 0, + 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![], + } + } + + fn ntoy(near_amount: Balance) -> Balance { + near_amount * 10u128.pow(24) + } + + /// Helper function: get default values for PaymentArgs + fn get_default_payment_args() -> PaymentArgs { + PaymentArgs { + amount: 1000000.into(), + fee_address: "fee.requestfinance.near".to_string().try_into().unwrap(), + fee_amount: 200.into(), + payment_reference: "abc7c8bb1234fd12".into(), + to: "dummy.payee.near".to_string().try_into().unwrap(), + } + } + + /// Helper function: convert a PaymentArgs into the msg string to be passed into `ft_transfer_call` + fn get_msg_from_args(args: PaymentArgs) -> String { + serde_json::to_string(&args).unwrap() + } + + #[test] + #[should_panic(expected = r#"Incorrect payment reference length"#)] + fn transfer_with_invalid_reference_length() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let mut args = get_default_payment_args(); + args.payment_reference = "0x11223344556677".to_string(); + let msg = get_msg_from_args(args); + + contract.ft_on_transfer(alice_account(), "1".into(), msg); + } + + #[test] + #[should_panic(expected = r#"Payment reference value error"#)] + fn transfer_with_invalid_reference_value() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let mut args = get_default_payment_args(); + args.payment_reference = "0x123".to_string(); + let msg = get_msg_from_args(args); + + contract.ft_on_transfer(alice_account(), "1".into(), msg); + } + + #[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(100), 10u64.pow(14), false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let args = get_default_payment_args(); + let msg = get_msg_from_args(args); + + contract.ft_on_transfer(alice_account(), "1".into(), msg); + } + + #[test] + #[should_panic(expected = r#"Incorrect msg format"#)] + fn transfer_with_invalid_msg_format() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let args = get_default_payment_args(); + let msg = get_msg_from_args(args) + "."; + + contract.ft_on_transfer(alice_account(), "1".into(), msg); + } + + #[test] + #[should_panic(expected = r#"Incorrect msg format"#)] + fn transfer_with_msg_missing_fields() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let args = get_default_payment_args(); + let msg = get_msg_from_args(args).replace("\"amount\":\"1000000\",", ""); + + contract.ft_on_transfer(alice_account(), "1".into(), msg); + } + + #[test] + #[should_panic(expected = r#"amount smaller than fee_amount"#)] + fn transfer_less_than_fee_amount() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let args = get_default_payment_args(); + let msg = get_msg_from_args(args); + + contract.ft_on_transfer(alice_account(), "1".into(), msg); + } + + #[test] + fn transfer_with_reference() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, false); + testing_env!(context); + let mut contract = FungibleProxy::default(); + + let args = get_default_payment_args(); + let msg = get_msg_from_args(args); + + contract.ft_on_transfer(alice_account(), "1000001".into(), msg); + } + + #[test] + fn test_get_transfer_with_reference_args() { + let context = get_context(alice_account(), ntoy(100), MIN_GAS, true); + testing_env!(context); + + let expected_msg = r#"{"amount":"1000000","fee_address":"fee.requestfinance.near","fee_amount":"200","payment_reference":"abc7c8bb1234fd12","to":"dummy.payee.near"}"#; + let msg: String = get_default_payment_args().into(); + assert_eq!(msg, expected_msg); + } +} diff --git a/fungible_proxy/test.sh b/fungible_proxy/test.sh new file mode 100755 index 0000000..d90662f --- /dev/null +++ b/fungible_proxy/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cargo test -- --nocapture \ No newline at end of file diff --git a/tests/sim/main.rs b/tests/sim/conversion_proxy.rs similarity index 100% rename from tests/sim/main.rs rename to tests/sim/conversion_proxy.rs From 85c3d263022e795eeec9887e6287cf060e94f0ab Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 14:11:21 +0100 Subject: [PATCH 02/14] split tests and renames --- test.sh | 2 +- tests/sim/conversion_proxy.rs | 408 +----------------------- tests/sim/fungible_conversion_proxy.rs | 420 +++++++++++++++++++++++++ tests/sim/main.rs | 3 + 4 files changed, 426 insertions(+), 407 deletions(-) create mode 100644 tests/sim/fungible_conversion_proxy.rs create mode 100644 tests/sim/main.rs diff --git a/test.sh b/test.sh index 0d12522..133e6f4 100755 --- a/test.sh +++ b/test.sh @@ -2,4 +2,4 @@ cd mocks/ ./build.sh cd .. ./build.sh -cargo test -- --nocapture \ No newline at end of file +cargo test \ No newline at end of file diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 8e6c93f..01942ed 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -1,8 +1,6 @@ use crate::utils::*; use conversion_proxy::ConversionProxyContract; -use fungible_conversion_proxy::FungibleConversionProxyContract; use mocks::fpo_oracle_mock::FPOContractContract; -use mocks::fungible_token_mock::FungibleTokenContractContract; use near_sdk::json_types::{U128, U64}; use near_sdk::Balance; use near_sdk_sim::init_simulator; @@ -16,19 +14,13 @@ use std::str; near_sdk::setup_alloc!(); const PROXY_ID: &str = "request_proxy"; -const FUNGIBLE_PROXY_ID: &str = "fungible_request_proxy"; lazy_static_include::lazy_static_include_bytes! { REQUEST_PROXY_BYTES => "out/conversion_proxy.wasm" } -lazy_static_include::lazy_static_include_bytes! { - FUNGIBLE_REQUEST_PROXY_BYTES => "out/fungible_conversion_proxy.wasm" -} lazy_static_include::lazy_static_include_bytes! { MOCKED_BYTES => "out/mocks.wasm" } -mod utils; - const DEFAULT_BALANCE: &str = "400000"; // Initialize test environment with 3 accounts (alice, bob, builder) and a conversion mock. @@ -75,65 +67,8 @@ fn init() -> ( (account, empty_account_1, empty_account_2, request_proxy) } -// Initialize test environment with 3 accounts (alice, bob, builder), a fungible conversion mock, and a fungible token mock. -fn init_fungible() -> ( - UserAccount, - UserAccount, - UserAccount, - ContractAccount, - ContractAccount, -) { - let genesis = GenesisConfig::default(); - let root = init_simulator(Some(genesis)); - - deploy!( - contract: FPOContractContract, - contract_id: "mockedfpo".to_string(), - bytes: &MOCKED_BYTES, - signer_account: root, - deposit: to_yocto("7") - ); - - let ft_contract = deploy!( - contract: FungibleTokenContractContract, - contract_id: "mockedft".to_string(), - bytes: &MOCKED_BYTES, - signer_account: root, - deposit: to_yocto("7") - ); - - let account = root.create_user("alice".to_string(), to_yocto(DEFAULT_BALANCE)); - let empty_account_1 = root.create_user("bob".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); - let empty_account_2 = root.create_user("builder".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); - - let fungible_request_proxy = deploy!( - contract: FungibleConversionProxyContract, - contract_id: FUNGIBLE_PROXY_ID, - bytes: &FUNGIBLE_REQUEST_PROXY_BYTES, - signer_account: root, - deposit: to_yocto("5"), - init_method: new("mockedfpo".into(), "any".into()) - ); - - let get_oracle_result = call!(root, fungible_request_proxy.get_oracle_account()); - get_oracle_result.assert_success(); - - debug_assert_eq!( - &get_oracle_result.unwrap_json_value().to_owned(), - &"mockedfpo".to_string() - ); - - ( - account, - empty_account_1, - empty_account_2, - fungible_request_proxy, - ft_contract, - ) -} - #[test] -fn test_transfer_usd_near() { +fn test_transfer() { let (alice, bob, builder, request_proxy) = init(); let initial_alice_balance = alice.account().unwrap().amount; let initial_bob_balance = bob.account().unwrap().amount; @@ -257,7 +192,7 @@ fn test_transfer_with_wrong_currency() { } #[test] -fn test_transfer_zero_usd_near() { +fn test_transfer_zero_usd() { let (alice, bob, builder, request_proxy) = init(); let initial_alice_balance = alice.account().unwrap().amount; let initial_bob_balance = bob.account().unwrap().amount; @@ -321,342 +256,3 @@ fn test_outdated_rate() { ); assert_one_promise_error(result, "Conversion rate too old"); } - -// Helper function for setting up fungible token transfer tests -fn fungible_transfer_setup( - alice: &UserAccount, - bob: &UserAccount, - builder: &UserAccount, - ft_contract: &ContractAccount, - send_amt: U128, -) -> (u128, u128, u128) { - // Register alice, bob, builder, and the contract with the fungible token - call!(alice, ft_contract.register_account(alice.account_id())); - call!(bob, ft_contract.register_account(bob.account_id())); - call!(builder, ft_contract.register_account(builder.account_id())); - call!( - builder, - ft_contract.register_account(FUNGIBLE_PROXY_ID.into()) - ); - - // Set initial balances - call!( - alice, - ft_contract.set_balance(alice.account_id(), 1000000000.into()) // 1000 USDC.e - ); - call!( - bob, - ft_contract.set_balance(bob.account_id(), 1000000.into()) // 1 USDC.e - ); - call!( - builder, - ft_contract.set_balance(builder.account_id(), 0.into()) // 0 USDC.e - ); - call!( - builder, - ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), 0.into()) // 0 USDC.e - ); - - let alice_balance_before = call!(alice, ft_contract.ft_balance_of(alice.account_id())) - .unwrap_json::() - .0; - let bob_balance_before = call!(bob, ft_contract.ft_balance_of(bob.account_id())) - .unwrap_json::() - .0; - let builder_balance_before = call!(builder, ft_contract.ft_balance_of(builder.account_id())) - .unwrap_json::() - .0; - - // In real usage, the user calls `ft_transfer_call` on the token contract, which calls `ft_on_transfer` on our contract - // The token contract will transfer the specificed tokens from the caller to our contract before calling our contract - call!( - alice, - ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), send_amt) - ); - call!( - alice, - ft_contract.set_balance( - alice.account_id(), - U128::from(alice_balance_before - send_amt.0) - ) - ); - - ( - alice_balance_before, - bob_balance_before, - builder_balance_before, - ) -} - -#[test] -fn test_transfer_usd_fungible() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); - - let send_amt = U128::from(500000000); // 500 USDC.e - let (alice_balance_before, bob_balance_before, builder_balance_before) = - fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); - - // Constuct the `msg` argument using our contract - // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder - let get_args = call!( - alice, - fungible_request_proxy.get_transfer_with_reference_args( - 10000.into(), // 100 USD - "USD".into(), - "builder".to_string().try_into().unwrap(), - 200.into(), // 2 USD - 0.into(), - "abc7c8bb1234fd12".into(), - bob.account_id().try_into().unwrap() - ) - ); - get_args.assert_success(); - let msg = get_args.unwrap_json::().replace("\\", ""); - - let result = call!( - ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) - ); - result.assert_success(); - let change = result.unwrap_json::().parse::().unwrap(); - - let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) - .unwrap_json::() - .0 - + change; - let bob_balance_after = call!(bob, ft_contract.ft_balance_of(bob.account_id())) - .unwrap_json::() - .0; - let builder_balance_after = call!(builder, ft_contract.ft_balance_of(builder.account_id())) - .unwrap_json::() - .0; - - // USDC.e has 6 decimals - let total_usdce_amount = 102 * 1000000; // 102 USD - let payment_usdce_amount = 100 * 1000000; // 100 USD - let fee_usdce_amount = 2 * 1000000; // 2 USD - - // The price of USDC.e returned by the oracle is 999900 with 6 decimals, so 1 USDC.e = 999900/1000000 USD = 0.9999 USD - // Here we need it the other way (USD in terms of USDC.e), so 1 USD = 1000000/999900 USDC.e = 1.00010001 USDC.e - let rate_numerator = 1000000; - let rate_denominator = 999900; - - assert!(alice_balance_after < alice_balance_before); - let spent_amount = alice_balance_before - alice_balance_after; - let expected_spent = total_usdce_amount * rate_numerator / rate_denominator; - assert!(spent_amount == expected_spent); - - assert!(bob_balance_after > bob_balance_before); - let received_amount = bob_balance_after - bob_balance_before; - let expected_received = payment_usdce_amount * rate_numerator / rate_denominator; - assert!(received_amount == expected_received); - - assert!(builder_balance_after > builder_balance_before); - let received_amount = builder_balance_after - builder_balance_before; - let expected_received = fee_usdce_amount * rate_numerator / rate_denominator; - assert!(received_amount == expected_received); -} - -#[test] -fn test_transfer_usd_fungible_not_enough() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); - - let send_amt = U128::from(500000000); // 500 USDC.e - let (_, _, _) = fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); - - // Constuct the `msg` argument using our contract - // Transferring 1000 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder - // The request is for 1000 USD, but alice only sends in 500 USDC.e - let get_args = call!( - alice, - fungible_request_proxy.get_transfer_with_reference_args( - 100000.into(), // 1000 USD - "USD".into(), - "builder".to_string().try_into().unwrap(), - 200.into(), // 2 USD - 0.into(), - "abc7c8bb1234fd12".into(), - bob.account_id().try_into().unwrap() - ) - ); - get_args.assert_success(); - let msg = get_args.unwrap_json::().replace("\\", ""); - - let result = call!( - ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) - ); - assert_one_promise_error(result, "Deposit too small") -} - -#[test] -fn test_transfer_usd_fungible_receiver_send_failed() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); - - let send_amt = U128::from(500000000); // 500 USDC.e - let (alice_balance_before, _, _) = - fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); - - // Previous line registers all accounts, so we unregister bob here - // Receiver is not registered with the token contract, so sending to it will fail - call!(bob, ft_contract.unregister_account(bob.account_id())); - - // Constuct the `msg` argument using our contract - // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder - let get_args = call!( - alice, - fungible_request_proxy.get_transfer_with_reference_args( - 10000.into(), // 100 USD - "USD".into(), - "builder".to_string().try_into().unwrap(), - 200.into(), // 2 USD - 0.into(), - "abc7c8bb1234fd12".into(), - bob.account_id().try_into().unwrap() - ) - ); - get_args.assert_success(); - let msg = get_args.unwrap_json::().replace("\\", ""); - - let result = call!( - ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) - ); - result.assert_success(); - let change = result.unwrap_json::().parse::().unwrap(); - - let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) - .unwrap_json::() - .0 - + change; - - // Ensure no balance changes / all funds returned to sender - assert!(alice_balance_after == alice_balance_before); -} - -#[test] -fn test_transfer_usd_fungible_fee_receiver_send_failed() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); - - let send_amt = U128::from(500000000); // 500 USDC.e - let (alice_balance_before, _, _) = - fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); - - // Previous line registers all accounts, so we unregister builder here - // Fee_receiver is not registered with the token contract, so sending to it will fail - call!( - builder, - ft_contract.unregister_account(builder.account_id()) - ); - - // Constuct the `msg` argument using our contract - // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder - let get_args = call!( - alice, - fungible_request_proxy.get_transfer_with_reference_args( - 10000.into(), // 100 USD - "USD".into(), - "builder".to_string().try_into().unwrap(), - 200.into(), // 2 USD - 0.into(), - "abc7c8bb1234fd12".into(), - bob.account_id().try_into().unwrap() - ) - ); - get_args.assert_success(); - let msg = get_args.unwrap_json::().replace("\\", ""); - - let result = call!( - ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) - ); - result.assert_success(); - let change = result.unwrap_json::().parse::().unwrap(); - - let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) - .unwrap_json::() - .0 - + change; - - // Ensure no balance changes / all funds returned to sender - assert!(alice_balance_after == alice_balance_before); -} - -#[test] -fn test_zero_usd_fungible() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); - - let send_amt = U128::from(0); // 0 USDC.e - let (alice_balance_before, bob_balance_before, builder_balance_before) = - fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); - - // Constuct the `msg` argument using our contract - // Transferring 0 USD worth of USDC.e from alice to bob, with a 0 USD fee to builder - let get_args = call!( - alice, - fungible_request_proxy.get_transfer_with_reference_args( - 0.into(), // 0 USD - "USD".into(), - "builder".to_string().try_into().unwrap(), - 0.into(), // 0 USD - 0.into(), - "abc7c8bb1234fd12".into(), - bob.account_id().try_into().unwrap() - ) - ); - get_args.assert_success(); - let msg = get_args.unwrap_json::().replace("\\", ""); - - let result = call!( - ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) - ); - result.assert_success(); - let change = result.unwrap_json::().parse::().unwrap(); - - let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) - .unwrap_json::() - .0 - + change; - let bob_balance_after = call!(bob, ft_contract.ft_balance_of(bob.account_id())) - .unwrap_json::() - .0; - let builder_balance_after = call!(builder, ft_contract.ft_balance_of(builder.account_id())) - .unwrap_json::() - .0; - - assert!(alice_balance_after == alice_balance_before); - assert!(bob_balance_after == bob_balance_before); - assert!(builder_balance_after == builder_balance_before); -} - -#[test] -fn test_outdated_rate_fungible() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); - - let send_amt = U128::from(500000000); // 500 USDC.e - fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); - - // Constuct the `msg` argument using our contract - // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder - let get_args = call!( - alice, - fungible_request_proxy.get_transfer_with_reference_args( - 10000.into(), // 100 USD - "USD".into(), - "builder".to_string().try_into().unwrap(), - 200.into(), // 2 USD - 1.into(), // 1 ns - "abc7c8bb1234fd12".into(), - bob.account_id().try_into().unwrap() - ) - ); - get_args.assert_success(); - let msg = get_args.unwrap_json::().replace("\\", ""); - - let result = call!( - ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) - ); - assert_one_promise_error(result, "Conversion rate too old"); -} diff --git a/tests/sim/fungible_conversion_proxy.rs b/tests/sim/fungible_conversion_proxy.rs new file mode 100644 index 0000000..da2e919 --- /dev/null +++ b/tests/sim/fungible_conversion_proxy.rs @@ -0,0 +1,420 @@ +use crate::utils::*; +use fungible_conversion_proxy::FungibleConversionProxyContract; +use mocks::fpo_oracle_mock::FPOContractContract; +use mocks::fungible_token_mock::FungibleTokenContractContract; +use near_sdk::json_types::{U128}; +use near_sdk_sim::init_simulator; +use near_sdk_sim::runtime::GenesisConfig; +use near_sdk_sim::ContractAccount; +use near_sdk_sim::UserAccount; +use near_sdk_sim::{call, deploy, lazy_static_include, to_yocto}; +use std::convert::TryInto; +use std::str; + +near_sdk::setup_alloc!(); + +const FUNGIBLE_PROXY_ID: &str = "fungible_request_proxy"; +lazy_static_include::lazy_static_include_bytes! { + FUNGIBLE_REQUEST_PROXY_BYTES => "out/fungible_conversion_proxy.wasm" +} +lazy_static_include::lazy_static_include_bytes! { + MOCKED_BYTES => "out/mocks.wasm" +} + +const DEFAULT_BALANCE: &str = "400000"; + +// Initialize test environment with 3 accounts (alice, bob, builder), a fungible conversion mock, and a fungible token mock. +fn init_fungible() -> ( + UserAccount, + UserAccount, + UserAccount, + ContractAccount, + ContractAccount, +) { + let genesis = GenesisConfig::default(); + let root = init_simulator(Some(genesis)); + + deploy!( + contract: FPOContractContract, + contract_id: "mockedfpo".to_string(), + bytes: &MOCKED_BYTES, + signer_account: root, + deposit: to_yocto("7") + ); + + let ft_contract = deploy!( + contract: FungibleTokenContractContract, + contract_id: "mockedft".to_string(), + bytes: &MOCKED_BYTES, + signer_account: root, + deposit: to_yocto("7") + ); + + let account = root.create_user("alice".to_string(), to_yocto(DEFAULT_BALANCE)); + let empty_account_1 = root.create_user("bob".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); + let empty_account_2 = root.create_user("builder".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); + + let fungible_request_proxy = deploy!( + contract: FungibleConversionProxyContract, + contract_id: FUNGIBLE_PROXY_ID, + bytes: &FUNGIBLE_REQUEST_PROXY_BYTES, + signer_account: root, + deposit: to_yocto("5"), + init_method: new("mockedfpo".into(), "any".into()) + ); + + let get_oracle_result = call!(root, fungible_request_proxy.get_oracle_account()); + get_oracle_result.assert_success(); + + debug_assert_eq!( + &get_oracle_result.unwrap_json_value().to_owned(), + &"mockedfpo".to_string() + ); + + ( + account, + empty_account_1, + empty_account_2, + fungible_request_proxy, + ft_contract, + ) +} + +// Helper function for setting up fungible token transfer tests +fn fungible_transfer_setup( + alice: &UserAccount, + bob: &UserAccount, + builder: &UserAccount, + ft_contract: &ContractAccount, + send_amt: U128, +) -> (u128, u128, u128) { + // Register alice, bob, builder, and the contract with the fungible token + call!(alice, ft_contract.register_account(alice.account_id())); + call!(bob, ft_contract.register_account(bob.account_id())); + call!(builder, ft_contract.register_account(builder.account_id())); + call!( + builder, + ft_contract.register_account(FUNGIBLE_PROXY_ID.into()) + ); + + // Set initial balances + call!( + alice, + ft_contract.set_balance(alice.account_id(), 1000000000.into()) // 1000 USDC.e + ); + call!( + bob, + ft_contract.set_balance(bob.account_id(), 1000000.into()) // 1 USDC.e + ); + call!( + builder, + ft_contract.set_balance(builder.account_id(), 0.into()) // 0 USDC.e + ); + call!( + builder, + ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), 0.into()) // 0 USDC.e + ); + + let alice_balance_before = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0; + let bob_balance_before = call!(bob, ft_contract.ft_balance_of(bob.account_id())) + .unwrap_json::() + .0; + let builder_balance_before = call!(builder, ft_contract.ft_balance_of(builder.account_id())) + .unwrap_json::() + .0; + + // In real usage, the user calls `ft_transfer_call` on the token contract, which calls `ft_on_transfer` on our contract + // The token contract will transfer the specificed tokens from the caller to our contract before calling our contract + call!( + alice, + ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), send_amt) + ); + call!( + alice, + ft_contract.set_balance( + alice.account_id(), + U128::from(alice_balance_before - send_amt.0) + ) + ); + + ( + alice_balance_before, + bob_balance_before, + builder_balance_before, + ) +} + +#[test] +fn test_transfer() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (alice_balance_before, bob_balance_before, builder_balance_before) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Constuct the `msg` argument using our contract + // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder + let get_args = call!( + alice, + fungible_request_proxy.get_transfer_with_reference_args( + 10000.into(), // 100 USD + "USD".into(), + "builder".to_string().try_into().unwrap(), + 200.into(), // 2 USD + 0.into(), + "abc7c8bb1234fd12".into(), + bob.account_id().try_into().unwrap() + ) + ); + get_args.assert_success(); + let msg = get_args.unwrap_json::().replace("\\", ""); + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + ); + result.assert_success(); + let change = result.unwrap_json::().parse::().unwrap(); + + let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0 + + change; + let bob_balance_after = call!(bob, ft_contract.ft_balance_of(bob.account_id())) + .unwrap_json::() + .0; + let builder_balance_after = call!(builder, ft_contract.ft_balance_of(builder.account_id())) + .unwrap_json::() + .0; + + // USDC.e has 6 decimals + let total_usdce_amount = 102 * 1000000; // 102 USD + let payment_usdce_amount = 100 * 1000000; // 100 USD + let fee_usdce_amount = 2 * 1000000; // 2 USD + + // The price of USDC.e returned by the oracle is 999900 with 6 decimals, so 1 USDC.e = 999900/1000000 USD = 0.9999 USD + // Here we need it the other way (USD in terms of USDC.e), so 1 USD = 1000000/999900 USDC.e = 1.00010001 USDC.e + let rate_numerator = 1000000; + let rate_denominator = 999900; + + assert!(alice_balance_after < alice_balance_before); + let spent_amount = alice_balance_before - alice_balance_after; + let expected_spent = total_usdce_amount * rate_numerator / rate_denominator; + assert!(spent_amount == expected_spent); + + assert!(bob_balance_after > bob_balance_before); + let received_amount = bob_balance_after - bob_balance_before; + let expected_received = payment_usdce_amount * rate_numerator / rate_denominator; + assert!(received_amount == expected_received); + + assert!(builder_balance_after > builder_balance_before); + let received_amount = builder_balance_after - builder_balance_before; + let expected_received = fee_usdce_amount * rate_numerator / rate_denominator; + assert!(received_amount == expected_received); +} + +#[test] +fn test_transfer_not_enough() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (_, _, _) = fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Constuct the `msg` argument using our contract + // Transferring 1000 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder + // The request is for 1000 USD, but alice only sends in 500 USDC.e + let get_args = call!( + alice, + fungible_request_proxy.get_transfer_with_reference_args( + 100000.into(), // 1000 USD + "USD".into(), + "builder".to_string().try_into().unwrap(), + 200.into(), // 2 USD + 0.into(), + "abc7c8bb1234fd12".into(), + bob.account_id().try_into().unwrap() + ) + ); + get_args.assert_success(); + let msg = get_args.unwrap_json::().replace("\\", ""); + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + ); + assert_one_promise_error(result, "Deposit too small") +} + +#[test] +fn test_transfer_receiver_send_failed() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (alice_balance_before, _, _) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Previous line registers all accounts, so we unregister bob here + // Receiver is not registered with the token contract, so sending to it will fail + call!(bob, ft_contract.unregister_account(bob.account_id())); + + // Constuct the `msg` argument using our contract + // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder + let get_args = call!( + alice, + fungible_request_proxy.get_transfer_with_reference_args( + 10000.into(), // 100 USD + "USD".into(), + "builder".to_string().try_into().unwrap(), + 200.into(), // 2 USD + 0.into(), + "abc7c8bb1234fd12".into(), + bob.account_id().try_into().unwrap() + ) + ); + get_args.assert_success(); + let msg = get_args.unwrap_json::().replace("\\", ""); + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + ); + result.assert_success(); + let change = result.unwrap_json::().parse::().unwrap(); + + let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0 + + change; + + // Ensure no balance changes / all funds returned to sender + assert!(alice_balance_after == alice_balance_before); +} + +#[test] +fn test_transfer_fee_receiver_send_failed() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (alice_balance_before, _, _) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Previous line registers all accounts, so we unregister builder here + // Fee_receiver is not registered with the token contract, so sending to it will fail + call!( + builder, + ft_contract.unregister_account(builder.account_id()) + ); + + // Constuct the `msg` argument using our contract + // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder + let get_args = call!( + alice, + fungible_request_proxy.get_transfer_with_reference_args( + 10000.into(), // 100 USD + "USD".into(), + "builder".to_string().try_into().unwrap(), + 200.into(), // 2 USD + 0.into(), + "abc7c8bb1234fd12".into(), + bob.account_id().try_into().unwrap() + ) + ); + get_args.assert_success(); + let msg = get_args.unwrap_json::().replace("\\", ""); + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + ); + result.assert_success(); + let change = result.unwrap_json::().parse::().unwrap(); + + let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0 + + change; + + // Ensure no balance changes / all funds returned to sender + assert!(alice_balance_after == alice_balance_before); +} + +#[test] +fn test_transfer_zero_usd() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(0); // 0 USDC.e + let (alice_balance_before, bob_balance_before, builder_balance_before) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Constuct the `msg` argument using our contract + // Transferring 0 USD worth of USDC.e from alice to bob, with a 0 USD fee to builder + let get_args = call!( + alice, + fungible_request_proxy.get_transfer_with_reference_args( + 0.into(), // 0 USD + "USD".into(), + "builder".to_string().try_into().unwrap(), + 0.into(), // 0 USD + 0.into(), + "abc7c8bb1234fd12".into(), + bob.account_id().try_into().unwrap() + ) + ); + get_args.assert_success(); + let msg = get_args.unwrap_json::().replace("\\", ""); + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + ); + result.assert_success(); + let change = result.unwrap_json::().parse::().unwrap(); + + let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0 + + change; + let bob_balance_after = call!(bob, ft_contract.ft_balance_of(bob.account_id())) + .unwrap_json::() + .0; + let builder_balance_after = call!(builder, ft_contract.ft_balance_of(builder.account_id())) + .unwrap_json::() + .0; + + assert!(alice_balance_after == alice_balance_before); + assert!(bob_balance_after == bob_balance_before); + assert!(builder_balance_after == builder_balance_before); +} + +#[test] +fn test_outdated_rate() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Constuct the `msg` argument using our contract + // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder + let get_args = call!( + alice, + fungible_request_proxy.get_transfer_with_reference_args( + 10000.into(), // 100 USD + "USD".into(), + "builder".to_string().try_into().unwrap(), + 200.into(), // 2 USD + 1.into(), // 1 ns + "abc7c8bb1234fd12".into(), + bob.account_id().try_into().unwrap() + ) + ); + get_args.assert_success(); + let msg = get_args.unwrap_json::().replace("\\", ""); + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + ); + assert_one_promise_error(result, "Conversion rate too old"); +} diff --git a/tests/sim/main.rs b/tests/sim/main.rs new file mode 100644 index 0000000..47b0327 --- /dev/null +++ b/tests/sim/main.rs @@ -0,0 +1,3 @@ +mod utils; +mod fungible_conversion_proxy; +mod conversion_proxy; \ No newline at end of file From 49ee32e9afd6d65b954b944f333b78d6cf8bfff7 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 15:25:15 +0100 Subject: [PATCH 03/14] build and Cargo --- Cargo.lock | 1 + Cargo.toml | 1 + build.sh | 1 + 3 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 234e506..39107b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2371,6 +2371,7 @@ version = "0.1.0" dependencies = [ "conversion_proxy", "fungible_conversion_proxy", + "fungible_proxy", "hex", "mocks", "near-sdk", diff --git a/Cargo.toml b/Cargo.toml index f017c0e..41b88c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ hex = "0.4" near-sdk-sim = "3.2.0" conversion_proxy = { path = "./conversion_proxy" } fungible_conversion_proxy = { path = "./fungible_conversion_proxy" } +fungible_proxy = { path = "./fungible_proxy" } mocks = { path = "./mocks" } [profile.release] diff --git a/build.sh b/build.sh index caab6ef..9c8b309 100755 --- a/build.sh +++ b/build.sh @@ -5,3 +5,4 @@ RUSTFLAGS='-C link-arg=-s' cargo build --all --target wasm32-unknown-unknown --r mkdir -p ./out cp target/wasm32-unknown-unknown/release/conversion_proxy.wasm ./out/ cp target/wasm32-unknown-unknown/release/fungible_conversion_proxy.wasm ./out/ +cp target/wasm32-unknown-unknown/release/fungible_proxy.wasm ./out/ From e515722f5328760a418e41eb366befbac14d477d Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 15:25:35 +0100 Subject: [PATCH 04/14] README for unit tests --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index ec374a8..a3eb3dd 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,15 @@ Smart contracts on NEAR used by the ## Unit tests +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 ``` @@ -33,7 +37,18 @@ cargo test ## Integration tests ``` +# To test everything ./test.sh + +# To test 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): +cargo test conversion_proxy::test_transfer -- --exact +cargo test fungible_conversionproxy::test_transfer -- --exact +cargo test fungible_proxy::test_transfer -- --exact ``` ## Deploying contract From 983d90ecd87d878d641234f650657d4be97074f9 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 15:25:57 +0100 Subject: [PATCH 05/14] simulated tests --- fungible_proxy/src/lib.rs | 67 +++++---- tests/sim/fungible_proxy.rs | 278 ++++++++++++++++++++++++++++++++++++ tests/sim/main.rs | 3 +- tests/sim/utils.rs | 31 +++- 4 files changed, 342 insertions(+), 37 deletions(-) create mode 100644 tests/sim/fungible_proxy.rs diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index fd5381e..8b35e87 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -9,7 +9,6 @@ near_sdk::setup_alloc!(); const NO_DEPOSIT: Balance = 0; const YOCTO_DEPOSIT: Balance = 1; // Fungible token transfers require a deposit of exactly 1 yoctoNEAR -const ONE_FIAT: Balance = 100; // Fiat values with two decimals const MIN_GAS: Gas = 150_000_000_000_000; const BASIC_GAS: Gas = 10_000_000_000_000; @@ -22,7 +21,6 @@ const BASIC_GAS: Gas = 10_000_000_000_000; /// - `to`: `amount` in `currency` of payment token will be paid to this address #[derive(Serialize, Deserialize)] pub struct PaymentArgs { - pub amount: U128, pub fee_address: ValidAccountId, pub fee_amount: U128, pub payment_reference: String, @@ -78,7 +76,7 @@ impl FungibleTokenReceiver for FungibleProxy { /// This is the function that will be called by the fungible token contract's `ft_transfer_call` function. /// We use the `msg` field to obtain the arguments supplied by the caller specifying the intended payment. /// `msg` should be a string in JSON format containing all the fields in `PaymentArgs`. - /// Eg. msg = {"payment_reference":"abc7c8bb1234fd12","to":"dummy.payee.near","amount":"1000000","currency":"USD","fee_address":"fee.requestfinance.near","fee_amount":"200","max_rate_timespan":"0"} + /// Eg. msg = {"payment_reference":"abc7c8bb1234fd12","to":"dummy.payee.near","fee_address":"fee.requestfinance.near","fee_amount":"200"} /// /// For more information on the fungible token standard, see https://nomicon.io/Standards/Tokens/FungibleToken/Core /// @@ -126,41 +124,40 @@ impl FungibleProxy { assert_eq!(reference_vec.len(), 8, "Incorrect payment reference length"); assert!(args.fee_amount.0 <= amount.0, "amount smaller than fee_amount"); let main_amount = amount.0 - args.fee_amount.0; - - ext_self::on_transfer_with_reference( - args, - token_address, - payer, - main_amount.into(), - &env::current_account_id(), - NO_DEPOSIT, - BASIC_GAS, + let main_transfer_args = + json!({ "receiver_id": args.to.to_string(), "amount":main_amount.to_string(), "memo": None:: }) + .to_string() + .into_bytes(); + + let fee_transfer_args = + json!({ "receiver_id": args.fee_address.to_string(), "amount":args.fee_amount.0.to_string(), "memo": None:: }) + .to_string() + .into_bytes(); + + Promise::new(token_address.to_string()) + .function_call( + "ft_transfer".into(), + main_transfer_args, + YOCTO_DEPOSIT, + BASIC_GAS * 2, + ) + .function_call( + "ft_transfer".into(), + fee_transfer_args, + YOCTO_DEPOSIT, + BASIC_GAS * 2, + ).then(ext_self::on_transfer_with_reference( + args, + token_address, + payer, + main_amount.into(), + &env::current_account_id(), + NO_DEPOSIT, + BASIC_GAS, + ) ) } - /// Convenience function for constructing the `msg` argument for `ft_transfer_call` in the fungible token contract. - /// Constructing the `msg` string could also easily be done on the frontend so is included here just for completeness - /// and convenience. - pub fn get_transfer_with_reference_args( - &self, - amount: U128, - currency: String, - fee_address: ValidAccountId, - fee_amount: U128, - max_rate_timespan: U64, - payment_reference: String, - to: ValidAccountId, - ) -> String { - let args = PaymentArgs { - amount, - fee_address, - fee_amount, - payment_reference, - to, - }; - args.into() - } - #[private] pub fn on_transfer_with_reference( &self, diff --git a/tests/sim/fungible_proxy.rs b/tests/sim/fungible_proxy.rs new file mode 100644 index 0000000..2909ce9 --- /dev/null +++ b/tests/sim/fungible_proxy.rs @@ -0,0 +1,278 @@ +use crate::utils::*; +use fungible_proxy::PaymentArgs; +use fungible_proxy::FungibleProxyContract; +use mocks::fungible_token_mock::FungibleTokenContractContract; +use near_sdk::json_types::{U128}; +use near_sdk_sim::init_simulator; +use near_sdk_sim::runtime::GenesisConfig; +use near_sdk_sim::ContractAccount; +use near_sdk_sim::UserAccount; +use near_sdk_sim::{call, deploy, lazy_static_include, to_yocto}; +use std::convert::TryInto; +use std::ops::Sub; +use std::str; + +near_sdk::setup_alloc!(); + +const FUNGIBLE_PROXY_ID: &str = "fungible_request_proxy"; +lazy_static_include::lazy_static_include_bytes! { + FUNGIBLE_REQUEST_PROXY_BYTES => "out/fungible_proxy.wasm" +} +lazy_static_include::lazy_static_include_bytes! { + MOCKED_BYTES => "out/mocks.wasm" +} + +const DEFAULT_BALANCE: &str = "400000"; + +// Initialize test environment with 3 accounts (alice, bob, builder), a fungible conversion mock, and a fungible token mock. +fn init_fungible() -> ( + UserAccount, + UserAccount, + UserAccount, + ContractAccount, + ContractAccount, +) { + let genesis = GenesisConfig::default(); + let root = init_simulator(Some(genesis)); + + let ft_contract = deploy!( + contract: FungibleTokenContractContract, + contract_id: "mockedft".to_string(), + bytes: &MOCKED_BYTES, + signer_account: root, + deposit: to_yocto("7") + ); + + let account = root.create_user("alice".to_string(), to_yocto(DEFAULT_BALANCE)); + let empty_account_1 = root.create_user("bob".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); + let empty_account_2 = root.create_user("builder".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); + + let fungible_request_proxy = deploy!( + contract: FungibleProxyContract, + contract_id: FUNGIBLE_PROXY_ID, + bytes: &FUNGIBLE_REQUEST_PROXY_BYTES, + signer_account: root, + deposit: to_yocto("5") + ); + + ( + account, + empty_account_1, + empty_account_2, + fungible_request_proxy, + ft_contract, + ) +} + +// Helper function for setting up fungible token transfer tests +fn fungible_transfer_setup( + alice: &UserAccount, + bob: &UserAccount, + builder: &UserAccount, + ft_contract: &ContractAccount, + send_amt: U128, +) -> (u128, u128, u128) { + // Register alice, bob, builder, and the contract with the fungible token + call!(alice, ft_contract.register_account(alice.account_id())); + call!(bob, ft_contract.register_account(bob.account_id())); + call!(builder, ft_contract.register_account(builder.account_id())); + call!( + builder, + ft_contract.register_account(FUNGIBLE_PROXY_ID.into()) + ); + + // Set initial balances + call!( + alice, + ft_contract.set_balance(alice.account_id(), 1000000000.into()) // 1000 USDC.e + ); + call!( + bob, + ft_contract.set_balance(bob.account_id(), 1000000.into()) // 1 USDC.e + ); + call!( + builder, + ft_contract.set_balance(builder.account_id(), 0.into()) // 0 USDC.e + ); + call!( + builder, + ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), 0.into()) // 0 USDC.e + ); + + let alice_balance_before = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0; + let bob_balance_before = call!(bob, ft_contract.ft_balance_of(bob.account_id())) + .unwrap_json::() + .0; + let builder_balance_before = call!(builder, ft_contract.ft_balance_of(builder.account_id())) + .unwrap_json::() + .0; + + // In real usage, the user calls `ft_transfer_call` on the token contract, which calls `ft_on_transfer` on our contract + // The token contract will transfer the specificed tokens from the caller to our contract before calling our contract + call!( + alice, + ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), send_amt) + ); + call!( + alice, + ft_contract.set_balance( + alice.account_id(), + U128::from(alice_balance_before - send_amt.0) + ) + ); + + ( + alice_balance_before, + bob_balance_before, + builder_balance_before, + ) +} + +#[test] +fn test_transfer() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (alice_balance_before, bob_balance_before, builder_balance_before) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + let args = PaymentArgs { + fee_address: "builder".try_into().unwrap(), // TODO to_string optional? + fee_amount: 2000000.into(), // 2 USDC.e + payment_reference: "abc7c8bb1234fd12".into(), + to: bob.account_id().try_into().unwrap(), + }; + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + ); + result.assert_success(); + + // The mocked fungible token does not handle change + let change = result.unwrap_json::().parse::().unwrap(); + assert!(change == 0); + + assert_spent(alice, alice_balance_before, send_amt.into(), &ft_contract); + assert_received(bob, bob_balance_before, 498000000, &ft_contract); + assert_received(builder, builder_balance_before, 2000000, &ft_contract); + +} + +#[test] +fn transfer_less_than_fee_amount() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (_, _, _) = fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + let args = PaymentArgs { + fee_address: "builder".try_into().unwrap(), + fee_amount: 500100000.into(), // 500.10 USDC.e + payment_reference: "abc7c8bb1234fd12".into(), + to: bob.account_id().try_into().unwrap(), + }; + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + ); + assert_one_promise_error(result, "amount smaller than fee_amount") +} + +#[test] +fn test_transfer_receiver_send_failed() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (alice_balance_before, _, _) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Previous line registers all accounts, so we unregister bob here + // Receiver is not registered with the token contract, so sending to it will fail + call!(bob, ft_contract.unregister_account(bob.account_id())); + + // Constuct the `msg` argument using our contract + // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder + let args = PaymentArgs { + fee_address: "builder".try_into().unwrap(), + fee_amount: 200.into(), // 2 USD + payment_reference: "abc7c8bb1234fd12".into(), + to: bob.account_id().try_into().unwrap() + }; + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + ); + result.assert_success(); + let change = result.unwrap_json::().parse::().unwrap(); + + let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) + .unwrap_json::() + .0 + + change; + + // Ensure no balance changes / all funds returned to sender + assert!(alice_balance_after == alice_balance_before); +} + +#[test] +fn test_transfer_fee_receiver_send_failed() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(500000000); // 500 USDC.e + let (alice_balance_before, _, _) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + // Previous line registers all accounts, so we unregister builder here + // Fee_receiver is not registered with the token contract, so sending to it will fail + call!( + builder, + ft_contract.unregister_account(builder.account_id()) + ); + + let args = PaymentArgs { + fee_address: "builder".try_into().unwrap(), + fee_amount: 200.into(), + payment_reference: "abc7c8bb1234fd12".into(), + to: bob.account_id().try_into().unwrap() + }; + + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + ); + result.assert_success(); + // The mocked fungible token does not handle change + let change = result.unwrap_json::().parse::().unwrap(); + + assert_unchanged_balance(alice, alice_balance_before.sub(change), &ft_contract, "Alice"); +} + +#[test] +fn test_transfer_zero_usd() { + let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + + let send_amt = U128::from(0); // 0 USDC.e + let (alice_balance_before, bob_balance_before, builder_balance_before) = + fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); + + let args = PaymentArgs { + fee_address: "builder".try_into().unwrap(), + fee_amount: 0.into(), + payment_reference: "abc7c8bb1234fd12".into(), + to: bob.account_id().try_into().unwrap() + }; + let result = call!( + ft_contract.user_account, + fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + ); + result.assert_success(); + + assert_unchanged_balance(alice, alice_balance_before, &ft_contract, "Alice"); + assert_unchanged_balance(bob, bob_balance_before, &ft_contract, "Bob"); + assert_unchanged_balance(builder, builder_balance_before, &ft_contract, "Builder"); +} diff --git a/tests/sim/main.rs b/tests/sim/main.rs index 47b0327..841b89b 100644 --- a/tests/sim/main.rs +++ b/tests/sim/main.rs @@ -1,3 +1,4 @@ mod utils; mod fungible_conversion_proxy; -mod conversion_proxy; \ No newline at end of file +mod conversion_proxy; +mod fungible_proxy; \ No newline at end of file diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index ec3ec6c..142ccf3 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -1,5 +1,7 @@ +use mocks::fungible_token_mock::FungibleTokenContractContract; +use near_sdk::json_types::U128; use near_sdk_sim::transaction::ExecutionStatus; -use near_sdk_sim::{lazy_static_include, to_yocto, ExecutionResult}; +use near_sdk_sim::{lazy_static_include, to_yocto, ExecutionResult, ContractAccount, call, UserAccount}; lazy_static_include::lazy_static_include_bytes! { REQUEST_PROXY_BYTES => "./out/conversion_proxy.wasm" @@ -20,6 +22,33 @@ pub fn assert_eq_with_gas(left: u128, right: u128) { assert_almost_eq_with_max_delta(left, right, to_yocto("0.005")); } +/// Util to check a balance is the same as in a previous state +pub fn assert_unchanged_balance(account: UserAccount, previous_balance: u128, ft_contract: &ContractAccount, account_name: &str) { + + let current_balance = call!(account, ft_contract.ft_balance_of(account.account_id())) + .unwrap_json::() + .0; + assert!(current_balance == previous_balance, "{}'s balance changed by {} (from {} to {})", account_name, previous_balance-current_balance, previous_balance, current_balance); +} + +/// Util to TODO +pub fn assert_spent(account: UserAccount, previous_balance: u128, expected_spent_amount: u128, ft_contract: &ContractAccount) { + let current_balance = call!(account, ft_contract.ft_balance_of(account.account_id())) + .unwrap_json::() + .0; + assert!(current_balance <= previous_balance, "Did not spend."); + assert!(current_balance == previous_balance -expected_spent_amount, "Spent {} instead of {}", previous_balance - current_balance, expected_spent_amount); +} + +/// Util to TODO +pub fn assert_received(account: UserAccount, previous_balance: u128, expected_received_amount: u128, ft_contract: &ContractAccount) { + let current_balance = call!(account, ft_contract.ft_balance_of(account.account_id())) + .unwrap_json::() + .0; + assert!(current_balance >= previous_balance, "Did not receive."); + assert!(current_balance == previous_balance + expected_received_amount, "Received {} instead of {}", current_balance - previous_balance, expected_received_amount); +} + pub fn assert_one_promise_error(promise_result: ExecutionResult, expected_error_message: &str) { assert_eq!(promise_result.promise_errors().len(), 1); From 4fbcb3965861cab859702f57b0df67f57a6dfe5f Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 15:27:45 +0100 Subject: [PATCH 06/14] cleaned TODOs --- tests/sim/fungible_proxy.rs | 2 +- tests/sim/utils.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/sim/fungible_proxy.rs b/tests/sim/fungible_proxy.rs index 2909ce9..6a96e3e 100644 --- a/tests/sim/fungible_proxy.rs +++ b/tests/sim/fungible_proxy.rs @@ -139,7 +139,7 @@ fn test_transfer() { fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); let args = PaymentArgs { - fee_address: "builder".try_into().unwrap(), // TODO to_string optional? + fee_address: "builder".try_into().unwrap(), fee_amount: 2000000.into(), // 2 USDC.e payment_reference: "abc7c8bb1234fd12".into(), to: bob.account_id().try_into().unwrap(), diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index 142ccf3..ba130db 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -31,7 +31,7 @@ pub fn assert_unchanged_balance(account: UserAccount, previous_balance: u128, ft assert!(current_balance == previous_balance, "{}'s balance changed by {} (from {} to {})", account_name, previous_balance-current_balance, previous_balance, current_balance); } -/// Util to TODO +/// Util to assert that an account has spent a given amount of token. pub fn assert_spent(account: UserAccount, previous_balance: u128, expected_spent_amount: u128, ft_contract: &ContractAccount) { let current_balance = call!(account, ft_contract.ft_balance_of(account.account_id())) .unwrap_json::() @@ -40,7 +40,7 @@ pub fn assert_spent(account: UserAccount, previous_balance: u128, expected_spent assert!(current_balance == previous_balance -expected_spent_amount, "Spent {} instead of {}", previous_balance - current_balance, expected_spent_amount); } -/// Util to TODO +/// Util to assert that an account has received a given amount of token. pub fn assert_received(account: UserAccount, previous_balance: u128, expected_received_amount: u128, ft_contract: &ContractAccount) { let current_balance = call!(account, ft_contract.ft_balance_of(account.account_id())) .unwrap_json::() From 23eec91edf2bcd6428a270d871b7f549a258448b Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 16:26:20 +0100 Subject: [PATCH 07/14] deploy.sh multi-contract --- deploy.sh | 66 ++++++++++++++++++++++----------------- fungible_proxy/src/lib.rs | 2 +- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/deploy.sh b/deploy.sh index ad4a2c2..8b17896 100755 --- a/deploy.sh +++ b/deploy.sh @@ -6,44 +6,54 @@ NEAR_ENV="testnet" oracle_account_id="fpo.opfilabs.testnet" provider_account_id="opfilabs.testnet" +contract_name="conversion_proxy"; -while getopts ":a:ph" opt; do - case $opt in - h) +die() { echo "$*" >&2; exit 2; } # complain to STDERR and exit with error +needs_arg() { if [ -z "$OPTARG" ]; then die "Missing arg for --$OPT option"; fi; } + +while getopts "pha:-:" OPT; do + if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG + OPT="${OPTARG%%=*}" # extract long option name + OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty) + OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=` + fi + case "$OPT" in + h | help) echo "Builds and deploys the contract with state initialization (first deployement)." echo "Defaults to testnet." echo "" echo "Options:" - echo " -p : for prod deployment" - echo " -a [account_id] : to override \$ACCOUNT_ID" + echo " -p | --mainnet : for prod deployment" + echo " -a [account_id] : to override \$ACCOUNT_ID" + echo " Choose the contract to deploy with:" + echo " --conversion_proxy [default]" + echo " --fungible_proxy" + echo " --fungible_conversionproxy" exit 0 ;; - p) - NEAR_ENV="mainnet" - oracle_account_id="fpo.opfilabs.near" - provider_account_id="opfilabs.near" - ;; - a) - ACCOUNT_ID="$OPTARG" - ;; - \?) - echo "Invalid option -$OPTARG" >&2 - exit 1 - ;; - :) - echo "Option -$OPTARG needs a valid argument" - exit 1 - ;; + p | prod | mainnet) NEAR_ENV="mainnet" ;; + a | account_id) needs_arg; ACCOUNT_ID="$OPTARG" ;; + conversion_proxy | fungible_proxy | fungible_conversion_proxy) contract_name="$OPT" ;; + ??* ) die "Unknown option --$OPT" ;; # bad long option + ? ) exit 2 ;; # bad short option (error reported via getopts) esac - done -printf "NEAR_ENV=%s\n" "$NEAR_ENV" -printf "ACCOUNT_ID=%s\n" "$ACCOUNT_ID" +if [ "$ACCOUNT_ID" = "" ]; then + echo "Missing account ID"; + exit 1; +fi + +printf "Deploying %s on NEAR_ENV=%s with ACCOUNT_ID=%s\n\n" "$contract_name" "$NEAR_ENV" "$ACCOUNT_ID" ./build.sh -near deploy -f --wasmFile ./out/conversion_proxy.wasm \ - --accountId $ACCOUNT_ID \ - --initFunction new \ - --initArgs '{"oracle_account_id": "'$oracle_account_id'", "provider_account_id": "'$provider_account_id'"}' \ No newline at end of file +if [ "$contract_name" = "fungible_proxy" ]; then + near deploy -f --wasmFile ./out/$contract_name.wasm \ + --accountId $ACCOUNT_ID +else + near deploy -f --wasmFile ./out/$contract_name.wasm \ + --accountId $ACCOUNT_ID \ + --initFunction new \ + --initArgs '{"oracle_account_id": "'$oracle_account_id'", "provider_account_id": "'$provider_account_id'"}' +fi \ No newline at end of file diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index 8b35e87..1a5ded5 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -1,5 +1,5 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::json_types::{ValidAccountId, U128, U64}; +use near_sdk::json_types::{ValidAccountId, U128}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::serde_json::json; use near_sdk::{ From c31be0e895a9e4d3f7b3ff14ddfad34edea0d9f2 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 17:38:25 +0100 Subject: [PATCH 08/14] README: FAU --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3eb3dd..937d746 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,36 @@ cargo ## Calling contract -The snippet below makes a NEAR payment for $80.50, with a $1.00 fee. +Commands below assumes a few variables are set: `ACCOUNT_ID`, `BUILDER_ID` and `ISSUER_ID`. + +This snippet makes a NEAR payment for $80.50, with a $1.00 fee. ``` -# set ACCOUNT_ID, BUILDER_ID and ISSUER_ID near call $ACCOUNT_ID transfer_with_reference '{"to": "'$ISSUER_ID'", "payment_reference": "0x1230012300001234", "amount": "8050", "currency": "USD", "fee_amount": "100", "fee_address": "'$BUILDER_ID'"}' --accountId $ACCOUNT_ID --gas 300000000000000 --deposit 30 ``` + +This snippet makes a fungible token payment, given that `fau.reqnetwork.testnet` is a fungible token address and the `fungible_proxy` contract is deployed at `pay.reqnetwork.testnet`. + +``` +near call fau.reqnetwork.testnet ft_transfer_call '{"receiver_id": "pay.reqnetwork.testnet", "amount": "2500000000000000000", "msg": "{\"fee_address\": \"'$BUILDER_ID'\", \"fee_amount\": \"1000000000000000000\", \"payment_reference\": \"abc7c8bb1234fd12\", \"to\": \"'$ISSUER_ID'\"}"}' --accountId $ACCOUNT_ID --depositYocto 1 --gas 300000000000000 +``` + +## FAU tokens (testnet) + +The FAU token at `fau.reqnetwork.testnet` has 18 decimals and a total supply of 1'000'000.00 FAU. +It is based on the example at https://github.com/near/near-sdk-rs/tree/master/examples/fungible-token, slightly updated and deployed using the commands: + +``` +./build.sh +near create-account fau.reqnetwork.testnet --masterAccount reqnetwork.testnet --initialBalance 8 +near deploy -f --wasmFile ./res/fungible_token.wasm --accountId fau.reqnetwork.testnet --initFunction new_default_meta --initArgs '{"owner_id": "reqnetwork.testnet", "total_supply": "1000000000000000000000000"}' +``` + +Get some FAU: + +``` +# Register the account +near call fau.reqnetwork.testnet storage_deposit '{"account_id": "'$ACCOUNT_ID'"}' --accountId $ACCOUNT_ID --amount 0.005 +# Transfer 1000.00 FAU to the account +near call fau.reqnetwork.testnet ft_transfer '{"receiver_id": "'$ACCOUNT_ID'", "amount": "1000000000000000000000"}' --accountId reqnetwork.testnet --depositYocto 1 +``` From 1dfce141566a1ca7e7cf13d4229c000691eb4180 Mon Sep 17 00:00:00 2001 From: yomarion Date: Thu, 16 Mar 2023 17:46:55 +0100 Subject: [PATCH 09/14] README FAU on new proxy --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 937d746..f723b02 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,11 @@ near call fau.reqnetwork.testnet storage_deposit '{"account_id": "'$ACCOUNT_ID'" # Transfer 1000.00 FAU to the account near call fau.reqnetwork.testnet ft_transfer '{"receiver_id": "'$ACCOUNT_ID'", "amount": "1000000000000000000000"}' --accountId reqnetwork.testnet --depositYocto 1 ``` + +To use FAU on a new proxy, you need to register it first: + +``` + near call fau.reqnetwork.testnet storage_deposit '{"account_id": "'$PROXY_ADDRESS'"}' --accountId $ACCOUNT_ID --amount 0.005 +``` + +You need to run the same command for every account before they receive FAU, or the smart contract will panick with the error message `The account some_account is not registered`. From 8649fd6c7e010e8af8b498d3d732b2b1bd1b0939 Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 4 Apr 2023 17:00:07 +0200 Subject: [PATCH 10/14] fix: fungible-proxy unit tests --- .github/workflows/build-and-test.yml | 3 +++ fungible_proxy/src/lib.rs | 11 +++++------ fungible_proxy/test.sh | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8b134b2..3b73fd9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -21,6 +21,9 @@ jobs: - name: Build and run conversion proxy unit tests working-directory: ./conversion_proxy run: cargo test + - name: Build and run fungible proxy unit tests + working-directory: ./fungible_proxy + run: cargo test - name: Build and run fungible conversion proxy unit tests working-directory: ./fungible_conversion_proxy run: cargo test diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index 1a5ded5..4430261 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -237,7 +237,6 @@ mod tests { /// Helper function: get default values for PaymentArgs fn get_default_payment_args() -> PaymentArgs { PaymentArgs { - amount: 1000000.into(), fee_address: "fee.requestfinance.near".to_string().try_into().unwrap(), fee_amount: 200.into(), payment_reference: "abc7c8bb1234fd12".into(), @@ -301,7 +300,7 @@ mod tests { let args = get_default_payment_args(); let msg = get_msg_from_args(args) + "."; - contract.ft_on_transfer(alice_account(), "1".into(), msg); + contract.ft_on_transfer(alice_account(), "1000".into(), msg); } #[test] @@ -312,9 +311,9 @@ mod tests { let mut contract = FungibleProxy::default(); let args = get_default_payment_args(); - let msg = get_msg_from_args(args).replace("\"amount\":\"1000000\",", ""); + let msg = get_msg_from_args(args).replace("\"fee_amount\":\"200\",", ""); - contract.ft_on_transfer(alice_account(), "1".into(), msg); + contract.ft_on_transfer(alice_account(), "1000000".into(), msg); } #[test] @@ -327,7 +326,7 @@ mod tests { let args = get_default_payment_args(); let msg = get_msg_from_args(args); - contract.ft_on_transfer(alice_account(), "1".into(), msg); + contract.ft_on_transfer(alice_account(), "100".into(), msg); } #[test] @@ -347,7 +346,7 @@ mod tests { let context = get_context(alice_account(), ntoy(100), MIN_GAS, true); testing_env!(context); - let expected_msg = r#"{"amount":"1000000","fee_address":"fee.requestfinance.near","fee_amount":"200","payment_reference":"abc7c8bb1234fd12","to":"dummy.payee.near"}"#; + let expected_msg = r#"{"fee_address":"fee.requestfinance.near","fee_amount":"200","payment_reference":"abc7c8bb1234fd12","to":"dummy.payee.near"}"#; let msg: String = get_default_payment_args().into(); assert_eq!(msg, expected_msg); } diff --git a/fungible_proxy/test.sh b/fungible_proxy/test.sh index d90662f..da25d56 100755 --- a/fungible_proxy/test.sh +++ b/fungible_proxy/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -cargo test -- --nocapture \ No newline at end of file +cargo test \ No newline at end of file From 81ed3372ef6d5b8044938ec97bf84d44cc2233e5 Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 11 Apr 2023 11:34:20 +0200 Subject: [PATCH 11/14] easy fixes from mantisclone's review --- conversion_proxy/test.sh | 2 +- deploy.sh | 7 ++-- fungible_conversion_proxy/test.sh | 2 +- fungible_proxy/src/lib.rs | 20 ++++----- fungible_proxy/test.sh | 2 +- mocks/test.sh | 2 +- tests/sim/fungible_conversion_proxy.rs | 56 +++++++++++++------------- tests/sim/fungible_proxy.rs | 42 +++++++++---------- tests/sim/main.rs | 2 +- tests/sim/utils.rs | 4 +- 10 files changed, 67 insertions(+), 72 deletions(-) diff --git a/conversion_proxy/test.sh b/conversion_proxy/test.sh index d90662f..2e1ff1f 100755 --- a/conversion_proxy/test.sh +++ b/conversion_proxy/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -cargo test -- --nocapture \ No newline at end of file +cargo test -- --nocapture diff --git a/deploy.sh b/deploy.sh index 8b17896..e6958d3 100755 --- a/deploy.sh +++ b/deploy.sh @@ -12,6 +12,7 @@ die() { echo "$*" >&2; exit 2; } # complain to STDERR and exit with error needs_arg() { if [ -z "$OPTARG" ]; then die "Missing arg for --$OPT option"; fi; } while getopts "pha:-:" OPT; do + # Source: https://stackoverflow.com/a/28466267 if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG OPT="${OPTARG%%=*}" # extract long option name OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty) @@ -23,8 +24,8 @@ while getopts "pha:-:" OPT; do echo "Defaults to testnet." echo "" echo "Options:" - echo " -p | --mainnet : for prod deployment" - echo " -a [account_id] : to override \$ACCOUNT_ID" + echo " -p | --prod | --mainnet : for prod deployment" + echo " -a [account_id] : to override \$ACCOUNT_ID" echo " Choose the contract to deploy with:" echo " --conversion_proxy [default]" echo " --fungible_proxy" @@ -56,4 +57,4 @@ else --accountId $ACCOUNT_ID \ --initFunction new \ --initArgs '{"oracle_account_id": "'$oracle_account_id'", "provider_account_id": "'$provider_account_id'"}' -fi \ No newline at end of file +fi diff --git a/fungible_conversion_proxy/test.sh b/fungible_conversion_proxy/test.sh index d90662f..2e1ff1f 100755 --- a/fungible_conversion_proxy/test.sh +++ b/fungible_conversion_proxy/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -cargo test -- --nocapture \ No newline at end of file +cargo test -- --nocapture diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index 4430261..d72b676 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -39,22 +39,12 @@ impl Into for PaymentArgs { } } -/** - * Fungible token-related declarations - */ - // Interface of fungible tokens #[near_sdk::ext_contract(ft_contract)] trait FungibleTokenContract { fn ft_transfer(receiver_id: String, amount: String, memo: Option); } -/// -/// This contract -#[near_bindgen] -#[derive(Default, BorshDeserialize, BorshSerialize)] -pub struct FungibleProxy {} - // Callback methods #[near_sdk::ext_contract(ext_self)] pub trait ExtSelfRequestProxy { @@ -71,6 +61,12 @@ trait FungibleTokenReceiver { fn ft_on_transfer(&mut self, sender_id: AccountId, amount: String, msg: String) -> Promise; } +/// +/// This contract +#[near_bindgen] +#[derive(Default, BorshDeserialize, BorshSerialize)] +pub struct FungibleProxy {} + #[near_bindgen] impl FungibleTokenReceiver for FungibleProxy { /// This is the function that will be called by the fungible token contract's `ft_transfer_call` function. @@ -93,8 +89,8 @@ impl FungibleTokenReceiver for FungibleProxy { #[near_bindgen] impl FungibleProxy { - /// Main function for this contract, transfers fungible tokens to a payment address (to) with a payment reference, as well as a fee. - /// The `amount` is denominated in `currency` with 2 decimals. + /// Main function for this contract. Transfers fungible tokens to the `to` address and fee to the `fee_address`, then logs a `payment_reference`. + /// The `amount` is denominated in `token_address` with the according decimals. /// /// Due to the way NEAR defines the fungible token standard, this function should NOT be called directly. Instead, the /// `ft_transfer_call` function in the contract of the fungible token being used for payment should be called with the diff --git a/fungible_proxy/test.sh b/fungible_proxy/test.sh index da25d56..c179d0f 100755 --- a/fungible_proxy/test.sh +++ b/fungible_proxy/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -cargo test \ No newline at end of file +cargo test diff --git a/mocks/test.sh b/mocks/test.sh index d90662f..2e1ff1f 100755 --- a/mocks/test.sh +++ b/mocks/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -cargo test -- --nocapture \ No newline at end of file +cargo test -- --nocapture diff --git a/tests/sim/fungible_conversion_proxy.rs b/tests/sim/fungible_conversion_proxy.rs index da2e919..a6c4ae5 100644 --- a/tests/sim/fungible_conversion_proxy.rs +++ b/tests/sim/fungible_conversion_proxy.rs @@ -13,9 +13,9 @@ use std::str; near_sdk::setup_alloc!(); -const FUNGIBLE_PROXY_ID: &str = "fungible_request_proxy"; +const PROXY_ID: &str = "fungible_conversion_proxy"; lazy_static_include::lazy_static_include_bytes! { - FUNGIBLE_REQUEST_PROXY_BYTES => "out/fungible_conversion_proxy.wasm" + PROXY_BYTES => "out/fungible_conversion_proxy.wasm" } lazy_static_include::lazy_static_include_bytes! { MOCKED_BYTES => "out/mocks.wasm" @@ -54,16 +54,16 @@ fn init_fungible() -> ( let empty_account_1 = root.create_user("bob".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); let empty_account_2 = root.create_user("builder".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); - let fungible_request_proxy = deploy!( + let proxy = deploy!( contract: FungibleConversionProxyContract, - contract_id: FUNGIBLE_PROXY_ID, - bytes: &FUNGIBLE_REQUEST_PROXY_BYTES, + contract_id: PROXY_ID, + bytes: &PROXY_BYTES, signer_account: root, deposit: to_yocto("5"), init_method: new("mockedfpo".into(), "any".into()) ); - let get_oracle_result = call!(root, fungible_request_proxy.get_oracle_account()); + let get_oracle_result = call!(root, proxy.get_oracle_account()); get_oracle_result.assert_success(); debug_assert_eq!( @@ -75,7 +75,7 @@ fn init_fungible() -> ( account, empty_account_1, empty_account_2, - fungible_request_proxy, + proxy, ft_contract, ) } @@ -94,7 +94,7 @@ fn fungible_transfer_setup( call!(builder, ft_contract.register_account(builder.account_id())); call!( builder, - ft_contract.register_account(FUNGIBLE_PROXY_ID.into()) + ft_contract.register_account(PROXY_ID.into()) ); // Set initial balances @@ -112,7 +112,7 @@ fn fungible_transfer_setup( ); call!( builder, - ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), 0.into()) // 0 USDC.e + ft_contract.set_balance(PROXY_ID.into(), 0.into()) // 0 USDC.e ); let alice_balance_before = call!(alice, ft_contract.ft_balance_of(alice.account_id())) @@ -129,7 +129,7 @@ fn fungible_transfer_setup( // The token contract will transfer the specificed tokens from the caller to our contract before calling our contract call!( alice, - ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), send_amt) + ft_contract.set_balance(PROXY_ID.into(), send_amt) ); call!( alice, @@ -148,7 +148,7 @@ fn fungible_transfer_setup( #[test] fn test_transfer() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (alice_balance_before, bob_balance_before, builder_balance_before) = @@ -158,7 +158,7 @@ fn test_transfer() { // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder let get_args = call!( alice, - fungible_request_proxy.get_transfer_with_reference_args( + proxy.get_transfer_with_reference_args( 10000.into(), // 100 USD "USD".into(), "builder".to_string().try_into().unwrap(), @@ -173,7 +173,7 @@ fn test_transfer() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) ); result.assert_success(); let change = result.unwrap_json::().parse::().unwrap(); @@ -217,7 +217,7 @@ fn test_transfer() { #[test] fn test_transfer_not_enough() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (_, _, _) = fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); @@ -227,7 +227,7 @@ fn test_transfer_not_enough() { // The request is for 1000 USD, but alice only sends in 500 USDC.e let get_args = call!( alice, - fungible_request_proxy.get_transfer_with_reference_args( + proxy.get_transfer_with_reference_args( 100000.into(), // 1000 USD "USD".into(), "builder".to_string().try_into().unwrap(), @@ -242,14 +242,14 @@ fn test_transfer_not_enough() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) ); assert_one_promise_error(result, "Deposit too small") } #[test] fn test_transfer_receiver_send_failed() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (alice_balance_before, _, _) = @@ -263,7 +263,7 @@ fn test_transfer_receiver_send_failed() { // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder let get_args = call!( alice, - fungible_request_proxy.get_transfer_with_reference_args( + proxy.get_transfer_with_reference_args( 10000.into(), // 100 USD "USD".into(), "builder".to_string().try_into().unwrap(), @@ -278,7 +278,7 @@ fn test_transfer_receiver_send_failed() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) ); result.assert_success(); let change = result.unwrap_json::().parse::().unwrap(); @@ -294,7 +294,7 @@ fn test_transfer_receiver_send_failed() { #[test] fn test_transfer_fee_receiver_send_failed() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (alice_balance_before, _, _) = @@ -311,7 +311,7 @@ fn test_transfer_fee_receiver_send_failed() { // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder let get_args = call!( alice, - fungible_request_proxy.get_transfer_with_reference_args( + proxy.get_transfer_with_reference_args( 10000.into(), // 100 USD "USD".into(), "builder".to_string().try_into().unwrap(), @@ -326,7 +326,7 @@ fn test_transfer_fee_receiver_send_failed() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) ); result.assert_success(); let change = result.unwrap_json::().parse::().unwrap(); @@ -342,7 +342,7 @@ fn test_transfer_fee_receiver_send_failed() { #[test] fn test_transfer_zero_usd() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(0); // 0 USDC.e let (alice_balance_before, bob_balance_before, builder_balance_before) = @@ -352,7 +352,7 @@ fn test_transfer_zero_usd() { // Transferring 0 USD worth of USDC.e from alice to bob, with a 0 USD fee to builder let get_args = call!( alice, - fungible_request_proxy.get_transfer_with_reference_args( + proxy.get_transfer_with_reference_args( 0.into(), // 0 USD "USD".into(), "builder".to_string().try_into().unwrap(), @@ -367,7 +367,7 @@ fn test_transfer_zero_usd() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) ); result.assert_success(); let change = result.unwrap_json::().parse::().unwrap(); @@ -390,7 +390,7 @@ fn test_transfer_zero_usd() { #[test] fn test_outdated_rate() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); @@ -399,7 +399,7 @@ fn test_outdated_rate() { // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder let get_args = call!( alice, - fungible_request_proxy.get_transfer_with_reference_args( + proxy.get_transfer_with_reference_args( 10000.into(), // 100 USD "USD".into(), "builder".to_string().try_into().unwrap(), @@ -414,7 +414,7 @@ fn test_outdated_rate() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), msg) ); assert_one_promise_error(result, "Conversion rate too old"); } diff --git a/tests/sim/fungible_proxy.rs b/tests/sim/fungible_proxy.rs index 6a96e3e..5935ccb 100644 --- a/tests/sim/fungible_proxy.rs +++ b/tests/sim/fungible_proxy.rs @@ -14,9 +14,9 @@ use std::str; near_sdk::setup_alloc!(); -const FUNGIBLE_PROXY_ID: &str = "fungible_request_proxy"; +const PROXY_ID: &str = "fungible_proxy"; lazy_static_include::lazy_static_include_bytes! { - FUNGIBLE_REQUEST_PROXY_BYTES => "out/fungible_proxy.wasm" + PROXY_BYTES => "out/fungible_proxy.wasm" } lazy_static_include::lazy_static_include_bytes! { MOCKED_BYTES => "out/mocks.wasm" @@ -47,10 +47,10 @@ fn init_fungible() -> ( let empty_account_1 = root.create_user("bob".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); let empty_account_2 = root.create_user("builder".parse().unwrap(), to_yocto(DEFAULT_BALANCE)); - let fungible_request_proxy = deploy!( + let proxy = deploy!( contract: FungibleProxyContract, - contract_id: FUNGIBLE_PROXY_ID, - bytes: &FUNGIBLE_REQUEST_PROXY_BYTES, + contract_id: PROXY_ID, + bytes: &PROXY_BYTES, signer_account: root, deposit: to_yocto("5") ); @@ -59,7 +59,7 @@ fn init_fungible() -> ( account, empty_account_1, empty_account_2, - fungible_request_proxy, + proxy, ft_contract, ) } @@ -78,7 +78,7 @@ fn fungible_transfer_setup( call!(builder, ft_contract.register_account(builder.account_id())); call!( builder, - ft_contract.register_account(FUNGIBLE_PROXY_ID.into()) + ft_contract.register_account(PROXY_ID.into()) ); // Set initial balances @@ -96,7 +96,7 @@ fn fungible_transfer_setup( ); call!( builder, - ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), 0.into()) // 0 USDC.e + ft_contract.set_balance(PROXY_ID.into(), 0.into()) // 0 USDC.e ); let alice_balance_before = call!(alice, ft_contract.ft_balance_of(alice.account_id())) @@ -113,7 +113,7 @@ fn fungible_transfer_setup( // The token contract will transfer the specificed tokens from the caller to our contract before calling our contract call!( alice, - ft_contract.set_balance(FUNGIBLE_PROXY_ID.into(), send_amt) + ft_contract.set_balance(PROXY_ID.into(), send_amt) ); call!( alice, @@ -132,7 +132,7 @@ fn fungible_transfer_setup( #[test] fn test_transfer() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (alice_balance_before, bob_balance_before, builder_balance_before) = @@ -147,7 +147,7 @@ fn test_transfer() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); @@ -163,7 +163,7 @@ fn test_transfer() { #[test] fn transfer_less_than_fee_amount() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (_, _, _) = fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); @@ -177,14 +177,14 @@ fn transfer_less_than_fee_amount() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); assert_one_promise_error(result, "amount smaller than fee_amount") } #[test] fn test_transfer_receiver_send_failed() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (alice_balance_before, _, _) = @@ -194,18 +194,16 @@ fn test_transfer_receiver_send_failed() { // Receiver is not registered with the token contract, so sending to it will fail call!(bob, ft_contract.unregister_account(bob.account_id())); - // Constuct the `msg` argument using our contract - // Transferring 100 USD worth of USDC.e from alice to bob, with a 2 USD fee to builder let args = PaymentArgs { fee_address: "builder".try_into().unwrap(), - fee_amount: 200.into(), // 2 USD + fee_amount: 2000000.into(), // 2 USDC.e payment_reference: "abc7c8bb1234fd12".into(), to: bob.account_id().try_into().unwrap() }; let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); let change = result.unwrap_json::().parse::().unwrap(); @@ -221,7 +219,7 @@ fn test_transfer_receiver_send_failed() { #[test] fn test_transfer_fee_receiver_send_failed() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(500000000); // 500 USDC.e let (alice_balance_before, _, _) = @@ -243,7 +241,7 @@ fn test_transfer_fee_receiver_send_failed() { let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); // The mocked fungible token does not handle change @@ -254,7 +252,7 @@ fn test_transfer_fee_receiver_send_failed() { #[test] fn test_transfer_zero_usd() { - let (alice, bob, builder, fungible_request_proxy, ft_contract) = init_fungible(); + let (alice, bob, builder, proxy, ft_contract) = init_fungible(); let send_amt = U128::from(0); // 0 USDC.e let (alice_balance_before, bob_balance_before, builder_balance_before) = @@ -268,7 +266,7 @@ fn test_transfer_zero_usd() { }; let result = call!( ft_contract.user_account, - fungible_request_proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) + proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); diff --git a/tests/sim/main.rs b/tests/sim/main.rs index 841b89b..5ea31f7 100644 --- a/tests/sim/main.rs +++ b/tests/sim/main.rs @@ -1,4 +1,4 @@ mod utils; mod fungible_conversion_proxy; mod conversion_proxy; -mod fungible_proxy; \ No newline at end of file +mod fungible_proxy; diff --git a/tests/sim/utils.rs b/tests/sim/utils.rs index ba130db..a4aac44 100644 --- a/tests/sim/utils.rs +++ b/tests/sim/utils.rs @@ -28,7 +28,7 @@ pub fn assert_unchanged_balance(account: UserAccount, previous_balance: u128, ft let current_balance = call!(account, ft_contract.ft_balance_of(account.account_id())) .unwrap_json::() .0; - assert!(current_balance == previous_balance, "{}'s balance changed by {} (from {} to {})", account_name, previous_balance-current_balance, previous_balance, current_balance); + assert!(current_balance == previous_balance, "{}'s balance changed by {} (from {} to {})", account_name, previous_balance - current_balance, previous_balance, current_balance); } /// Util to assert that an account has spent a given amount of token. @@ -37,7 +37,7 @@ pub fn assert_spent(account: UserAccount, previous_balance: u128, expected_spent .unwrap_json::() .0; assert!(current_balance <= previous_balance, "Did not spend."); - assert!(current_balance == previous_balance -expected_spent_amount, "Spent {} instead of {}", previous_balance - current_balance, expected_spent_amount); + assert!(current_balance == previous_balance - expected_spent_amount, "Spent {} instead of {}", previous_balance - current_balance, expected_spent_amount); } /// Util to assert that an account has received a given amount of token. From 6dd32141bf8404451f82d4e4ba1dd9c28b55375e Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 11 Apr 2023 12:17:35 +0200 Subject: [PATCH 12/14] easy fixes #2 form mantisclone's suggestions --- fungible_proxy/src/lib.rs | 2 +- tests/sim/conversion_proxy.rs | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index d72b676..b9a97c1 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -47,7 +47,7 @@ trait FungibleTokenContract { // Callback methods #[near_sdk::ext_contract(ext_self)] -pub trait ExtSelfRequestProxy { +trait ExtSelfRequestProxy { fn on_transfer_with_reference( &self, args: PaymentArgs, diff --git a/tests/sim/conversion_proxy.rs b/tests/sim/conversion_proxy.rs index 01942ed..2e89fdd 100644 --- a/tests/sim/conversion_proxy.rs +++ b/tests/sim/conversion_proxy.rs @@ -13,9 +13,9 @@ use std::str; near_sdk::setup_alloc!(); -const PROXY_ID: &str = "request_proxy"; +const PROXY_ID: &str = "conversion_proxy"; lazy_static_include::lazy_static_include_bytes! { - REQUEST_PROXY_BYTES => "out/conversion_proxy.wasm" + PROXY_BYTES => "out/conversion_proxy.wasm" } lazy_static_include::lazy_static_include_bytes! { MOCKED_BYTES => "out/mocks.wasm" @@ -47,16 +47,16 @@ fn init() -> ( let empty_account_1 = root.create_user("bob".parse().unwrap(), zero_balance); let empty_account_2 = root.create_user("builder".parse().unwrap(), zero_balance); - let request_proxy = deploy!( + let proxy = deploy!( contract: ConversionProxyContract, contract_id: PROXY_ID, - bytes: &REQUEST_PROXY_BYTES, + bytes: &PROXY_BYTES, signer_account: root, deposit: to_yocto("5"), init_method: new("mockedfpo".into(), "any".into()) ); - let get_oracle_result = call!(root, request_proxy.get_oracle_account()); + let get_oracle_result = call!(root, proxy.get_oracle_account()); get_oracle_result.assert_success(); debug_assert_eq!( @@ -64,12 +64,12 @@ fn init() -> ( &"mockedfpo".to_string() ); - (account, empty_account_1, empty_account_2, request_proxy) + (account, empty_account_1, empty_account_2, proxy) } #[test] fn test_transfer() { - let (alice, bob, builder, request_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; @@ -81,7 +81,7 @@ fn test_transfer() { // Token transfer failed let result = call!( alice, - request_proxy.transfer_with_reference( + proxy.transfer_with_reference( "0x1122334455667788".to_string(), payment_address, // 12000.00 USD (main) @@ -135,14 +135,14 @@ fn test_transfer() { fn test_transfer_with_invalid_reference_length() { let transfer_amount = to_yocto("500"); - let (alice, bob, builder, request_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(); // Token transfer failed let result = call!( alice, - request_proxy.transfer_with_reference( + proxy.transfer_with_reference( "0x11223344556677".to_string(), payment_address, U128::from(12), @@ -169,7 +169,7 @@ fn test_transfer_with_invalid_reference_length() { #[test] fn test_transfer_with_wrong_currency() { - let (alice, bob, builder, request_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(); @@ -177,7 +177,7 @@ fn test_transfer_with_wrong_currency() { // Token transfer failed let result = call!( alice, - request_proxy.transfer_with_reference( + proxy.transfer_with_reference( "0x1122334455667788".to_string(), payment_address, U128::from(1200), @@ -193,7 +193,7 @@ fn test_transfer_with_wrong_currency() { #[test] fn test_transfer_zero_usd() { - let (alice, bob, builder, request_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"); @@ -202,7 +202,7 @@ fn test_transfer_zero_usd() { let result = call!( alice, - request_proxy.transfer_with_reference( + proxy.transfer_with_reference( "0x1122334455667788".to_string(), payment_address, U128::from(0), @@ -235,14 +235,14 @@ fn test_transfer_zero_usd() { #[test] fn test_outdated_rate() { - let (alice, bob, builder, request_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(); let result = call!( alice, - request_proxy.transfer_with_reference( + proxy.transfer_with_reference( "0x1122334455667788".to_string(), payment_address, U128::from(0), From 967f566fa9b99c48e96047d999c4cc1b0e54c08e Mon Sep 17 00:00:00 2001 From: yomarion Date: Tue, 11 Apr 2023 15:45:21 +0200 Subject: [PATCH 13/14] minor test param --- fungible_proxy/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index b9a97c1..11a519f 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -276,7 +276,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(100), 10u64.pow(14), false); + let context = get_context(alice_account(), ntoy(100), MIN_GAS - 1, false); testing_env!(context); let mut contract = FungibleProxy::default(); From 1b0f5277b56ec0bb0ddf390fcdd9830ec2c16392 Mon Sep 17 00:00:00 2001 From: yomarion Date: Wed, 12 Apr 2023 11:12:37 +0200 Subject: [PATCH 14/14] test logs sent by success or failure --- fungible_proxy/src/lib.rs | 6 +++-- tests/sim/fungible_proxy.rs | 47 ++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/fungible_proxy/src/lib.rs b/fungible_proxy/src/lib.rs index 11a519f..4d1c889 100644 --- a/fungible_proxy/src/lib.rs +++ b/fungible_proxy/src/lib.rs @@ -178,13 +178,15 @@ impl FungibleProxy { ); 0.to_string() } else { + // return full amount for `ft_resolve_transfer` on the token contract + let change = (amount.0 + args.fee_amount.0).to_string(); env::log( format!( "Failed to transfer to account {}. Returning attached amount of {} of token {} to {}", - args.to, amount.0, token_address, payer) + args.to, change, token_address, payer) .as_bytes(), ); - (amount.0 + args.fee_amount.0).to_string() // return full amount for `ft_resolve_transfer` on the token contract + change } } } diff --git a/tests/sim/fungible_proxy.rs b/tests/sim/fungible_proxy.rs index 5935ccb..417a1ab 100644 --- a/tests/sim/fungible_proxy.rs +++ b/tests/sim/fungible_proxy.rs @@ -3,6 +3,7 @@ use fungible_proxy::PaymentArgs; use fungible_proxy::FungibleProxyContract; use mocks::fungible_token_mock::FungibleTokenContractContract; use near_sdk::json_types::{U128}; +use near_sdk::serde_json::json; use near_sdk_sim::init_simulator; use near_sdk_sim::runtime::GenesisConfig; use near_sdk_sim::ContractAccount; @@ -139,9 +140,9 @@ fn test_transfer() { fungible_transfer_setup(&alice, &bob, &builder, &ft_contract, send_amt); let args = PaymentArgs { - fee_address: "builder".try_into().unwrap(), + fee_address: builder.account_id().try_into().unwrap(), fee_amount: 2000000.into(), // 2 USDC.e - payment_reference: "abc7c8bb1234fd12".into(), + payment_reference: "abc7c8bb1234fd11".into(), to: bob.account_id().try_into().unwrap(), }; @@ -150,6 +151,17 @@ fn test_transfer() { proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); + assert_eq!(result.logs().len(), 1, "Wrong number of logs"); + let expected_log = json!({ + "amount": "498000000", // 500 USDC.e - 2 USDC.e fee + "token_address": "mockedft", + "fee_address": "builder", + "fee_amount": "2000000", + "payment_reference": "abc7c8bb1234fd11", + "to": "bob", + }) + .to_string(); + assert_eq!(result.logs()[0], expected_log); // The mocked fungible token does not handle change let change = result.unwrap_json::().parse::().unwrap(); @@ -195,7 +207,7 @@ fn test_transfer_receiver_send_failed() { call!(bob, ft_contract.unregister_account(bob.account_id())); let args = PaymentArgs { - fee_address: "builder".try_into().unwrap(), + fee_address: builder.account_id().try_into().unwrap(), fee_amount: 2000000.into(), // 2 USDC.e payment_reference: "abc7c8bb1234fd12".into(), to: bob.account_id().try_into().unwrap() @@ -206,12 +218,17 @@ fn test_transfer_receiver_send_failed() { proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); + assert_eq!(result.logs().len(), 1, "Wrong number of logs"); + assert_eq!(result.logs()[0], format!("Failed to transfer to account bob. Returning attached amount of 500000000 of token mockedft to alice")); + + // The mocked fungible token does not handle change let change = result.unwrap_json::().parse::().unwrap(); - let alice_balance_after = call!(alice, ft_contract.ft_balance_of(alice.account_id())) - .unwrap_json::() + let result = call!(alice, ft_contract.ft_balance_of(alice.account_id())); + let alice_balance_after = result.unwrap_json::() .0 + change; + // Ensure no balance changes / all funds returned to sender assert!(alice_balance_after == alice_balance_before); @@ -227,10 +244,7 @@ fn test_transfer_fee_receiver_send_failed() { // Previous line registers all accounts, so we unregister builder here // Fee_receiver is not registered with the token contract, so sending to it will fail - call!( - builder, - ft_contract.unregister_account(builder.account_id()) - ); + call!(builder, ft_contract.unregister_account(builder.account_id())); let args = PaymentArgs { fee_address: "builder".try_into().unwrap(), @@ -244,8 +258,12 @@ fn test_transfer_fee_receiver_send_failed() { proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); + assert_eq!(result.logs().len(), 1, "Wrong number of logs"); + assert_eq!(result.logs()[0], format!("Failed to transfer to account bob. Returning attached amount of 500000000 of token mockedft to alice")); + // The mocked fungible token does not handle change let change = result.unwrap_json::().parse::().unwrap(); + assert_eq!(change, 500000000); assert_unchanged_balance(alice, alice_balance_before.sub(change), &ft_contract, "Alice"); } @@ -269,6 +287,17 @@ fn test_transfer_zero_usd() { proxy.ft_on_transfer(alice.account_id(), send_amt.0.to_string(), args.into()) ); result.assert_success(); + assert_eq!(result.logs().len(), 1, "Wrong number of logs"); + let expected_log = json!({ + "amount": "0", + "token_address": "mockedft", + "fee_address": "builder", + "fee_amount": "0", + "payment_reference": "abc7c8bb1234fd12", + "to": "bob", + }) + .to_string(); + assert_eq!(result.logs()[0], expected_log); assert_unchanged_balance(alice, alice_balance_before, &ft_contract, "Alice"); assert_unchanged_balance(bob, bob_balance_before, &ft_contract, "Bob");