diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index c4b4253f..46805211 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -105,7 +105,7 @@ impl ClaimCallbacks for Contract { #[private] fn after_claim(&mut self, claimed_amount: U128, jars_before_transfer: Vec, event: EventKind) -> U128 { if is_promise_success() { - for jar_before_transfer in jars_before_transfer.iter() { + for jar_before_transfer in jars_before_transfer { let jar = self.get_jar_internal(jar_before_transfer.index); self.jars.replace(jar_before_transfer.index, jar.unlocked()); @@ -115,7 +115,7 @@ impl ClaimCallbacks for Contract { claimed_amount } else { - for jar_before_transfer in jars_before_transfer.iter() { + for jar_before_transfer in jars_before_transfer { self.jars .replace(jar_before_transfer.index, jar_before_transfer.unlocked()); } diff --git a/contract/src/common.rs b/contract/src/common.rs index d01b09fd..9fd5358e 100644 --- a/contract/src/common.rs +++ b/contract/src/common.rs @@ -1,3 +1,5 @@ +use std::ops::Mul; + use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, serde::{self, Deserialize, Deserializer, Serialize, Serializer}, @@ -33,12 +35,24 @@ pub struct UDecimal { } impl UDecimal { - pub(crate) fn mul(&self, value: u128) -> u128 { + pub(crate) fn to_f32(&self) -> f32 { + self.significand as f32 / 10u128.pow(self.exponent) as f32 + } +} + +impl Mul for UDecimal { + type Output = u128; + + fn mul(self, value: u128) -> Self::Output { value * self.significand / 10u128.pow(self.exponent) } +} - pub(crate) fn to_f32(&self) -> f32 { - self.significand as f32 / 10u128.pow(self.exponent) as f32 +impl Mul for &UDecimal { + type Output = u128; + + fn mul(self, value: u128) -> Self::Output { + value * self.significand / 10u128.pow(self.exponent) } } diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index 04c9ead8..5eaaad87 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -207,7 +207,10 @@ impl JarApi for Contract { mod tests { use near_sdk::AccountId; - use crate::{jar::model::Jar, product::tests::get_product}; + use crate::{ + jar::model::Jar, + product::tests::{get_product, YEAR_IN_MS}, + }; #[test] fn get_interest_before_maturity() { @@ -220,7 +223,7 @@ mod tests { 0, ); - let interest = jar.get_interest(&product, 365 * 24 * 60 * 60 * 1000); + let interest = jar.get_interest(&product, YEAR_IN_MS); assert_eq!(12_000_000, interest); } diff --git a/contract/src/jar/mod.rs b/contract/src/jar/mod.rs index c502ea6a..e98916e5 100644 --- a/contract/src/jar/mod.rs +++ b/contract/src/jar/mod.rs @@ -5,9 +5,12 @@ pub mod view; #[cfg(test)] mod helpers { use near_sdk::AccountId; - use crate::common::{Timestamp, TokenAmount}; - use crate::jar::model::{Jar, JarState}; - use crate::product::model::ProductId; + + use crate::{ + common::{Timestamp, TokenAmount}, + jar::model::{Jar, JarState}, + product::model::ProductId, + }; impl Jar { pub(crate) fn generate(index: u32, account_id: &AccountId, product_id: &ProductId) -> Jar { @@ -35,4 +38,4 @@ mod helpers { self } } -} \ No newline at end of file +} diff --git a/contract/src/jar/model.rs b/contract/src/jar/model.rs index eaf2af58..cb6d5c39 100644 --- a/contract/src/jar/model.rs +++ b/contract/src/jar/model.rs @@ -206,7 +206,7 @@ impl Jar { let term_in_minutes = (effective_term / MS_IN_MINUTE) as u128; let apy = self.get_apy(product); - let total_interest = apy.mul(self.principal); + let total_interest = apy * self.principal; let interest = (term_in_minutes * total_interest) / MINUTES_IN_YEAR as u128; diff --git a/contract/src/migration/model.rs b/contract/src/migration/model.rs index 6fc99916..473e3e87 100644 --- a/contract/src/migration/model.rs +++ b/contract/src/migration/model.rs @@ -7,7 +7,7 @@ use near_sdk::{ use crate::product::model::ProductId; -#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, PartialEq, Clone)] +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] #[serde(crate = "near_sdk::serde")] pub struct CeFiJar { pub id: String, diff --git a/contract/src/product/command.rs b/contract/src/product/command.rs index faa6e2bd..6e8382fb 100644 --- a/contract/src/product/command.rs +++ b/contract/src/product/command.rs @@ -23,6 +23,23 @@ pub struct RegisterProductCommand { pub is_enabled: bool, } +#[cfg(test)] +impl Default for RegisterProductCommand { + fn default() -> Self { + Self { + id: "default_product".to_string(), + apy_default: (U128(12), 2), + apy_fallback: None, + cap_min: U128(100), + cap_max: U128(100_000_000_000), + terms: TermsDto::default(), + withdrawal_fee: None, + public_key: None, + is_enabled: true, + } + } +} + impl From for Product { fn from(value: RegisterProductCommand) -> Self { let apy = if let Some(apy_fallback) = value.apy_fallback { @@ -62,6 +79,13 @@ pub enum TermsDto { Flexible, } +#[cfg(test)] +impl Default for TermsDto { + fn default() -> Self { + Self::Fixed(Default::default()) + } +} + #[derive(BorshDeserialize, BorshSerialize, Serialize, PartialEq, Deserialize, Clone, Debug)] #[serde(crate = "near_sdk::serde")] pub struct FixedProductTermsDto { @@ -70,6 +94,17 @@ pub struct FixedProductTermsDto { pub allows_restaking: bool, } +#[cfg(test)] +impl Default for FixedProductTermsDto { + fn default() -> Self { + Self { + lockup_term: U64(crate::product::tests::YEAR_IN_MS), + allows_restaking: false, + allows_top_up: false, + } + } +} + impl From for Terms { fn from(value: TermsDto) -> Self { match value { diff --git a/contract/src/product/mod.rs b/contract/src/product/mod.rs index 8d8570a5..c5ada6ce 100644 --- a/contract/src/product/mod.rs +++ b/contract/src/product/mod.rs @@ -6,12 +6,12 @@ pub mod view; #[cfg(test)] pub(crate) mod tests { use near_sdk::{ - json_types::{Base64VecU8, U128, U64}, + json_types::{Base64VecU8, U128}, test_utils::accounts, }; use crate::{ - common::{tests::Context, UDecimal}, + common::{tests::Context, Duration, UDecimal}, product::{ api::ProductApi, command::{FixedProductTermsDto, RegisterProductCommand, TermsDto, WithdrawalFeeDto}, @@ -26,6 +26,8 @@ pub(crate) mod tests { ] } + pub(crate) const YEAR_IN_MS: Duration = 365 * 24 * 60 * 60 * 1000; + pub(crate) fn get_product() -> Product { Product { id: "product".to_string(), @@ -35,7 +37,7 @@ pub(crate) mod tests { max: 100_000_000_000, }, terms: Terms::Fixed(FixedProductTerms { - lockup_term: 365 * 24 * 60 * 60 * 1000, + lockup_term: YEAR_IN_MS, allows_top_up: false, allows_restaking: false, }), @@ -45,71 +47,48 @@ pub(crate) mod tests { } } - pub(crate) fn get_fee_product_command(fee: WithdrawalFeeDto) -> RegisterProductCommand { + pub(crate) fn get_product_with_fee_command(fee: WithdrawalFeeDto) -> RegisterProductCommand { RegisterProductCommand { id: "product_with_fee".to_string(), - apy_default: (U128(12), 2), - apy_fallback: None, - cap_min: U128(100), - cap_max: U128(100_000_000_000), - terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: U64(365 * 24 * 60 * 60 * 1000), - allows_restaking: false, - allows_top_up: false, - }), withdrawal_fee: Some(fee), - public_key: None, - is_enabled: true, + ..Default::default() } } pub(crate) fn get_register_product_command() -> RegisterProductCommand { RegisterProductCommand { id: "product".to_string(), - apy_default: (U128(12), 2), - apy_fallback: None, - cap_min: U128(100), - cap_max: U128(100_000_000_000), + ..Default::default() + } + } + + pub(crate) fn get_register_refillable_product_command() -> RegisterProductCommand { + RegisterProductCommand { + id: "product_refillable".to_string(), terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: U64(365 * 24 * 60 * 60 * 1000), - allows_restaking: false, - allows_top_up: false, + allows_top_up: true, + ..Default::default() }), - withdrawal_fee: None, - public_key: None, - is_enabled: true, + ..Default::default() } } pub(crate) fn get_register_flexible_product_command() -> RegisterProductCommand { RegisterProductCommand { id: "product_flexible".to_string(), - apy_default: (U128(12), 2), - apy_fallback: None, - cap_min: U128(100), - cap_max: U128(100_000_000_000), terms: TermsDto::Flexible, - withdrawal_fee: None, - public_key: None, - is_enabled: true, + ..Default::default() } } pub(crate) fn get_register_restakable_product_command() -> RegisterProductCommand { RegisterProductCommand { id: "product_restakable".to_string(), - apy_default: (U128(12), 2), - apy_fallback: None, - cap_min: U128(100), - cap_max: U128(100_000_000_000), terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: U64(365 * 24 * 60 * 60 * 1000), allows_restaking: true, - allows_top_up: false, + ..Default::default() }), - withdrawal_fee: None, - public_key: None, - is_enabled: true, + ..Default::default() } } @@ -118,16 +97,8 @@ pub(crate) mod tests { id: "product_premium".to_string(), apy_default: (U128(20), 2), apy_fallback: Some((U128(10), 2)), - cap_min: U128(100), - cap_max: U128(100_000_000_000), - terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: U64(365 * 24 * 60 * 60 * 1000), - allows_top_up: false, - allows_restaking: false, - }), - withdrawal_fee: None, public_key: public_key.or_else(|| Some(Base64VecU8(get_premium_product_public_key()))), - is_enabled: true, + ..Default::default() } } diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index 6e0eccc5..aa18376a 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -110,10 +110,10 @@ impl Contract { fn get_fee(&self, product: &Product, jar: &Jar) -> Option { product .withdrawal_fee - .clone() + .as_ref() .map(|fee| match fee { - WithdrawalFee::Fix(amount) => amount, - WithdrawalFee::Percent(percent) => percent.mul(jar.principal), + WithdrawalFee::Fix(amount) => *amount, + WithdrawalFee::Percent(percent) => percent * jar.principal, }) .map(|fee| Fee { amount: fee, @@ -173,37 +173,50 @@ impl WithdrawCallbacks for Contract { #[cfg(test)] mod tests { - use near_sdk::{Duration, json_types::{U128, U64}, test_utils::accounts}; + use near_sdk::{ + json_types::{U128, U64}, + test_utils::accounts, + AccountId, PromiseOrValue, + }; use crate::{ common::{tests::Context, U32}, jar::{api::JarApi, model::JarTicket}, product::{ api::ProductApi, - command::WithdrawalFeeDto, + command::{RegisterProductCommand, WithdrawalFeeDto}, model::Product, - tests::{get_fee_product_command, get_register_flexible_product_command, get_register_product_command}, + tests::{ + get_product_with_fee_command, get_register_flexible_product_command, get_register_product_command, + }, }, withdraw::api::WithdrawApi, }; - #[test] - #[should_panic(expected = "Account doesn't own this jar")] - fn withdraw_locked_jar_before_maturity_by_not_owner() { + fn prepare_jar(product: RegisterProductCommand) -> (AccountId, Product, Context) { let alice = accounts(0); let admin = accounts(1); let mut context = Context::new(admin.clone()); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract.register_product(get_register_product_command()) - }); - let ticket = JarTicket { - product_id: "product".to_string(), + product_id: product.id.clone(), valid_until: U64(0), }; - context.contract.create_jar(alice, ticket, U128(1_000_000), None); + + context.switch_account(&admin); + context.with_deposit_yocto(1, |context| context.contract.register_product(product.clone())); + + context + .contract + .create_jar(alice.clone(), ticket, U128(1_000_000), None); + + (alice, product.into(), context) + } + + #[test] + #[should_panic(expected = "Account doesn't own this jar")] + fn withdraw_locked_jar_before_maturity_by_not_owner() { + let (_, _, mut context) = prepare_jar(get_register_product_command()); context.contract.withdraw(U32(0), None); } @@ -211,24 +224,7 @@ mod tests { #[test] #[should_panic(expected = "The jar is not mature yet")] fn withdraw_locked_jar_before_maturity_by_owner() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract.register_product(get_register_product_command()) - }); - - let ticket = JarTicket { - product_id: "product".to_string(), - - valid_until: U64(0), - }; - - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); + let (alice, _, mut context) = prepare_jar(get_register_product_command()); context.switch_account(&alice); context.contract.withdraw(U32(0), None); @@ -237,51 +233,17 @@ mod tests { #[test] #[should_panic(expected = "Account doesn't own this jar")] fn withdraw_locked_jar_after_maturity_by_not_owner() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); - - let product: &Product = &get_register_product_command().into(); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract.register_product(get_register_product_command()) - }); - - let ticket = JarTicket { - product_id: product.id.clone(), - valid_until: U64(0), - }; - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); + let (_, product, mut context) = prepare_jar(get_register_product_command()); context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.contract.withdraw(U32(0), None); } #[test] fn withdraw_locked_jar_after_maturity_by_owner() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); - - let product: Product = get_register_product_command().into(); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract.register_product(get_register_product_command()) - }); - - let ticket = JarTicket { - product_id: product.id.clone(), - valid_until: U64(0), - }; - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); + let (alice, product, mut context) = prepare_jar(get_register_product_command()); context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); context.contract.withdraw(U32(0), None); } @@ -289,25 +251,7 @@ mod tests { #[test] #[should_panic(expected = "Account doesn't own this jar")] fn withdraw_flexible_jar_by_not_owner() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); - - let product: Product = get_register_flexible_product_command().into(); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context - .contract - .register_product(get_register_flexible_product_command()) - }); - - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - }; - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); + let (_, _, mut context) = prepare_jar(get_register_flexible_product_command()); context.set_block_timestamp_in_days(1); context.contract.withdraw(U32(0), None); @@ -315,25 +259,7 @@ mod tests { #[test] fn withdraw_flexible_jar_by_owner_full() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); - - let product: Product = get_register_flexible_product_command().into(); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context - .contract - .register_product(get_register_flexible_product_command()) - }); - - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - }; - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); + let (alice, _, mut context) = prepare_jar(get_register_flexible_product_command()); context.set_block_timestamp_in_days(1); context.switch_account(&alice); @@ -345,25 +271,7 @@ mod tests { #[test] fn withdraw_flexible_jar_by_owner_with_sufficient_balance() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); - - let product: Product = get_register_flexible_product_command().into(); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context - .contract - .register_product(get_register_flexible_product_command()) - }); - - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - }; - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); + let (alice, _, mut context) = prepare_jar(get_register_flexible_product_command()); context.set_block_timestamp_in_days(1); context.switch_account(&alice); @@ -376,53 +284,63 @@ mod tests { #[test] #[should_panic(expected = "Insufficient balance")] fn withdraw_flexible_jar_by_owner_with_insufficient_balance() { - let alice = accounts(0); - let admin = accounts(1); - let mut context = Context::new(admin.clone()); + let (alice, _, mut context) = prepare_jar(get_register_flexible_product_command()); - let product: Product = get_register_flexible_product_command().into(); - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context - .contract - .register_product(get_register_flexible_product_command()) - }); + context.set_block_timestamp_in_days(1); + context.switch_account(&alice); + context.contract.withdraw(U32(0), Some(U128(2_000_000))); + } - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), + #[test] + fn product_with_fixed_fee() { + let (alice, product, mut context) = prepare_jar(get_product_with_fee_command(WithdrawalFeeDto::Fix(U128(10)))); + + context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); + context.switch_account(&alice); + + let PromiseOrValue::Value(withdraw) = context.contract.withdraw(U32(0), Some(U128(100_000))) else { + panic!("Invalid promise type"); }; - context - .contract - .create_jar(alice.clone(), ticket, U128(1_000_000), None); - context.set_block_timestamp_in_days(1); + assert_eq!(withdraw.withdrawn_amount, U128(99990)); + assert_eq!(withdraw.fee, U128(10)); + + let jar = context.contract.get_jar(U32(0)); + + assert_eq!(jar.principal, U128(900000)); + } + + #[test] + fn product_with_percent_fee() { + let (alice, product, mut context) = + prepare_jar(get_product_with_fee_command(WithdrawalFeeDto::Percent(U128(5), 4))); + + context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - context.contract.withdraw(U32(0), Some(U128(2_000_000))); + let PromiseOrValue::Value(withdraw) = context.contract.withdraw(U32(0), Some(U128(100_000))) else { + panic!("Invalid promise type"); + }; + + assert_eq!(withdraw.withdrawn_amount, U128(99500)); + assert_eq!(withdraw.fee, U128(500)); + + let jar = context.contract.get_jar(U32(0)); + + assert_eq!(jar.principal, U128(900000)); } #[test] - fn product_with_fee() { - // let alice = accounts(0); - // let admin = accounts(1); - // let mut context = Context::new(admin.clone()); - // - // let product: Product = get_fee_product_command(WithdrawalFeeDto::Fix(U128(10))).into(); - // context.switch_account(&admin); - // context.with_deposit_yocto(1, |context| { - // context - // .contract - // .register_product(get_register_flexible_product_command()) - // }); - // - // let ticket = JarTicket { - // product_id: product.id, - // valid_until: U64(0), - // }; - // - // context - // .contract - // .create_jar(alice.clone(), ticket, U128(1_000_000), None); + fn test_failed_withdraw() { + let (_, _, mut context) = prepare_jar(get_register_product_command()); + + let jar_view = context.contract.get_jar(U32(0)); + let jar = context.contract.jars[0].clone(); + let withdraw = context.contract.after_withdraw_internal(jar, 1234, None, false); + + assert_eq!(withdraw.withdrawn_amount, U128(0)); + assert_eq!(withdraw.fee, U128(0)); + + assert_eq!(jar_view, context.contract.get_jar(U32(0))); } } diff --git a/contract/src/withdraw/view.rs b/contract/src/withdraw/view.rs index eaddcef3..9a1c29d1 100644 --- a/contract/src/withdraw/view.rs +++ b/contract/src/withdraw/view.rs @@ -10,10 +10,10 @@ use crate::{common::TokenAmount, ft_interface::Fee}; #[serde(crate = "near_sdk::serde")] pub struct WithdrawView { /// The amount of tokens that has been transferred to the user's account as part of the withdrawal. - withdrawn_amount: U128, + pub withdrawn_amount: U128, /// The possible fee that a user must pay for withdrawal, if it's defined by the associated Product. - fee: U128, + pub fee: U128, } impl WithdrawView { diff --git a/res/sweat_jar.wasm b/res/sweat_jar.wasm index c26b40f3..af6f21e3 100755 Binary files a/res/sweat_jar.wasm and b/res/sweat_jar.wasm differ