diff --git a/Cargo.lock b/Cargo.lock index 8035d1d..35ac18c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,6 +773,7 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" name = "locked_in" version = "0.0.1" dependencies = [ + "loktin-common", "mock_pool", "soroban-sdk", "stellar-xdr", @@ -784,6 +785,13 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "loktin-common" +version = "0.0.1" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -805,6 +813,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" name = "mock_pool" version = "0.0.1" dependencies = [ + "loktin-common", "soroban-sdk", "stellar-xdr", ] @@ -892,6 +901,7 @@ dependencies = [ name = "plans" version = "0.0.1" dependencies = [ + "loktin-common", "soroban-sdk", "stellar-xdr", ] @@ -1349,6 +1359,7 @@ dependencies = [ name = "spend_save" version = "0.0.1" dependencies = [ + "loktin-common", "soroban-sdk", "stellar-xdr", ] @@ -1442,6 +1453,7 @@ dependencies = [ name = "target_savings" version = "0.0.1" dependencies = [ + "loktin-common", "mock_pool", "soroban-sdk", "stellar-xdr", diff --git a/Cargo.toml b/Cargo.toml index ad48642..0f754e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["contracts/*"] +members = ["contracts/*", "crates/*"] resolver = "2" [workspace.package] @@ -12,6 +12,9 @@ version = "0.0.1" [workspace.dependencies.soroban-sdk] version = "23.0.2" +[workspace.dependencies.loktin-common] +path = "crates/loktin-common" + [workspace.dependencies.stellar-access] git = "https://github.com/OpenZeppelin/stellar-contracts" tag = "v0.5.1" diff --git a/contracts/locked_in/Cargo.toml b/contracts/locked_in/Cargo.toml index d17e015..7a82d98 100644 --- a/contracts/locked_in/Cargo.toml +++ b/contracts/locked_in/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib"] doctest = false [dependencies] +loktin-common.workspace = true soroban-sdk = "23.0.3" [dev-dependencies] diff --git a/contracts/locked_in/src/lib.rs b/contracts/locked_in/src/lib.rs index 8d55516..5528456 100644 --- a/contracts/locked_in/src/lib.rs +++ b/contracts/locked_in/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +use loktin_common::{LEDGER_TTL_THRESHOLD, LEDGER_TTL_EXTEND}; mod error; mod events; @@ -14,9 +15,7 @@ use soroban_sdk::{ use error::Error; use types::{DataKey, Lock}; -const DAY_IN_LEDGERS: u32 = 17280; -const LEDGER_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30; -const LEDGER_TTL_EXTEND: u32 = DAY_IN_LEDGERS * 365; + const SECONDS_PER_MONTH: u64 = 2_592_000; // 30 days const SECONDS_PER_YEAR: i128 = 31_536_000; diff --git a/contracts/mock_pool/Cargo.toml b/contracts/mock_pool/Cargo.toml index 5038a5c..3dba0c1 100644 --- a/contracts/mock_pool/Cargo.toml +++ b/contracts/mock_pool/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["lib", "cdylib"] doctest = false [dependencies] +loktin-common.workspace = true soroban-sdk = "23.0.3" [dev-dependencies] diff --git a/contracts/mock_pool/src/lib.rs b/contracts/mock_pool/src/lib.rs index 7c0d949..d8b3a82 100644 --- a/contracts/mock_pool/src/lib.rs +++ b/contracts/mock_pool/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +use loktin_common::{LEDGER_TTL_THRESHOLD, LEDGER_TTL_EXTEND}; //! Mock Blend pool. //! @@ -23,9 +24,7 @@ use soroban_sdk::{contract, contractimpl, token, Address, Env}; use error::Error; use types::{DataKey, Position}; -const DAY_IN_LEDGERS: u32 = 17280; -const LEDGER_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30; -const LEDGER_TTL_EXTEND: u32 = DAY_IN_LEDGERS * 365; + const BPS_DENOMINATOR: i128 = 10_000; const SECONDS_PER_YEAR: i128 = 31_536_000; diff --git a/contracts/plans/Cargo.toml b/contracts/plans/Cargo.toml index 2c340a3..d22b8c1 100644 --- a/contracts/plans/Cargo.toml +++ b/contracts/plans/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib"] doctest = false [dependencies] +loktin-common.workspace = true soroban-sdk = "23.0.3" [dev-dependencies] diff --git a/contracts/plans/src/lib.rs b/contracts/plans/src/lib.rs index e07f34c..795d181 100644 --- a/contracts/plans/src/lib.rs +++ b/contracts/plans/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +use loktin_common::{LEDGER_TTL_THRESHOLD, LEDGER_TTL_EXTEND}; mod error; mod events; @@ -10,9 +11,7 @@ use soroban_sdk::{contract, contractimpl, token, Address, Env, String, Vec}; use error::Error; use types::{Bill, BillCycle, DataKey}; -const DAY_IN_LEDGERS: u32 = 17280; // ~24 hours -const LEDGER_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30; // 30 days -const LEDGER_TTL_EXTEND: u32 = DAY_IN_LEDGERS * 365; // 1 year + #[contract] pub struct Plans; diff --git a/contracts/spend_save/Cargo.toml b/contracts/spend_save/Cargo.toml index 845b016..b4dbb41 100644 --- a/contracts/spend_save/Cargo.toml +++ b/contracts/spend_save/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib"] doctest = false [dependencies] +loktin-common.workspace = true soroban-sdk = "23.0.3" [dev-dependencies] diff --git a/contracts/spend_save/src/error.rs b/contracts/spend_save/src/error.rs index 045d7ad..839bda0 100644 --- a/contracts/spend_save/src/error.rs +++ b/contracts/spend_save/src/error.rs @@ -11,4 +11,7 @@ pub enum Error { InvalidAmount = 12, NotWithdrawalDay = 13, InsufficientSavedBalance = 14, + PoolNotSet = 15, + UsdcTokenNotSet = 16, + ZeroShares = 17, } diff --git a/contracts/spend_save/src/lib.rs b/contracts/spend_save/src/lib.rs index a7367c8..341d165 100644 --- a/contracts/spend_save/src/lib.rs +++ b/contracts/spend_save/src/lib.rs @@ -1,17 +1,26 @@ #![no_std] +use loktin_common::{LEDGER_TTL_THRESHOLD, LEDGER_TTL_EXTEND}; mod error; mod events; mod types; -use soroban_sdk::{contract, contractimpl, token, Address, Env}; +use soroban_sdk::auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}; +use soroban_sdk::{ + contract, contractclient, contractimpl, symbol_short, token, vec, Address, Env, IntoVal, +}; use error::Error; use types::{DataKey, SpendSavePosition}; -const DAY_IN_LEDGERS: u32 = 17280; -const LEDGER_TTL_THRESHOLD: u32 = DAY_IN_LEDGERS * 30; -const LEDGER_TTL_EXTEND: u32 = DAY_IN_LEDGERS * 365; +#[contractclient(name = "PoolClient")] +pub trait Pool { + fn supply(env: Env, from: Address, amount: i128); + fn withdraw(env: Env, from: Address, amount: i128) -> i128; + fn get_position(env: Env, supplier: Address) -> i128; +} + + const BPS_DENOMINATOR: i128 = 10_000; const SECONDS_PER_DAY: u64 = 86_400; @@ -36,8 +45,19 @@ impl SpendSave { env.storage().instance().get(&DataKey::Admin).ok_or(Error::AdminNotSet) } - pub fn usdc_token(env: Env) -> Address { - env.storage().instance().get(&DataKey::UsdcToken).unwrap() + pub fn usdc_token(env: Env) -> Result
{ + env.storage().instance().get(&DataKey::UsdcToken).ok_or(Error::UsdcTokenNotSet) + } + + pub fn set_pool(env: Env, pool: Address) -> Result<(), Error> { + let admin = Self::admin(env.clone())?; + admin.require_auth(); + env.storage().instance().set(&DataKey::Pool, &pool); + Ok(()) + } + + pub fn pool(env: Env) -> Result { + Self::pool_addr(&env) } // User functions @@ -60,6 +80,7 @@ impl SpendSave { user: user.clone(), save_percentage: save_percentage_bps, saved_balance: 0, + shares: 0, total_spent_lifetime: 0, total_saved_lifetime: 0, created_date: now, @@ -85,18 +106,36 @@ impl SpendSave { let saved = total_amount * (position.save_percentage as i128) / BPS_DENOMINATOR; let sent = total_amount - saved; - let token = Self::token_client(&env); + let token = Self::token_client(&env)?; + let self_addr = env.current_contract_address(); + + let total_assets = token.balance(&self_addr) + Self::blend_position_internal(&env); + let total_shares = Self::total_shares(&env); + + let shares_to_mint = if total_shares == 0 || total_assets == 0 { + saved + } else { + saved * total_shares / total_assets + }; + + if shares_to_mint == 0 && saved > 0 { + return Err(Error::ZeroShares); + } + // Pull total_amount from user - token.transfer(&user, &env.current_contract_address(), &total_amount); + token.transfer(&user, &self_addr, &total_amount); // Forward sent to recipient if sent > 0 { - token.transfer(&env.current_contract_address(), &recipient, &sent); + token.transfer(&self_addr, &recipient, &sent); } // saved stays in this contract position.saved_balance += saved; - position.total_spent_lifetime += sent; + position.shares += shares_to_mint; + position.total_spent_lifetime += total_amount; position.total_saved_lifetime += saved; + + env.storage().instance().set(&DataKey::TotalShares, &(total_shares + shares_to_mint)); env.storage().persistent().set(&DataKey::Position(user.clone()), &position); Self::extend_ttl(&env, &DataKey::Position(user.clone())); @@ -120,10 +159,54 @@ impl SpendSave { return Err(Error::InsufficientSavedBalance); } - let token = Self::token_client(&env); - token.transfer(&env.current_contract_address(), &user, &amount); + let token = Self::token_client(&env)?; + let self_addr = env.current_contract_address(); + + let total_assets = token.balance(&self_addr) + Self::blend_position_internal(&env); + let total_shares = Self::total_shares(&env); + + let shares_to_burn = if total_assets == 0 { + amount + } else { + (amount * total_shares + total_assets - 1) / total_assets + }; + + if shares_to_burn == 0 || shares_to_burn > position.shares { + return Err(Error::InsufficientSavedBalance); + } + + let idle = token.balance(&self_addr); + if idle < amount { + let pulled = match Self::pool_addr(&env) { + Ok(pool) => { + let pc = PoolClient::new(&env, &pool); + let pool_pos = pc.get_position(&self_addr); + let shortfall = amount - idle; + let pull = if shortfall <= pool_pos { shortfall } else { pool_pos }; + if pull > 0 { + pc.withdraw(&self_addr, &pull); + } + pull + } + Err(_) => 0, + }; + if idle + pulled < amount { + return Err(Error::InsufficientSavedBalance); + } + } + + token.transfer(&self_addr, &user, &amount); - position.saved_balance -= amount; + position.saved_balance -= amount; // wait, if they earned yield, this could go negative? + // Actually, if amount > saved_balance, we should handle it, or just use saved_balance as principal tracker loosely. + if position.saved_balance < amount { + position.saved_balance = 0; + } else { + position.saved_balance -= amount; + } + position.shares -= shares_to_burn; + + env.storage().instance().set(&DataKey::TotalShares, &(total_shares - shares_to_burn)); env.storage().persistent().set(&DataKey::Position(user.clone()), &position); Self::extend_ttl(&env, &DataKey::Position(user.clone())); @@ -152,6 +235,26 @@ impl SpendSave { pub fn deposit_to_blend(env: Env, amount: i128) -> Result<(), Error> { let admin = Self::admin(env.clone())?; admin.require_auth(); + if amount <= 0 { + return Err(Error::InvalidAmount); + } + let pool = Self::pool_addr(&env)?; + let token = Self::usdc_token(env.clone())?; + let self_addr = env.current_contract_address(); + + env.authorize_as_current_contract(vec![ + &env, + InvokerContractAuthEntry::Contract(SubContractInvocation { + context: ContractContext { + contract: token.clone(), + fn_name: symbol_short!("transfer"), + args: (self_addr.clone(), pool.clone(), amount).into_val(&env), + }, + sub_invocations: vec![&env], + }), + ]); + + PoolClient::new(&env, &pool).supply(&self_addr, &amount); events::BlendDeposit { amount }.publish(&env); Ok(()) } @@ -159,19 +262,41 @@ impl SpendSave { pub fn withdraw_from_blend(env: Env, amount: i128) -> Result<(), Error> { let admin = Self::admin(env.clone())?; admin.require_auth(); + if amount <= 0 { + return Err(Error::InvalidAmount); + } + let pool = Self::pool_addr(&env)?; + PoolClient::new(&env, &pool).withdraw(&env.current_contract_address(), &amount); events::BlendWithdraw { amount }.publish(&env); Ok(()) } - pub fn blend_position(_env: Env) -> i128 { - 0 + pub fn blend_position(env: Env) -> i128 { + Self::blend_position_internal(&env) } // Internal helpers - fn token_client(env: &Env) -> token::TokenClient<'_> { - let token: Address = env.storage().instance().get(&DataKey::UsdcToken).unwrap(); - token::TokenClient::new(env, &token) + fn token_client(env: &Env) -> Result