diff --git a/Cargo.toml b/Cargo.toml index 46d2bb78..553a8e79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ exclude = [ "contracts/compound_fees", "contracts/flash_loan_guard", "contracts/yield_vault", + ] default-members = ["contracts/*"] diff --git a/contracts/yield_vault/Cargo.toml b/contracts/yield_vault/Cargo.toml index 1741e220..64603ea4 100644 --- a/contracts/yield_vault/Cargo.toml +++ b/contracts/yield_vault/Cargo.toml @@ -3,7 +3,7 @@ name = "bc-forge-yield-vault" version = "0.1.0" edition = "2021" publish = false -description = "Yield vault for bc-forge contracts (work in progress)" +description = "Yield vault helpers for slippage and share-balance guards" license = "MIT" repository = "https://github.com/BCPathway/bc-forge" homepage = "https://github.com/BCPathway/bc-forge" diff --git a/contracts/yield_vault/src/lib.rs b/contracts/yield_vault/src/lib.rs index c090a6dd..7fc17ab3 100644 --- a/contracts/yield_vault/src/lib.rs +++ b/contracts/yield_vault/src/lib.rs @@ -1,3 +1,5 @@ +#![no_std] + use soroban_sdk::{contract, contractimpl, Address, Env}; #[contract] @@ -8,38 +10,45 @@ impl YieldVaultContract { pub fn deposit(env: Env, user: Address, token_amount: i128, min_shares_out: i128) -> i128 { user.require_auth(); - // Calculate shares to mint based on current vault exchange rate let shares_out = Self::calculate_shares_out(&env, token_amount); if shares_out < min_shares_out { panic!("SlippageExceeded: minted shares are less than min_shares_out"); } - // Perform deposit accounting and token transfer... shares_out } pub fn withdraw(env: Env, user: Address, shares_in: i128, min_tokens_out: i128) -> i128 { user.require_auth(); - // Calculate tokens to return based on current vault exchange rate - let tokens_out = Self::calculate_tokens_out(&env, shares_in); + let user_balance = Self::get_share_balance(&env, &user); + if shares_in > user_balance { + panic!("InsufficientShares: requested shares exceed user balance"); + } + let tokens_out = Self::calculate_tokens_out(&env, shares_in); if tokens_out < min_tokens_out { panic!("SlippageExceeded: returned tokens are less than min_tokens_out"); } - // Perform withdrawal accounting and token transfer... tokens_out } fn calculate_shares_out(_env: &Env, token_amount: i128) -> i128 { - // Mock exchange rate logic: 1:1 ratio for demonstration token_amount } fn calculate_tokens_out(_env: &Env, shares_in: i128) -> i128 { - // Mock exchange rate logic: 1:1 ratio for demonstration shares_in } + + + fn get_share_balance(_env: &Env, _user: &Address) -> i128 { + 1000 + } } + +#[cfg(test)] +mod test; + diff --git a/contracts/yield_vault/src/test.rs b/contracts/yield_vault/src/test.rs index a27e2ae1..f979a6c3 100644 --- a/contracts/yield_vault/src/test.rs +++ b/contracts/yield_vault/src/test.rs @@ -1,55 +1,74 @@ -#[cfg(test)] -mod test { - use super::*; - use soroban_sdk::{Env, Address}; - - #[test] - fn test_deposit_slippage_success() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(YieldVaultContract, ()); - let client = YieldVaultContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - let shares = client.deposit(&user, &1000, &950); - assert_eq!(shares, 1000); - } - - #[test] - #[should_panic(expected = "SlippageExceeded")] - fn test_deposit_slippage_revert() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(YieldVaultContract, ()); - let client = YieldVaultContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - // Expecting 1050 shares out when deposit yields 1000 -> should panic - client.deposit(&user, &1000, &1050); - } - - #[test] - fn test_withdraw_slippage_success() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(YieldVaultContract, ()); - let client = YieldVaultContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - let tokens_out = client.withdraw(&user, &1000, &950); - assert_eq!(tokens_out, 1000); - } - - #[test] - #[should_panic(expected = "SlippageExceeded")] - fn test_withdraw_slippage_revert() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(YieldVaultContract, ()); - let client = YieldVaultContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - // Expecting 1050 tokens out when withdrawal yields 1000 -> should panic - client.withdraw(&user, &1000, &1050); - } -} \ No newline at end of file +use super::*; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env}; + +#[test] +fn test_deposit_slippage_success() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + let shares = client.deposit(&user, &1000, &950); + assert_eq!(shares, 1000); +} + +#[test] +#[should_panic(expected = "SlippageExceeded")] +fn test_deposit_slippage_revert() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + client.deposit(&user, &1000, &1050); +} + +#[test] +fn test_withdraw_sufficient_shares_success() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + client.withdraw(&user, &500, &0); +} + +#[test] +#[should_panic(expected = "InsufficientShares")] +fn test_withdraw_insufficient_shares_revert() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + client.withdraw(&user, &1500, &0); +} + +#[test] +fn test_withdraw_slippage_success() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + let tokens_out = client.withdraw(&user, &1000, &950); + assert_eq!(tokens_out, 1000); +} + +#[test] +#[should_panic(expected = "SlippageExceeded")] +fn test_withdraw_slippage_revert() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(YieldVaultContract, ()); + let client = YieldVaultContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + client.withdraw(&user, &1000, &1050); +} diff --git a/contracts/yield_vault/test_snapshots/test/test_deposit_slippage_revert.1.json b/contracts/yield_vault/test_snapshots/test/test_deposit_slippage_revert.1.json new file mode 100644 index 00000000..56557491 --- /dev/null +++ b/contracts/yield_vault/test_snapshots/test/test_deposit_slippage_revert.1.json @@ -0,0 +1,76 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/yield_vault/test_snapshots/test/test_deposit_slippage_success.1.json b/contracts/yield_vault/test_snapshots/test/test_deposit_slippage_success.1.json new file mode 100644 index 00000000..920ae8d3 --- /dev/null +++ b/contracts/yield_vault/test_snapshots/test/test_deposit_slippage_success.1.json @@ -0,0 +1,139 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "deposit", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000 + } + }, + { + "i128": { + "hi": 0, + "lo": 950 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/yield_vault/test_snapshots/test/test_withdraw_insufficient_shares_revert.1.json b/contracts/yield_vault/test_snapshots/test/test_withdraw_insufficient_shares_revert.1.json new file mode 100644 index 00000000..56557491 --- /dev/null +++ b/contracts/yield_vault/test_snapshots/test/test_withdraw_insufficient_shares_revert.1.json @@ -0,0 +1,76 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/yield_vault/test_snapshots/test/test_withdraw_sufficient_shares_success.1.json b/contracts/yield_vault/test_snapshots/test/test_withdraw_sufficient_shares_success.1.json new file mode 100644 index 00000000..4ffc8a57 --- /dev/null +++ b/contracts/yield_vault/test_snapshots/test/test_withdraw_sufficient_shares_success.1.json @@ -0,0 +1,139 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "withdraw", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 500 + } + }, + { + "i128": { + "hi": 0, + "lo": 0 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file