@layerzerolabs/protocol-stellar-v2 0.2.65 → 0.2.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +218 -299
  2. package/.turbo/turbo-lint.log +225 -98
  3. package/.turbo/turbo-test.log +2010 -1924
  4. package/Cargo.lock +0 -16
  5. package/Cargo.toml +0 -1
  6. package/contracts/oapps/oft/integration-tests/extensions/test_oft_fee.rs +22 -0
  7. package/contracts/oapps/oft/integration-tests/extensions/test_pausable.rs +9 -2
  8. package/contracts/oapps/oft/integration-tests/extensions/test_rate_limiter.rs +27 -2
  9. package/contracts/oapps/oft/integration-tests/setup.rs +22 -18
  10. package/contracts/oapps/oft/integration-tests/utils.rs +81 -34
  11. package/contracts/oapps/oft/src/extensions/oft_fee.rs +13 -0
  12. package/contracts/oapps/oft/src/oft.rs +10 -2
  13. package/package.json +4 -4
  14. package/sdk/.turbo/turbo-test.log +299 -307
  15. package/sdk/dist/generated/oft.d.ts +3 -3
  16. package/sdk/dist/generated/oft.js +3 -3
  17. package/sdk/node_modules/.bin/vitest +2 -2
  18. package/sdk/package.json +1 -1
  19. package/contracts/oapps/console-oft/Cargo.toml +0 -30
  20. package/contracts/oapps/console-oft/integration-tests/extensions/mod.rs +0 -5
  21. package/contracts/oapps/console-oft/integration-tests/extensions/test_combined.rs +0 -90
  22. package/contracts/oapps/console-oft/integration-tests/extensions/test_oft_fee.rs +0 -186
  23. package/contracts/oapps/console-oft/integration-tests/extensions/test_ownership.rs +0 -161
  24. package/contracts/oapps/console-oft/integration-tests/extensions/test_pausable.rs +0 -154
  25. package/contracts/oapps/console-oft/integration-tests/extensions/test_rate_limiter.rs +0 -479
  26. package/contracts/oapps/console-oft/integration-tests/mod.rs +0 -3
  27. package/contracts/oapps/console-oft/integration-tests/setup.rs +0 -303
  28. package/contracts/oapps/console-oft/integration-tests/utils.rs +0 -685
  29. package/contracts/oapps/console-oft/src/errors.rs +0 -7
  30. package/contracts/oapps/console-oft/src/extensions/mod.rs +0 -3
  31. package/contracts/oapps/console-oft/src/extensions/oft_fee.rs +0 -239
  32. package/contracts/oapps/console-oft/src/extensions/pausable.rs +0 -185
  33. package/contracts/oapps/console-oft/src/extensions/rate_limiter.rs +0 -478
  34. package/contracts/oapps/console-oft/src/interfaces/mintable.rs +0 -14
  35. package/contracts/oapps/console-oft/src/interfaces/mod.rs +0 -3
  36. package/contracts/oapps/console-oft/src/lib.rs +0 -26
  37. package/contracts/oapps/console-oft/src/oft.rs +0 -208
  38. package/contracts/oapps/console-oft/src/oft_access_control.rs +0 -93
  39. package/contracts/oapps/console-oft/src/oft_types/lock_unlock.rs +0 -50
  40. package/contracts/oapps/console-oft/src/oft_types/mint_burn.rs +0 -50
  41. package/contracts/oapps/console-oft/src/oft_types/mod.rs +0 -24
  42. package/contracts/oapps/console-oft/src/tests/extensions/mod.rs +0 -3
  43. package/contracts/oapps/console-oft/src/tests/extensions/oft_fee.rs +0 -255
  44. package/contracts/oapps/console-oft/src/tests/extensions/pausable.rs +0 -212
  45. package/contracts/oapps/console-oft/src/tests/extensions/rate_limiter.rs +0 -992
  46. package/contracts/oapps/console-oft/src/tests/mod.rs +0 -2
  47. package/contracts/oapps/console-oft/src/tests/oft_types/lock_unlock.rs +0 -185
  48. package/contracts/oapps/console-oft/src/tests/oft_types/mod.rs +0 -1
@@ -1,208 +0,0 @@
1
- //! Console OFT — Omnichain Fungible Token with RBAC, pausable, fee, and rate limiter extensions.
2
- //!
3
- //! Supports two operating modes:
4
- //!
5
- //! - **LockUnlock**: Locks tokens on send (transfer to contract), unlocks on receive.
6
- //! - **MintBurn**: Burns tokens on send, mints on receive via a configurable `Mintable` contract.
7
- //!
8
- //! ## Extension Hooks (applied in `__debit` / `__credit`)
9
- //!
10
- //! **Send path (`__debit`):**
11
- //! 1. Pause check (`__assert_not_paused`) — reverts if paused for the destination ID
12
- //! 2. Token debit (burn or lock `amount_received_ld`)
13
- //! 3. Rate limit outflow (`__outflow`) — consumes outbound capacity, releases inbound if net accounting
14
- //! 4. Fee charge (`__charge_fee`) — transfers fee to the fee deposit address
15
- //!
16
- //! **Receive path (`__credit`):**
17
- //! 1. Rate limit inflow (`__inflow`) — consumes inbound capacity, releases outbound if net accounting
18
- //! 2. Token credit (mint or unlock `amount_ld`)
19
- //! 3. No pause check, no fee
20
-
21
- use crate::{
22
- self as oft,
23
- extensions::{
24
- oft_fee::{OFTFee, OFTFeeInternal},
25
- pausable::{OFTPausable, OFTPausableInternal},
26
- rate_limiter::{RateLimiter, RateLimiterInternal, UNLIMITED_AMOUNT},
27
- },
28
- oft_types::{lock_unlock, mint_burn, OftType},
29
- };
30
- use common_macros::{contract_impl, storage, ttl_configurable, ttl_extendable, upgradeable};
31
- use oapp_macros::oapp;
32
- use oft_core::{
33
- assert_nonnegative_amount, impl_oft_lz_receive, utils as oft_utils, OFTCore, OFTError, OFTFeeDetail, OFTInternal,
34
- OFTLimit, OFTReceipt, SendParam,
35
- };
36
- use soroban_sdk::{assert_with_error, contract, vec, Address, Bytes, Env, Vec};
37
-
38
- // =========================================================================
39
- // Storage
40
- // =========================================================================
41
-
42
- #[storage]
43
- enum OFTStorage {
44
- #[instance(OftType)]
45
- OftType,
46
- }
47
-
48
- // =========================================================================
49
- // OFT Contract
50
- // =========================================================================
51
-
52
- #[contract]
53
- #[ttl_configurable]
54
- #[ttl_extendable]
55
- #[upgradeable(no_migration, rbac)]
56
- #[oapp(custom = [core])]
57
- pub struct OFT;
58
-
59
- // LzReceiveInternal implementation using default OFT receive logic
60
- impl_oft_lz_receive!(OFT);
61
-
62
- #[contract_impl]
63
- impl OFT {
64
- pub fn __constructor(
65
- env: &Env,
66
- token: &Address,
67
- shared_decimals: u32,
68
- oft_type: OftType,
69
- endpoint: &Address,
70
- delegate: &Address,
71
- fee_deposit: &Address,
72
- ) {
73
- Self::__initialize_oft(env, token, shared_decimals, delegate, endpoint, delegate);
74
- OFTStorage::set_oft_type(env, &oft_type);
75
- Self::__set_fee_deposit(env, fee_deposit);
76
- }
77
-
78
- /// Returns the OFT type with its target address and configuration.
79
- pub fn oft_type(env: &Env) -> OftType {
80
- OFTStorage::oft_type(env).unwrap()
81
- }
82
- }
83
-
84
- /// OFTCore trait implementation for console OFT with extensions
85
- #[contract_impl(contracttrait)]
86
- impl OFTCore for OFT {
87
- fn quote_oft(env: &Env, _from: &Address, send_param: &SendParam) -> (OFTLimit, Vec<OFTFeeDetail>, OFTReceipt) {
88
- assert_nonnegative_amount(env, send_param);
89
-
90
- let dst_id = send_param.dst_eid as u128;
91
-
92
- // 1. Rate limit capacity, accounting for pause
93
- let mut max_amount_ld = if Self::is_paused(env, dst_id) {
94
- 0
95
- } else {
96
- Self::get_rate_limit_usages(env, dst_id).outbound_available_amount
97
- };
98
-
99
- // 2. Back-calculate max sendable amount accounting for fees.
100
- // If the rate limit is unlimited, that means there is no rate limiting applied.
101
- if max_amount_ld != UNLIMITED_AMOUNT {
102
- max_amount_ld = Self::get_amount_before_fee(env, dst_id, max_amount_ld);
103
- }
104
-
105
- let oft_limit = OFTLimit { min_amount_ld: 0, max_amount_ld };
106
-
107
- // 3. Compute receipt
108
- let (amount_sent_ld, amount_received_ld) =
109
- Self::__debit_view(env, send_param.amount_ld, send_param.min_amount_ld, send_param.dst_eid);
110
- let oft_receipt = OFTReceipt { amount_sent_ld, amount_received_ld };
111
-
112
- // 4. Fee details
113
- let fee_details = if amount_sent_ld > amount_received_ld {
114
- vec![
115
- env,
116
- OFTFeeDetail {
117
- fee_amount_ld: amount_sent_ld - amount_received_ld,
118
- description: Bytes::from_slice(env, b"Fee"),
119
- },
120
- ]
121
- } else {
122
- vec![env]
123
- };
124
-
125
- (oft_limit, fee_details, oft_receipt)
126
- }
127
- }
128
-
129
- /// OFT behavior for Console OFT with extension hooks
130
- impl OFTInternal for OFT {
131
- /// Calculates the amounts for a send without executing token operations.
132
- ///
133
- /// UI should ensure amounts are dust-free to avoid unintended fees.
134
- fn __debit_view(env: &Env, amount_ld: i128, min_amount_ld: i128, dst_eid: u32) -> (i128, i128) {
135
- let fee = Self::__get_fee(env, dst_eid as u128, amount_ld);
136
- let conversion_rate = Self::__decimal_conversion_rate(env);
137
- let amount_received_ld = oft_utils::remove_dust(amount_ld - fee, conversion_rate);
138
-
139
- assert_with_error!(env, amount_received_ld >= min_amount_ld, OFTError::SlippageExceeded);
140
-
141
- (amount_ld, amount_received_ld)
142
- }
143
-
144
- /// Executes the full send-side debit with all extension hooks.
145
- ///
146
- /// Flow: pause check → token debit → rate limit outflow → fee charge.
147
- fn __debit(env: &Env, sender: &Address, amount_ld: i128, min_amount_ld: i128, dst_eid: u32) -> (i128, i128) {
148
- Self::__assert_not_paused(env, dst_eid as u128);
149
-
150
- let (amount_sent_ld, amount_received_ld) = match Self::oft_type(env) {
151
- OftType::LockUnlock => {
152
- lock_unlock::debit::<Self>(env, &Self::token(env), sender, amount_ld, min_amount_ld, dst_eid)
153
- }
154
- OftType::MintBurn(_mintable) => {
155
- mint_burn::debit::<Self>(env, &Self::token(env), sender, amount_ld, min_amount_ld, dst_eid)
156
- }
157
- };
158
-
159
- Self::__outflow(env, dst_eid as u128, sender, amount_received_ld);
160
-
161
- let fee = amount_sent_ld - amount_received_ld;
162
- Self::__charge_fee(env, &Self::token(env), sender, fee);
163
-
164
- (amount_sent_ld, amount_received_ld)
165
- }
166
-
167
- /// Executes the full receive-side credit with extension hooks.
168
- ///
169
- /// Flow: rate limit inflow → token credit.
170
- ///
171
- /// No pause check on receive — this is intentional to prevent in-flight token lockups
172
- /// when a chain is paused mid-transfer. Matches EVM where `whenNotPaused` is only on
173
- /// `_debit`, not `_credit`.
174
- fn __credit(env: &Env, to: &Address, amount_ld: i128, src_eid: u32) -> i128 {
175
- Self::__inflow(env, src_eid as u128, to, amount_ld);
176
-
177
- match Self::oft_type(env) {
178
- OftType::LockUnlock => lock_unlock::credit::<Self>(env, &Self::token(env), to, amount_ld, src_eid),
179
- OftType::MintBurn(mintable) => mint_burn::credit::<Self>(env, &mintable, to, amount_ld, src_eid),
180
- }
181
- }
182
- }
183
-
184
- // =========================================================================
185
- // Extension Trait Implementations
186
- // =========================================================================
187
-
188
- /// Pausable extension — per-Destination ID pause/unpause with separate PAUSER/UNPAUSER roles.
189
- /// Default state: unpaused. Only enforced on send path (`__debit`), not receive (`__credit`).
190
- #[contract_impl(contracttrait)]
191
- impl OFTPausable for OFT {}
192
- impl OFTPausableInternal for OFT {}
193
-
194
- /// Fee extension — proportional fee on outbound transfers.
195
- /// Default state: 0 BPS (no fee). Fees transferred to the fee deposit address.
196
- #[contract_impl(contracttrait)]
197
- impl OFTFee for OFT {}
198
- impl OFTFeeInternal for OFT {}
199
-
200
- /// Rate limiter extension — rolling window decay with net accounting.
201
- /// Default state: closed (limits=0, outbound+inbound enabled). Admin must set limits to open.
202
- #[contract_impl(contracttrait)]
203
- impl RateLimiter for OFT {}
204
- impl RateLimiterInternal for OFT {}
205
-
206
- // Console-specific access control (ownership, auth, RBAC)
207
- #[path = "oft_access_control.rs"]
208
- mod oft_access_control;
@@ -1,93 +0,0 @@
1
- //! Console-specific access control: ownership, auth, and RBAC.
2
- //!
3
- //! - Delegate is permanently synced with the owner.
4
- //! - `transfer_ownership` (single-step) and `renounce_ownership` are disabled.
5
- //! - Only 2-step transfer via `begin_ownership_transfer` + `accept_ownership`.
6
- //! - `accept_ownership` auto-syncs the endpoint delegate to the new owner.
7
-
8
- use super::{OFTArgs, OFTClient, OFT};
9
- use crate::errors::OFTError;
10
- use common_macros::contract_impl;
11
- use endpoint_v2::LayerZeroEndpointV2Client;
12
- use oapp::oapp_core::OAppCore;
13
- use soroban_sdk::{panic_with_error, Address, Env};
14
- use utils::{
15
- auth::Auth,
16
- ownable::{Ownable, OwnableInitializer},
17
- rbac::RoleBasedAccessControl,
18
- };
19
-
20
- // =========================================================================
21
- // Ownable & Auth implementation for OFT
22
- // =========================================================================
23
-
24
- impl OwnableInitializer for OFT {}
25
-
26
- #[contract_impl]
27
- impl Auth for OFT {
28
- fn authorizer(env: &soroban_sdk::Env) -> Option<Address> {
29
- Self::owner(env)
30
- }
31
- }
32
-
33
- #[contract_impl(contracttrait)]
34
- impl Ownable for OFT {
35
- /// Disabled one-step ownership transfer.
36
- fn transfer_ownership(env: &soroban_sdk::Env, _new_owner: &soroban_sdk::Address) {
37
- panic_with_error!(env, OFTError::Disabled);
38
- }
39
-
40
- /// Disabled renounce ownership.
41
- fn renounce_ownership(env: &soroban_sdk::Env) {
42
- panic_with_error!(env, OFTError::Disabled);
43
- }
44
-
45
- /// Accepts ownership and syncs the delegate on the endpoint to the new owner.
46
- fn accept_ownership(env: &soroban_sdk::Env) {
47
- OwnableDefault::accept_ownership(env);
48
-
49
- let new_owner = Self::owner(env).unwrap();
50
- LayerZeroEndpointV2Client::new(env, &Self::endpoint(env))
51
- .set_delegate(&env.current_contract_address(), &Some(new_owner));
52
- }
53
- }
54
-
55
- // =========================================================================
56
- // RoleBasedAccessControl implementation
57
- // =========================================================================
58
-
59
- #[contract_impl(contracttrait)]
60
- impl RoleBasedAccessControl for OFT {}
61
-
62
- // =========================================================================
63
- // OAppCore override — set_delegate disabled
64
- // =========================================================================
65
-
66
- #[contract_impl(contracttrait)]
67
- impl OAppCore for OFT {
68
- /// Disabled set delegate.
69
- fn set_delegate(
70
- env: &soroban_sdk::Env,
71
- _delegate: &Option<soroban_sdk::Address>,
72
- _operator: &soroban_sdk::Address,
73
- ) {
74
- panic_with_error!(env, OFTError::Disabled);
75
- }
76
- }
77
-
78
- // =========================================================================
79
- // Helper: access default Ownable implementation
80
- // =========================================================================
81
-
82
- /// Helper type to call the default `accept_ownership` logic before applying
83
- /// Console-specific post-acceptance hooks (delegate sync).
84
- /// Reads from the same `OwnableStorage` — no duplication of logic.
85
- struct OwnableDefault;
86
-
87
- impl Ownable for OwnableDefault {}
88
-
89
- impl Auth for OwnableDefault {
90
- fn authorizer(env: &Env) -> Option<Address> {
91
- <Self as Ownable>::owner(env)
92
- }
93
- }
@@ -1,50 +0,0 @@
1
- //! LockUnlock type implementation for OFT.
2
- //!
3
- //! This OFT type locks tokens in the contract on debit (send) and unlocks
4
- //! tokens from the contract on credit (receive).
5
- //! Operates directly on the token via standard SEP-41 `transfer`.
6
-
7
- use oft_core::OFTCore;
8
- use soroban_sdk::{token::TokenClient, Address, Env};
9
-
10
- /// Debit tokens using LockUnlock OFT type (locks tokens in contract).
11
- ///
12
- /// # Parameters
13
- /// * `env` - The Soroban environment
14
- /// * `token` - Address of the token
15
- /// * `sender` - Address of the token sender
16
- /// * `amount_ld` - Amount to debit in local decimals
17
- /// * `min_amount_ld` - Minimum amount that must be received (for slippage protection)
18
- /// * `dst_eid` - Destination endpoint ID
19
- ///
20
- /// # Returns
21
- /// * `amount_sent_ld` - The amount sent in local decimals
22
- /// * `amount_received_ld` - The amount received in local decimals on the remote
23
- pub fn debit<T: OFTCore>(
24
- env: &Env,
25
- token: &Address,
26
- sender: &Address,
27
- amount_ld: i128,
28
- min_amount_ld: i128,
29
- dst_eid: u32,
30
- ) -> (i128, i128) {
31
- let (amount_sent_ld, amount_received_ld) = T::__debit_view(env, amount_ld, min_amount_ld, dst_eid);
32
- TokenClient::new(env, token).transfer(sender, env.current_contract_address(), &amount_received_ld);
33
- (amount_sent_ld, amount_received_ld)
34
- }
35
-
36
- /// Credit tokens using LockUnlock OFT type (unlocks tokens from contract).
37
- ///
38
- /// # Parameters
39
- /// * `env` - The Soroban environment
40
- /// * `token` - Address of the token
41
- /// * `to` - Address of the token recipient
42
- /// * `amount_ld` - Amount to credit in local decimals
43
- /// * `_src_eid` - Source endpoint ID (unused)
44
- ///
45
- /// # Returns
46
- /// The amount credited
47
- pub fn credit<T: OFTCore>(env: &Env, token: &Address, to: &Address, amount_ld: i128, _src_eid: u32) -> i128 {
48
- TokenClient::new(env, token).transfer(&env.current_contract_address(), to, &amount_ld);
49
- amount_ld
50
- }
@@ -1,50 +0,0 @@
1
- //! MintBurn type implementation for OFT.
2
- //!
3
- //! This OFT type burns tokens on debit (send) and mints tokens on credit (receive).
4
- //! Used when the OFT contract has mint authority over the token.
5
-
6
- use crate::interfaces::MintableClient;
7
- use oft_core::OFTCore;
8
- use soroban_sdk::{token::TokenClient, Address, Env};
9
-
10
- /// Debit tokens using MintBurn OFT type (burns tokens from sender).
11
- ///
12
- /// # Parameters
13
- /// * `env` - The Soroban environment
14
- /// * `token` - Address of the token (SAC) to burn from
15
- /// * `sender` - Address of the token sender
16
- /// * `amount_ld` - Amount to debit in local decimals
17
- /// * `min_amount_ld` - Minimum amount that must be received (for slippage protection)
18
- /// * `dst_eid` - Destination endpoint ID
19
- ///
20
- /// # Returns
21
- /// * `amount_sent_ld` - The amount sent in local decimals
22
- /// * `amount_received_ld` - The amount received in local decimals on the remote
23
- pub fn debit<T: OFTCore>(
24
- env: &Env,
25
- token: &Address,
26
- sender: &Address,
27
- amount_ld: i128,
28
- min_amount_ld: i128,
29
- dst_eid: u32,
30
- ) -> (i128, i128) {
31
- let (amount_sent_ld, amount_received_ld) = T::__debit_view(env, amount_ld, min_amount_ld, dst_eid);
32
- TokenClient::new(env, token).burn(sender, &amount_received_ld);
33
- (amount_sent_ld, amount_received_ld)
34
- }
35
-
36
- /// Credit tokens using MintBurn OFT type (mints tokens to recipient).
37
- ///
38
- /// # Parameters
39
- /// * `env` - The Soroban environment
40
- /// * `mintable` - Address of the contract responsible for minting tokens (e.g. SAC wrapper)
41
- /// * `to` - Address of the token recipient
42
- /// * `amount_ld` - Amount to credit in local decimals
43
- /// * `_src_eid` - Source endpoint ID (unused)
44
- ///
45
- /// # Returns
46
- /// The amount credited
47
- pub fn credit<T: OFTCore>(env: &Env, mintable: &Address, to: &Address, amount_ld: i128, _src_eid: u32) -> i128 {
48
- MintableClient::new(env, mintable).mint(to, &amount_ld, &env.current_contract_address());
49
- amount_ld
50
- }
@@ -1,24 +0,0 @@
1
- //! OFT mode implementations.
2
- //!
3
- //! This module provides reference implementations for the two main OFT types:
4
- //!
5
- //! - **LockUnlock**: Locks tokens on send, unlocks on receive. Operates directly on the
6
- //! token via standard SEP-41 `transfer`.
7
- //! - **MintBurn**: Burns tokens on send (via TokenClient on the token), mints on receive
8
- //! via a contract that implements [`Mintable`](crate::interfaces::Mintable).
9
-
10
- use soroban_sdk::{contracttype, Address};
11
-
12
- pub mod lock_unlock;
13
- pub mod mint_burn;
14
-
15
- /// The OFT operation type.
16
- #[contracttype]
17
- #[derive(Clone, Debug, Eq, PartialEq)]
18
- pub enum OftType {
19
- /// Lock tokens on send, unlock on receive.
20
- LockUnlock,
21
- /// Burn tokens on send, mint on receive.
22
- /// The address is the Mintable contract used for minting on credit
23
- MintBurn(Address),
24
- }
@@ -1,3 +0,0 @@
1
- mod oft_fee;
2
- mod pausable;
3
- mod rate_limiter;
@@ -1,255 +0,0 @@
1
- extern crate std;
2
-
3
- use crate::extensions::oft_fee::FEE_CONFIG_MANAGER_ROLE;
4
- use crate::extensions::oft_fee::{OFTFee, OFTFeeError, OFTFeeInternal};
5
- use soroban_sdk::{
6
- contract, contractimpl,
7
- testutils::Address as _,
8
- token::{StellarAssetClient, TokenClient},
9
- Address, Env, Symbol,
10
- };
11
- use utils::auth::Auth;
12
- use utils::rbac::{grant_role_no_auth, RoleBasedAccessControl};
13
-
14
- // ============================================================================
15
- // Test Contract
16
- // ============================================================================
17
-
18
- #[contract]
19
- struct FeeTestContract;
20
-
21
- impl Auth for FeeTestContract {
22
- fn authorizer(env: &Env) -> Option<Address> {
23
- Some(env.current_contract_address())
24
- }
25
- }
26
-
27
- impl OFTFeeInternal for FeeTestContract {}
28
-
29
- #[contractimpl(contracttrait)]
30
- impl OFTFee for FeeTestContract {}
31
-
32
- #[contractimpl(contracttrait)]
33
- impl RoleBasedAccessControl for FeeTestContract {}
34
-
35
- #[contractimpl]
36
- impl FeeTestContract {
37
- pub fn init_roles(env: Env) {
38
- let contract_id = env.current_contract_address();
39
- grant_role_no_auth(&env, &contract_id, &Symbol::new(&env, FEE_CONFIG_MANAGER_ROLE), &contract_id);
40
- }
41
-
42
- pub fn charge_fee(env: Env, token: Address, from: Address, fee_amount: i128) {
43
- <Self as OFTFeeInternal>::__charge_fee(&env, &token, &from, fee_amount);
44
- }
45
- }
46
-
47
- // ============================================================================
48
- // Test Setup
49
- // ============================================================================
50
-
51
- struct TestSetup {
52
- env: Env,
53
- client: FeeTestContractClient<'static>,
54
- contract_id: Address,
55
- token: Address,
56
- from: Address,
57
- fee_deposit: Address,
58
- }
59
-
60
- fn setup() -> TestSetup {
61
- let env = Env::default();
62
- env.mock_all_auths_allowing_non_root_auth();
63
-
64
- let contract_id = env.register(FeeTestContract, ());
65
- let client = FeeTestContractClient::new(&env, &contract_id);
66
- client.init_roles();
67
-
68
- let token_admin = Address::generate(&env);
69
- let sac = env.register_stellar_asset_contract_v2(token_admin.clone());
70
- let token = sac.address();
71
-
72
- let from = Address::generate(&env);
73
- let fee_deposit = Address::generate(&env);
74
-
75
- StellarAssetClient::new(&env, &token).mint(&from, &1_000_000i128);
76
-
77
- TestSetup { env, client, contract_id, token, from, fee_deposit }
78
- }
79
-
80
- fn id(v: u32) -> u128 {
81
- v as u128
82
- }
83
-
84
- // ============================================================================
85
- // Set Default Fee BPS Tests
86
- // ============================================================================
87
-
88
- #[test]
89
- fn test_set_default_fee_bps_rejects_invalid_value() {
90
- let TestSetup { client, contract_id, .. } = setup();
91
-
92
- let res = client.try_set_default_fee_bps(&10_001u32, &contract_id);
93
- assert_eq!(res.err().unwrap().ok().unwrap(), OFTFeeError::InvalidBps.into());
94
- }
95
-
96
- // ============================================================================
97
- // Set Fee BPS Tests
98
- // ============================================================================
99
-
100
- #[test]
101
- fn test_set_fee_bps_rejects_invalid_value() {
102
- let TestSetup { client, contract_id, .. } = setup();
103
- let id_101 = id(101);
104
-
105
- let res = client.try_set_fee_bps(&id_101, &Some(10_001u32), &contract_id);
106
- assert_eq!(res.err().unwrap().ok().unwrap(), OFTFeeError::InvalidBps.into());
107
- }
108
-
109
- #[test]
110
- fn test_set_fee_bps_set_and_remove() {
111
- let TestSetup { client, contract_id, .. } = setup();
112
- let id_101 = id(101);
113
-
114
- client.set_fee_bps(&id_101, &Some(200u32), &contract_id);
115
- assert_eq!(client.fee_bps(&id_101), Some(200));
116
- assert_eq!(client.get_fee(&id_101, &10_000i128), 200);
117
-
118
- client.set_default_fee_bps(&111u32, &contract_id);
119
- client.set_fee_bps(&id_101, &None, &contract_id);
120
- assert_eq!(client.fee_bps(&id_101), None);
121
- assert_eq!(client.default_fee_bps(), 111);
122
- assert_eq!(client.get_fee(&id_101, &10_000i128), 111);
123
-
124
- // Per-ID Some(0) explicitly overrides default to zero fee
125
- client.set_fee_bps(&id_101, &Some(0u32), &contract_id);
126
- assert_eq!(client.fee_bps(&id_101), Some(0));
127
- assert_eq!(client.get_fee(&id_101, &10_000i128), 0);
128
- assert_eq!(client.get_fee(&id(99), &10_000i128), 111, "other IDs still use default");
129
-
130
- // set_default_fee_bps(0) removes the default fee
131
- client.set_default_fee_bps(&0u32, &contract_id);
132
- assert_eq!(client.default_fee_bps(), 0);
133
- assert_eq!(client.get_fee(&id(99), &10_000i128), 0);
134
- }
135
-
136
- // ============================================================================
137
- // Set Fee Deposit Tests
138
- // ============================================================================
139
-
140
- #[test]
141
- fn test_set_fee_deposit_same_value() {
142
- let TestSetup { client, contract_id, fee_deposit, .. } = setup();
143
-
144
- client.set_fee_deposit(&fee_deposit, &contract_id);
145
- let res = client.try_set_fee_deposit(&fee_deposit, &contract_id);
146
- assert_eq!(res.err().unwrap().ok().unwrap(), OFTFeeError::SameValue.into());
147
- }
148
-
149
- // ============================================================================
150
- // Charge Fee Tests
151
- // ============================================================================
152
-
153
- #[test]
154
- fn test_charge_fee_zero_amount_no_transfer() {
155
- let TestSetup { env, client, contract_id, token, from, fee_deposit, .. } = setup();
156
-
157
- client.set_fee_deposit(&fee_deposit, &contract_id);
158
-
159
- let token_client = TokenClient::new(&env, &token);
160
- let from_before = token_client.balance(&from);
161
- let dep_before = token_client.balance(&fee_deposit);
162
-
163
- client.charge_fee(&token, &from, &0i128);
164
-
165
- assert_eq!(token_client.balance(&from), from_before);
166
- assert_eq!(token_client.balance(&fee_deposit), dep_before);
167
- }
168
-
169
- #[test]
170
- fn test_charge_fee_transfers() {
171
- let TestSetup { env, client, contract_id, token, from, fee_deposit, .. } = setup();
172
-
173
- client.set_fee_deposit(&fee_deposit, &contract_id);
174
-
175
- let token_client = TokenClient::new(&env, &token);
176
- let from_before = token_client.balance(&from);
177
- let dep_before = token_client.balance(&fee_deposit);
178
-
179
- client.charge_fee(&token, &from, &123i128);
180
-
181
- assert_eq!(token_client.balance(&from), from_before - 123);
182
- assert_eq!(token_client.balance(&fee_deposit), dep_before + 123);
183
- }
184
-
185
- // ============================================================================
186
- // Fee View Tests
187
- // ============================================================================
188
-
189
- #[test]
190
- fn test_fee_view_no_fee_returns_zero() {
191
- let TestSetup { client, .. } = setup();
192
-
193
- assert_eq!(client.get_fee(&id(1), &10_000i128), 0);
194
- }
195
-
196
- #[test]
197
- fn test_fee_view_computes_correct_fee() {
198
- let TestSetup { client, contract_id, fee_deposit, .. } = setup();
199
-
200
- client.set_fee_deposit(&fee_deposit, &contract_id);
201
- client.set_default_fee_bps(&250u32, &contract_id);
202
-
203
- assert_eq!(client.get_fee(&id(1), &10_000i128), 250);
204
- }
205
-
206
- // ============================================================================
207
- // Get Amount Before Fee Tests
208
- // ============================================================================
209
-
210
- #[test]
211
- fn test_get_amount_before_fee_no_fee() {
212
- let TestSetup { client, .. } = setup();
213
-
214
- let result = client.get_amount_before_fee(&id(1), &10_000i128);
215
- assert_eq!(result, 10_000);
216
- }
217
-
218
- #[test]
219
- fn test_get_amount_before_fee_roundtrip() {
220
- let TestSetup { client, contract_id, .. } = setup();
221
-
222
- client.set_default_fee_bps(&250u32, &contract_id); // 2.5%
223
-
224
- let original = 100_000i128;
225
- let fee = client.get_fee(&id(1), &original);
226
- let after_fee = original - fee;
227
- let recovered = client.get_amount_before_fee(&id(1), &after_fee);
228
- assert_eq!(recovered, original);
229
- }
230
-
231
- #[test]
232
- fn test_get_amount_before_fee_full_fee_returns_zero() {
233
- let TestSetup { client, contract_id, .. } = setup();
234
-
235
- client.set_default_fee_bps(&10_000u32, &contract_id); // 100%
236
-
237
- let result = client.get_amount_before_fee(&id(1), &5_000i128);
238
- assert_eq!(result, 0);
239
- }
240
-
241
- #[test]
242
- fn test_get_amount_before_fee_per_destination_override() {
243
- let TestSetup { client, contract_id, .. } = setup();
244
-
245
- client.set_default_fee_bps(&100u32, &contract_id); // 1% default
246
- client.set_fee_bps(&id(42), &Some(500u32), &contract_id); // 5% for id 42
247
-
248
- // 5% fee: after = 9_500 → before = 9_500 * 10_000 / 9_500 = 10_000
249
- let result = client.get_amount_before_fee(&id(42), &9_500i128);
250
- assert_eq!(result, 10_000);
251
-
252
- // Other destinations still use default 1%
253
- let result_default = client.get_amount_before_fee(&id(99), &9_900i128);
254
- assert_eq!(result_default, 10_000);
255
- }