From 9205bff72364c67c496142a01179662f7c5c09b3 Mon Sep 17 00:00:00 2001 From: Ozgun Ozerk Date: Tue, 28 Jul 2026 11:38:41 +0300 Subject: [PATCH] make fungible total supply tracking optional Tracking the total supply is not required by SEP-41 and has a scalability cost: every mint and burn writes a shared storage entry, serializing otherwise independent transactions under parallel execution. The base FungibleToken trait no longer tracks or exposes it. The opt-in total_supply extension provides the FungibleTotalSupply trait and a supply-aware TotalSupply contract type, storing the supply in its own persistent entry so mints and burns only conflict with each other, never with plain transfers. Compose resolves the curated combinations with the allowlist/blocklist policies; the combined contract types stay internal. RWA, Vault and FungibleVotes are inherently supply-aware (votes serves the query from its checkpoints, removing the previous double-tracking). Closes #33 Co-Authored-By: Claude Fable 5 --- examples/fungible-allowlist/src/contract.rs | 15 +- examples/fungible-capped/src/contract.rs | 12 +- examples/fungible-pausable/src/contract.rs | 19 +- examples/fungible-vault/src/contract.rs | 5 +- examples/fungible-votes/src/contract.rs | 7 +- examples/rwa/token/src/contract.rs | 5 +- .../src/fungible/extensions/allowlist/mod.rs | 10 +- .../src/fungible/extensions/blocklist/mod.rs | 10 +- .../src/fungible/extensions/burnable/mod.rs | 10 +- .../fungible/extensions/burnable/storage.rs | 4 +- .../src/fungible/extensions/burnable/test.rs | 5 - .../src/fungible/extensions/capped/mod.rs | 7 +- .../src/fungible/extensions/capped/test.rs | 35 ++-- .../fungible/extensions/combinations/mod.rs | 67 ++++-- .../extensions/combinations/storage.rs | 152 ++++++++++++++ .../fungible/extensions/combinations/test.rs | 102 +++++++++ .../tokens/src/fungible/extensions/mod.rs | 1 + .../fungible/extensions/total_supply/mod.rs | 90 ++++++++ .../extensions/total_supply/storage.rs | 198 ++++++++++++++++++ .../fungible/extensions/total_supply/test.rs | 155 ++++++++++++++ .../src/fungible/extensions/votes/storage.rs | 19 +- .../src/fungible/extensions/votes/test.rs | 8 +- packages/tokens/src/fungible/mod.rs | 57 ++--- packages/tokens/src/fungible/overrides.rs | 30 ++- packages/tokens/src/fungible/storage.rs | 50 ++--- packages/tokens/src/fungible/test.rs | 4 - packages/tokens/src/rwa/mod.rs | 5 + packages/tokens/src/rwa/storage.rs | 12 +- packages/tokens/src/rwa/test.rs | 2 +- packages/tokens/src/vault/mod.rs | 4 + packages/tokens/src/vault/storage.rs | 11 +- packages/tokens/src/vault/test.rs | 12 +- 32 files changed, 977 insertions(+), 146 deletions(-) create mode 100644 packages/tokens/src/fungible/extensions/combinations/storage.rs create mode 100644 packages/tokens/src/fungible/extensions/combinations/test.rs create mode 100644 packages/tokens/src/fungible/extensions/total_supply/mod.rs create mode 100644 packages/tokens/src/fungible/extensions/total_supply/storage.rs create mode 100644 packages/tokens/src/fungible/extensions/total_supply/test.rs diff --git a/examples/fungible-allowlist/src/contract.rs b/examples/fungible-allowlist/src/contract.rs index 932bd15de..f46ecba24 100644 --- a/examples/fungible-allowlist/src/contract.rs +++ b/examples/fungible-allowlist/src/contract.rs @@ -4,6 +4,10 @@ //! SEP-41-compliant fungible token. It includes essential features such as //! controlled token transfers by an admin who can allow or disallow specific //! accounts. +//! +//! It also demonstrates composing the contract type: `Compose<(AllowList, +//! TotalSupply)>` pairs the allowlist transfer policy with total supply +//! tracking. use soroban_sdk::{ contract, contractimpl, symbol_short, Address, Env, MuxedAddress, String, Symbol, Vec, @@ -13,7 +17,9 @@ use stellar_macros::only_role; use stellar_tokens::fungible::{ allowlist::{AllowList, FungibleAllowList}, burnable::FungibleBurnable, - Base, Compose, FungibleToken, + combinations::Compose, + total_supply::{self, FungibleTotalSupply, TotalSupply}, + Base, FungibleToken, }; #[contract] @@ -40,15 +46,18 @@ impl ExampleContract { AllowList::allow_user(e, &admin); // Mint initial supply to the admin - Base::mint(e, &admin, initial_supply); + total_supply::mint(e, &admin, initial_supply); } } #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = Compose<(AllowList,)>; + type ContractType = Compose<(AllowList, TotalSupply)>; } +#[contractimpl(contracttrait)] +impl FungibleTotalSupply for ExampleContract {} + #[contractimpl(contracttrait)] impl FungibleAllowList for ExampleContract { #[only_role(operator, "manager")] diff --git a/examples/fungible-capped/src/contract.rs b/examples/fungible-capped/src/contract.rs index 7392341f0..38445c44c 100644 --- a/examples/fungible-capped/src/contract.rs +++ b/examples/fungible-capped/src/contract.rs @@ -10,7 +10,8 @@ use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use stellar_tokens::fungible::{ capped::{check_cap, set_cap}, - Base, Compose, FungibleToken, + total_supply::{self, FungibleTotalSupply, TotalSupply}, + Compose, FungibleToken, }; #[contract] @@ -23,12 +24,15 @@ impl ExampleContract { } pub fn mint(e: &Env, to: Address, amount: i128) { - check_cap(e, amount, Base::total_supply(e)); - Base::mint(e, &to, amount); + check_cap(e, amount, total_supply::total_supply(e)); + total_supply::mint(e, &to, amount); } } #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = Compose<(Base,)>; + type ContractType = Compose<(TotalSupply,)>; } + +#[contractimpl(contracttrait)] +impl FungibleTotalSupply for ExampleContract {} diff --git a/examples/fungible-pausable/src/contract.rs b/examples/fungible-pausable/src/contract.rs index 823163f95..a122c75a0 100644 --- a/examples/fungible-pausable/src/contract.rs +++ b/examples/fungible-pausable/src/contract.rs @@ -15,7 +15,11 @@ use soroban_sdk::{ }; use stellar_contract_utils::pausable::{self as pausable, Pausable}; use stellar_macros::when_not_paused; -use stellar_tokens::fungible::{burnable::FungibleBurnable, Base, Compose, FungibleToken}; +use stellar_tokens::fungible::{ + burnable::FungibleBurnable, + total_supply::{self, FungibleTotalSupply, TotalSupply}, + Base, Compose, ContractOverrides, FungibleToken, +}; pub const OWNER: Symbol = symbol_short!("OWNER"); @@ -39,7 +43,7 @@ impl ExampleContract { initial_supply: i128, ) { Base::set_metadata(e, 18, name, symbol); - Base::mint(e, &owner, initial_supply); + total_supply::mint(e, &owner, initial_supply); e.storage().instance().set(&OWNER, &owner); } @@ -51,7 +55,7 @@ impl ExampleContract { let owner: Address = e.storage().instance().get(&OWNER).expect("owner should be set"); owner.require_auth(); - Base::mint(e, &to, amount); + total_supply::mint(e, &to, amount); } } @@ -90,11 +94,7 @@ impl Pausable for ExampleContract { #[contractimpl] impl FungibleToken for ExampleContract { - type ContractType = Compose<(Base,)>; - - fn total_supply(e: &Env) -> i128 { - Self::ContractType::total_supply(e) - } + type ContractType = Compose<(TotalSupply,)>; fn balance(e: &Env, account: Address) -> i128 { Self::ContractType::balance(e, &account) @@ -131,6 +131,9 @@ impl FungibleToken for ExampleContract { } } +#[contractimpl(contracttrait)] +impl FungibleTotalSupply for ExampleContract {} + #[contractimpl] impl FungibleBurnable for ExampleContract { #[when_not_paused] diff --git a/examples/fungible-vault/src/contract.rs b/examples/fungible-vault/src/contract.rs index de1cdd8e8..3ea9f0eae 100644 --- a/examples/fungible-vault/src/contract.rs +++ b/examples/fungible-vault/src/contract.rs @@ -2,7 +2,7 @@ use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use stellar_tokens::{ - fungible::{Base, Compose, FungibleToken}, + fungible::{total_supply::FungibleTotalSupply, Base, Compose, FungibleToken}, vault::{FungibleVault, Vault}, }; @@ -38,5 +38,8 @@ impl FungibleToken for ExampleContract { } } +#[contractimpl(contracttrait)] +impl FungibleTotalSupply for ExampleContract {} + #[contractimpl(contracttrait)] impl FungibleVault for ExampleContract {} diff --git a/examples/fungible-votes/src/contract.rs b/examples/fungible-votes/src/contract.rs index 1e1d29a83..a06eb1e4e 100644 --- a/examples/fungible-votes/src/contract.rs +++ b/examples/fungible-votes/src/contract.rs @@ -3,7 +3,8 @@ use stellar_access::ownable::{set_owner, Ownable}; use stellar_governance::votes::Votes; use stellar_macros::only_owner; use stellar_tokens::fungible::{ - burnable::FungibleBurnable, votes::FungibleVotes, Base, Compose, FungibleToken, + burnable::FungibleBurnable, total_supply::FungibleTotalSupply, votes::FungibleVotes, Base, + Compose, FungibleToken, }; #[contract] @@ -27,6 +28,10 @@ impl FungibleToken for ExampleContract { type ContractType = Compose<(FungibleVotes,)>; } +// The total supply is served from the voting checkpoints. +#[contractimpl(contracttrait)] +impl FungibleTotalSupply for ExampleContract {} + #[contractimpl(contracttrait)] impl Votes for ExampleContract {} diff --git a/examples/rwa/token/src/contract.rs b/examples/rwa/token/src/contract.rs index d29e4e180..d42ee4c0d 100644 --- a/examples/rwa/token/src/contract.rs +++ b/examples/rwa/token/src/contract.rs @@ -7,7 +7,7 @@ use stellar_access::access_control::{self as access_control, AccessControl}; use stellar_contract_utils::pausable::{self as pausable, Pausable}; use stellar_macros::{only_admin, only_role}; use stellar_tokens::{ - fungible::{Base, Compose, FungibleToken}, + fungible::{total_supply::FungibleTotalSupply, Base, Compose, FungibleToken}, rwa::{RWAToken, RWA}, }; @@ -57,6 +57,9 @@ impl FungibleToken for RWATokenContract { type ContractType = Compose<(RWA,)>; } +#[contractimpl(contracttrait)] +impl FungibleTotalSupply for RWATokenContract {} + #[contractimpl(contracttrait)] impl RWAToken for RWATokenContract { #[only_role(operator, "manager")] diff --git a/packages/tokens/src/fungible/extensions/allowlist/mod.rs b/packages/tokens/src/fungible/extensions/allowlist/mod.rs index 2869cf379..b2eadfa55 100644 --- a/packages/tokens/src/fungible/extensions/allowlist/mod.rs +++ b/packages/tokens/src/fungible/extensions/allowlist/mod.rs @@ -8,6 +8,14 @@ pub use storage::AllowList; use crate::fungible::FungibleToken; +/// Marker trait for contract types that enforce the allowlist transfer +/// policy: [`AllowList`] itself, or a combination resolved by +/// [`crate::fungible::combinations::Compose`] that includes it, e.g. +/// `Compose<(AllowList, TotalSupply)>`. +pub trait AllowListContractType {} + +impl AllowListContractType for AllowList {} + /// AllowList Trait for Fungible Token /// /// The `FungibleAllowList` trait extends the `FungibleToken` trait to @@ -30,7 +38,7 @@ use crate::fungible::FungibleToken; /// "storage.rs", because the authorizations are to be handled in the access /// control helpers or directly implemented #[contracttrait] -pub trait FungibleAllowList: FungibleToken { +pub trait FungibleAllowList: FungibleToken { /// Returns the allowed status of an account. /// /// # Arguments diff --git a/packages/tokens/src/fungible/extensions/blocklist/mod.rs b/packages/tokens/src/fungible/extensions/blocklist/mod.rs index 54ef8380d..d235db2a9 100644 --- a/packages/tokens/src/fungible/extensions/blocklist/mod.rs +++ b/packages/tokens/src/fungible/extensions/blocklist/mod.rs @@ -8,6 +8,14 @@ pub use storage::BlockList; use crate::fungible::FungibleToken; +/// Marker trait for contract types that enforce the blocklist transfer +/// policy: [`BlockList`] itself, or a combination resolved by +/// [`crate::fungible::combinations::Compose`] that includes it, e.g. +/// `Compose<(BlockList, TotalSupply)>`. +pub trait BlockListContractType {} + +impl BlockListContractType for BlockList {} + /// BlockList Trait for Fungible Token /// /// The `FungibleBlockList` trait extends the `FungibleToken` trait to @@ -30,7 +38,7 @@ use crate::fungible::FungibleToken; /// "storage.rs", because the authorizations are to be handled in the access /// control helpers or directly implemented. #[contracttrait] -pub trait FungibleBlockList: FungibleToken { +pub trait FungibleBlockList: FungibleToken { /// Returns the blocked status of an account. /// /// # Arguments diff --git a/packages/tokens/src/fungible/extensions/burnable/mod.rs b/packages/tokens/src/fungible/extensions/burnable/mod.rs index 0b5e2f07a..3cf007a5a 100644 --- a/packages/tokens/src/fungible/extensions/burnable/mod.rs +++ b/packages/tokens/src/fungible/extensions/burnable/mod.rs @@ -40,8 +40,9 @@ use crate::fungible::{overrides::BurnableOverrides, FungibleToken}; /// background. #[contracttrait] pub trait FungibleBurnable: FungibleToken { - /// Destroys `amount` of tokens from `from`. Updates the total - /// supply accordingly. + /// Destroys `amount` of tokens from `from`. For supply-aware contract + /// types (e.g. [`crate::fungible::total_supply::TotalSupply`]), the total + /// supply is updated accordingly. /// /// # Arguments /// @@ -64,8 +65,9 @@ pub trait FungibleBurnable: FungibleToken { Self::ContractType::burn(e, &from, amount); } - /// Destroys `amount` of tokens from `from`. Updates the total - /// supply accordingly. + /// Destroys `amount` of tokens from `from`. For supply-aware contract + /// types (e.g. [`crate::fungible::total_supply::TotalSupply`]), the total + /// supply is updated accordingly. /// /// # Arguments /// diff --git a/packages/tokens/src/fungible/extensions/burnable/storage.rs b/packages/tokens/src/fungible/extensions/burnable/storage.rs index 88cf85731..8a5f6dcc6 100644 --- a/packages/tokens/src/fungible/extensions/burnable/storage.rs +++ b/packages/tokens/src/fungible/extensions/burnable/storage.rs @@ -3,8 +3,7 @@ use soroban_sdk::{Address, Env}; use crate::fungible::{extensions::burnable::emit_burn, Base}; impl Base { - /// Destroys `amount` of tokens from `from`. Updates the total - /// supply accordingly. + /// Destroys `amount` of tokens from `from`. /// /// # Arguments /// @@ -32,7 +31,6 @@ impl Base { /// Destroys `amount` of tokens from `from` using the allowance mechanism. /// `amount` is then deducted from `spender` allowance. - /// Updates the total supply accordingly. /// /// # Arguments /// diff --git a/packages/tokens/src/fungible/extensions/burnable/test.rs b/packages/tokens/src/fungible/extensions/burnable/test.rs index c7241e742..22185d427 100644 --- a/packages/tokens/src/fungible/extensions/burnable/test.rs +++ b/packages/tokens/src/fungible/extensions/burnable/test.rs @@ -18,7 +18,6 @@ fn burn_works() { Base::mint(&e, &account, 100); Base::burn(&e, &account, 50); assert_eq!(Base::balance(&e, &account), 50); - assert_eq!(Base::total_supply(&e), 50); let mut event_assert = EventAssertion::new(&e, address.clone()); event_assert.assert_event_count(2); @@ -40,7 +39,6 @@ fn burn_with_allowance_works() { Base::burn_from(&e, &spender, &owner, 30); assert_eq!(Base::balance(&e, &owner), 70); assert_eq!(Base::balance(&e, &spender), 0); - assert_eq!(Base::total_supply(&e), 70); let mut event_assert = EventAssertion::new(&e, address.clone()); event_assert.assert_event_count(3); @@ -60,7 +58,6 @@ fn burn_with_insufficient_balance_panics() { e.as_contract(&address, || { Base::mint(&e, &account, 100); assert_eq!(Base::balance(&e, &account), 100); - assert_eq!(Base::total_supply(&e), 100); Base::burn(&e, &account, 101); }); } @@ -76,7 +73,6 @@ fn burn_with_no_allowance_panics() { e.as_contract(&address, || { Base::mint(&e, &owner, 100); assert_eq!(Base::balance(&e, &owner), 100); - assert_eq!(Base::total_supply(&e), 100); Base::burn_from(&e, &spender, &owner, 50); }); } @@ -94,7 +90,6 @@ fn burn_with_insufficient_allowance_panics() { Base::approve(&e, &owner, &spender, 50, 100); assert_eq!(Base::allowance(&e, &owner, &spender), 50); assert_eq!(Base::balance(&e, &owner), 100); - assert_eq!(Base::total_supply(&e), 100); Base::burn_from(&e, &spender, &owner, 60); }); } diff --git a/packages/tokens/src/fungible/extensions/capped/mod.rs b/packages/tokens/src/fungible/extensions/capped/mod.rs index f8edc9ea0..732e59c44 100644 --- a/packages/tokens/src/fungible/extensions/capped/mod.rs +++ b/packages/tokens/src/fungible/extensions/capped/mod.rs @@ -4,12 +4,17 @@ /// Instead, the `capped` extension modifies the business logic of the `mint` /// function to enforce a supply cap. /// +/// The cap is checked against the tracked total supply, so this extension is +/// meant to be used together with +/// [`crate::fungible::total_supply::FungibleTotalSupply`] (or another +/// supply-aware contract type). +/// /// This module provides the following helper functions: /// - `set_cap`: Sets the maximum token supply. /// - `query_cap`: Returns the maximum token supply. /// - `check_cap`: Panics if minting a specified `amount` added to the given /// `total_supply` would exceed the cap. Should be used before calling -/// `mint()`. +/// [`crate::fungible::total_supply::mint`]. mod storage; pub use self::storage::{check_cap, query_cap, set_cap, CapStorageKey}; #[cfg(test)] diff --git a/packages/tokens/src/fungible/extensions/capped/test.rs b/packages/tokens/src/fungible/extensions/capped/test.rs index 6cc50234f..ae0348015 100644 --- a/packages/tokens/src/fungible/extensions/capped/test.rs +++ b/packages/tokens/src/fungible/extensions/capped/test.rs @@ -3,7 +3,10 @@ extern crate std; use soroban_sdk::{contract, testutils::Address as _, Address, Env}; use crate::fungible::{ - extensions::capped::{check_cap, query_cap, set_cap}, + extensions::{ + capped::{check_cap, query_cap, set_cap}, + total_supply::{mint, total_supply}, + }, Base, }; @@ -19,11 +22,11 @@ fn test_mint_under_cap() { e.as_contract(&contract_address, || { set_cap(&e, 1000); - check_cap(&e, 500, Base::total_supply(&e)); - Base::mint(&e, &user, 500); + check_cap(&e, 500, total_supply(&e)); + mint(&e, &user, 500); assert_eq!(Base::balance(&e, &user), 500); - assert_eq!(Base::total_supply(&e), 500); + assert_eq!(total_supply(&e), 500); }); } @@ -36,11 +39,11 @@ fn test_mint_exact_cap() { e.as_contract(&contract_address, || { set_cap(&e, 1000); - check_cap(&e, 1000, Base::total_supply(&e)); - Base::mint(&e, &user, 1000); + check_cap(&e, 1000, total_supply(&e)); + mint(&e, &user, 1000); assert_eq!(Base::balance(&e, &user), 1000); - assert_eq!(Base::total_supply(&e), 1000); + assert_eq!(total_supply(&e), 1000); }); } @@ -54,8 +57,8 @@ fn test_mint_exceeds_cap() { e.as_contract(&contract_address, || { set_cap(&e, 1000); - check_cap(&e, 1001, Base::total_supply(&e)); - Base::mint(&e, &user, 1001); // This should panic + check_cap(&e, 1001, total_supply(&e)); + mint(&e, &user, 1001); // This should panic }); } @@ -70,15 +73,15 @@ fn test_mint_multiple_exceeds_cap() { set_cap(&e, 1000); // Mint 600 tokens first - check_cap(&e, 600, Base::total_supply(&e)); - Base::mint(&e, &user, 600); + check_cap(&e, 600, total_supply(&e)); + mint(&e, &user, 600); assert_eq!(Base::balance(&e, &user), 600); - assert_eq!(Base::total_supply(&e), 600); + assert_eq!(total_supply(&e), 600); // Attempt to mint 500 more tokens (would exceed cap) - check_cap(&e, 500, Base::total_supply(&e)); - Base::mint(&e, &user, 500); // This should panic + check_cap(&e, 500, total_supply(&e)); + mint(&e, &user, 500); // This should panic }); } @@ -91,9 +94,9 @@ fn test_check_cap_overflows() { e.as_contract(&contract_address, || { set_cap(&e, i128::MAX); - Base::mint(&e, &user, i128::MAX); + mint(&e, &user, i128::MAX); - check_cap(&e, 1, Base::total_supply(&e)); // should overflow + check_cap(&e, 1, total_supply(&e)); // should overflow }); } diff --git a/packages/tokens/src/fungible/extensions/combinations/mod.rs b/packages/tokens/src/fungible/extensions/combinations/mod.rs index 4515c2357..2fd2c1d20 100644 --- a/packages/tokens/src/fungible/extensions/combinations/mod.rs +++ b/packages/tokens/src/fungible/extensions/combinations/mod.rs @@ -4,8 +4,10 @@ //! [`crate::fungible::FungibleToken`] implementation, and that single slot //! decides all overridable behavior. The contract type is selected uniformly //! with [`Compose`], by listing the contract types that override the `Base` -//! behavior. [`Compose`] resolves the list to the curated contract type at -//! compile time. +//! behavior: a single one, or several whose overrides have to apply at the +//! same time. [`Compose`] resolves the list to the curated contract type at +//! compile time, so no dedicated combination type has to be named by the +//! contract author. //! //! Note that [`Compose`] only selects the `ContractType`; it says nothing //! about extensions that add new functionality without overriding the base @@ -18,25 +20,38 @@ //! ```ignore //! #[contractimpl(contracttrait)] //! impl FungibleToken for MyToken { -//! type ContractType = Compose<(AllowList,)>; +//! type ContractType = Compose<(AllowList, TotalSupply)>; //! } //! //! #[contractimpl(contracttrait)] +//! impl FungibleTotalSupply for MyToken {} +//! +//! #[contractimpl(contracttrait)] //! impl FungibleAllowList for MyToken { //! // ... //! } //! ``` //! -//! No multi-type combinations are curated for fungible tokens yet; every -//! valid list currently holds a single contract type. Invalid lists do not -//! compile: `AllowList` and `BlockList` are mutually exclusive, and -//! implementing an extension trait the list does not back (e.g. -//! [`crate::fungible::allowlist::FungibleAllowList`] without `AllowList` in -//! the list) is rejected by that trait's bound. +//! The list is order-insensitive: `Compose<(TotalSupply, AllowList)>` and +//! `Compose<(AllowList, TotalSupply)>` resolve to the same contract type. +//! Invalid lists do not compile: `AllowList` and `BlockList` are mutually +//! exclusive, and implementing an extension trait the list does not back +//! (e.g. [`crate::fungible::total_supply::FungibleTotalSupply`] without +//! `TotalSupply` in the list) is rejected by that trait's bound. + +mod storage; + +#[cfg(test)] +mod test; + +use storage::{TotalSupplyAllowList, TotalSupplyBlockList}; use crate::{ fungible::{ - extensions::{allowlist::AllowList, blocklist::BlockList, votes::FungibleVotes}, + extensions::{ + allowlist::AllowList, blocklist::BlockList, total_supply::TotalSupply, + votes::FungibleVotes, + }, Base, ContractOverrides, }, rwa::RWA, @@ -44,7 +59,7 @@ use crate::{ }; /// Resolves a list of contract types to the combined contract type, e.g. -/// `Compose<(AllowList,)>`. +/// `Compose<(AllowList, TotalSupply)>`. /// /// This is shorthand for `::Out`; refer to [`Composable`] for /// the valid lists. @@ -54,14 +69,15 @@ pub type Compose = ::Out; /// resolves to its combined contract type through the `Out` associated type. /// /// Single contract types resolve to themselves (with or without the -/// one-element tuple form). Mutually exclusive contract types (e.g. +/// one-element tuple form), and pairs are declared in both orders, making the +/// list order-insensitive. Mutually exclusive contract types (e.g. /// `AllowList` and `BlockList`) have no implementation, so combining them /// does not compile. #[diagnostic::on_unimplemented( message = "`{Self}` is not a valid contract type combination", - note = "valid single contract types: `Base`, `AllowList`, `BlockList`, `RWA`, `Vault`, \ - `FungibleVotes`", - note = "no multi-type combinations are curated for fungible tokens yet", + note = "valid single contract types: `Base`, `AllowList`, `BlockList`, `TotalSupply`, `RWA`, \ + `Vault`, `FungibleVotes`", + note = "valid combinations: `(AllowList, TotalSupply)`, `(BlockList, TotalSupply)`", note = "`AllowList` and `BlockList` are mutually exclusive" )] pub trait Composable { @@ -89,6 +105,12 @@ impl Composable for BlockList { impl Composable for (BlockList,) { type Out = BlockList; } +impl Composable for TotalSupply { + type Out = TotalSupply; +} +impl Composable for (TotalSupply,) { + type Out = TotalSupply; +} impl Composable for RWA { type Out = RWA; } @@ -107,3 +129,18 @@ impl Composable for FungibleVotes { impl Composable for (FungibleVotes,) { type Out = FungibleVotes; } + +// Pairs are declared in both orders, so the contract type list is +// order-insensitive. +impl Composable for (AllowList, TotalSupply) { + type Out = TotalSupplyAllowList; +} +impl Composable for (TotalSupply, AllowList) { + type Out = TotalSupplyAllowList; +} +impl Composable for (BlockList, TotalSupply) { + type Out = TotalSupplyBlockList; +} +impl Composable for (TotalSupply, BlockList) { + type Out = TotalSupplyBlockList; +} diff --git a/packages/tokens/src/fungible/extensions/combinations/storage.rs b/packages/tokens/src/fungible/extensions/combinations/storage.rs new file mode 100644 index 000000000..9220942f3 --- /dev/null +++ b/packages/tokens/src/fungible/extensions/combinations/storage.rs @@ -0,0 +1,152 @@ +use soroban_sdk::{Address, Env, MuxedAddress}; + +use crate::fungible::{ + extensions::{ + allowlist::{AllowList, AllowListContractType}, + blocklist::{BlockList, BlockListContractType}, + total_supply::{decrease_total_supply, mint, total_supply, TotalSupplyOverrides}, + }, + overrides::BurnableOverrides, + ContractOverrides, +}; + +/// Contract type combining the [`AllowList`] transfer policy with total +/// supply tracking. +pub struct TotalSupplyAllowList; + +/// Contract type combining the [`BlockList`] transfer policy with total +/// supply tracking. +pub struct TotalSupplyBlockList; + +impl TotalSupplyOverrides for TotalSupplyAllowList {} +impl TotalSupplyOverrides for TotalSupplyBlockList {} + +// The combined contract types keep enforcing their respective list policy. +impl AllowListContractType for TotalSupplyAllowList {} +impl BlockListContractType for TotalSupplyBlockList {} + +// Transfers and approvals never touch the total supply, so they are routed +// to the respective list policy unchanged. +impl ContractOverrides for TotalSupplyAllowList { + fn transfer(e: &Env, from: &Address, to: &MuxedAddress, amount: i128) { + AllowList::transfer(e, from, to, amount); + } + + fn transfer_from(e: &Env, spender: &Address, from: &Address, to: &Address, amount: i128) { + AllowList::transfer_from(e, spender, from, to, amount); + } + + fn approve(e: &Env, owner: &Address, spender: &Address, amount: i128, live_until_ledger: u32) { + AllowList::approve(e, owner, spender, amount, live_until_ledger); + } +} + +impl ContractOverrides for TotalSupplyBlockList { + fn transfer(e: &Env, from: &Address, to: &MuxedAddress, amount: i128) { + BlockList::transfer(e, from, to, amount); + } + + fn transfer_from(e: &Env, spender: &Address, from: &Address, to: &Address, amount: i128) { + BlockList::transfer_from(e, spender, from, to, amount); + } + + fn approve(e: &Env, owner: &Address, spender: &Address, amount: i128, live_until_ledger: u32) { + BlockList::approve(e, owner, spender, amount, live_until_ledger); + } +} + +impl BurnableOverrides for TotalSupplyAllowList { + fn burn(e: &Env, from: &Address, amount: i128) { + TotalSupplyAllowList::burn(e, from, amount); + } + + fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { + TotalSupplyAllowList::burn_from(e, spender, from, amount); + } +} + +impl BurnableOverrides for TotalSupplyBlockList { + fn burn(e: &Env, from: &Address, amount: i128) { + TotalSupplyBlockList::burn(e, from, amount); + } + + fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { + TotalSupplyBlockList::burn_from(e, spender, from, amount); + } +} + +impl TotalSupplyAllowList { + /// Returns the total amount of tokens in circulation. + /// + /// refer to [`total_supply`] for the inline documentation. + pub fn total_supply(e: &Env) -> i128 { + total_supply(e) + } + + /// Creates `amount` of tokens and assigns them to `to`, increasing the + /// total supply accordingly. + /// + /// refer to [`mint`] for the inline documentation. + pub fn mint(e: &Env, to: &Address, amount: i128) { + mint(e, to, amount); + } + + /// Destroys `amount` of tokens from `from` through the allowlist burn + /// policy and decreases the total supply accordingly. + /// + /// refer to [`AllowList::burn`] and [`decrease_total_supply`] for the + /// inline documentation. + pub fn burn(e: &Env, from: &Address, amount: i128) { + AllowList::burn(e, from, amount); + decrease_total_supply(e, amount); + } + + /// Destroys `amount` of tokens from `from` using the allowance mechanism, + /// through the allowlist burn policy, and decreases the total supply + /// accordingly. + /// + /// refer to [`AllowList::burn_from`] and [`decrease_total_supply`] for + /// the inline documentation. + pub fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { + AllowList::burn_from(e, spender, from, amount); + decrease_total_supply(e, amount); + } +} + +impl TotalSupplyBlockList { + /// Returns the total amount of tokens in circulation. + /// + /// refer to [`total_supply`] for the inline documentation. + pub fn total_supply(e: &Env) -> i128 { + total_supply(e) + } + + /// Creates `amount` of tokens and assigns them to `to`, increasing the + /// total supply accordingly. + /// + /// refer to [`mint`] for the inline documentation. + pub fn mint(e: &Env, to: &Address, amount: i128) { + mint(e, to, amount); + } + + /// Destroys `amount` of tokens from `from` through the blocklist burn + /// policy and decreases the total supply accordingly. + /// + /// refer to [`BlockList::burn`] and [`decrease_total_supply`] for the + /// inline documentation. + pub fn burn(e: &Env, from: &Address, amount: i128) { + BlockList::burn(e, from, amount); + decrease_total_supply(e, amount); + } + + /// Destroys `amount` of tokens from `from` using the allowance mechanism, + /// through the blocklist burn policy, and decreases the total supply + /// accordingly. + /// + /// refer to [`BlockList::burn_from`] and [`decrease_total_supply`] for + /// the inline documentation. + pub fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { + BlockList::burn_from(e, spender, from, amount); + decrease_total_supply(e, amount); + } +} diff --git a/packages/tokens/src/fungible/extensions/combinations/test.rs b/packages/tokens/src/fungible/extensions/combinations/test.rs new file mode 100644 index 000000000..c6c7172a3 --- /dev/null +++ b/packages/tokens/src/fungible/extensions/combinations/test.rs @@ -0,0 +1,102 @@ +extern crate std; + +use soroban_sdk::{contract, testutils::Address as _, Address, Env, MuxedAddress}; + +use crate::fungible::{ + allowlist::AllowList, + blocklist::BlockList, + extensions::combinations::Compose, + overrides::BurnableOverrides, + total_supply::{mint, total_supply, TotalSupply}, + Base, ContractOverrides, +}; + +type AllowListWithSupply = Compose<(AllowList, TotalSupply)>; +// deliberately the swapped order, asserting that the list is +// order-insensitive +type BlockListWithSupply = Compose<(TotalSupply, BlockList)>; + +#[contract] +struct MockContract; + +#[test] +fn allowlist_burn_decreases_supply() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let account = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &account, 100); + AllowList::allow_user(&e, &account); + ::burn(&e, &account, 40); + assert_eq!(Base::balance(&e, &account), 60); + assert_eq!(total_supply(&e), 60); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #113)")] +fn allowlist_burn_respects_policy() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let account = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &account, 100); + // `account` is not allowed, the allowlist policy has to reject the + // burn + ::burn(&e, &account, 40); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #113)")] +fn allowlist_transfer_respects_policy() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let from = Address::generate(&e); + let recipient = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &from, 100); + // neither account is allowed, the allowlist policy has to reject the + // transfer + ::transfer( + &e, + &from, + &MuxedAddress::from(recipient), + 30, + ); + }); +} + +#[test] +fn blocklist_burn_from_decreases_supply() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let owner = Address::generate(&e); + let spender = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &owner, 100); + Base::approve(&e, &owner, &spender, 40, e.ledger().sequence() + 100); + ::burn_from(&e, &spender, &owner, 40); + assert_eq!(Base::balance(&e, &owner), 60); + assert_eq!(total_supply(&e), 60); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #114)")] +fn blocklist_burn_respects_policy() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let account = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &account, 100); + BlockList::block_user(&e, &account); + // `account` is blocked, the blocklist policy has to reject the burn + ::burn(&e, &account, 40); + }); +} diff --git a/packages/tokens/src/fungible/extensions/mod.rs b/packages/tokens/src/fungible/extensions/mod.rs index 3af97ab36..491d98595 100644 --- a/packages/tokens/src/fungible/extensions/mod.rs +++ b/packages/tokens/src/fungible/extensions/mod.rs @@ -3,4 +3,5 @@ pub mod blocklist; pub mod burnable; pub mod capped; pub mod combinations; +pub mod total_supply; pub mod votes; diff --git a/packages/tokens/src/fungible/extensions/total_supply/mod.rs b/packages/tokens/src/fungible/extensions/total_supply/mod.rs new file mode 100644 index 000000000..b545713b2 --- /dev/null +++ b/packages/tokens/src/fungible/extensions/total_supply/mod.rs @@ -0,0 +1,90 @@ +//! # Total Supply Extension for Fungible Token. +//! +//! Tracking the total supply is not required by SEP-41 and is therefore not +//! part of the base [`crate::fungible::FungibleToken`] trait. Keeping a +//! global supply counter has a scalability cost: every mint and burn writes +//! the same storage entry, and a transaction writing an entry cannot execute +//! in parallel with transactions reading it. Tokens that do not need the +//! counter are better off without it, which is why supply tracking is +//! provided as this opt-in extension. +//! +//! The extension consists of two parts: +//! +//! - [`FungibleTotalSupply`]: exposes the `total_supply()` function on the +//! contract. +//! - A supply-aware contract type, so that burns performed through +//! [`crate::fungible::burnable::FungibleBurnable`] decrease the supply: +//! [`TotalSupply`] for the vanilla behavior, or a combination resolved by +//! [`crate::fungible::combinations::Compose`] (e.g. `Compose<(AllowList, +//! TotalSupply)>`) to pair tracking with the allowlist or blocklist policy. +//! +//! Minting has to go through [`mint`] (instead of +//! [`crate::fungible::Base::mint`]) for the supply to be increased. +//! +//! Usage: +//! +//! ```ignore +//! #[contractimpl(contracttrait)] +//! impl FungibleToken for MyToken { +//! type ContractType = Compose<(TotalSupply,)>; +//! } +//! +//! #[contractimpl(contracttrait)] +//! impl FungibleTotalSupply for MyToken {} +//! ``` +//! +//! The [`crate::rwa::RWA`], [`crate::vault::Vault`] and +//! [`crate::fungible::votes::FungibleVotes`] contract types are inherently +//! supply-aware; [`FungibleTotalSupply`] can be implemented on top of them +//! directly. +//! +//! The supply is stored in its own `persistent` entry, ensuring that mints +//! and burns only conflict with each other and never with plain transfers. + +pub mod storage; + +#[cfg(test)] +mod test; + +use soroban_sdk::{contracttrait, Env}; +pub use storage::{ + decrease_total_supply, increase_total_supply, mint, total_supply, TotalSupply, + TotalSupplyStorageKey, +}; + +// The trait is defined alongside its siblings (`ContractOverrides`, +// `BurnableOverrides`) in the private `overrides` module; this re-export is +// its public path. +pub use crate::fungible::overrides::TotalSupplyOverrides; +use crate::fungible::FungibleToken; + +/// Total Supply Trait for Fungible Token +/// +/// The `FungibleTotalSupply` trait extends the `FungibleToken` trait to +/// expose the total amount of tokens in circulation. +/// +/// This trait can only be implemented when the contract's `ContractType` +/// accounts for the total supply: +/// +/// * [`TotalSupply`] (vanilla behavior), +/// * `Compose<(AllowList, TotalSupply)>` and `Compose<(BlockList, +/// TotalSupply)>` (refer to [`crate::fungible::combinations::Compose`]), +/// * [`crate::rwa::RWA`], +/// * [`crate::vault::Vault`], +/// * [`crate::fungible::votes::FungibleVotes`] (the supply is served from the +/// voting checkpoints). +/// +/// When using one of the `TotalSupply*` contract types, minting has to be +/// performed with [`mint`] so that the supply is increased; burns through +/// [`crate::fungible::burnable::FungibleBurnable`] decrease it automatically. +#[contracttrait] +pub trait FungibleTotalSupply: FungibleToken { + /// Returns the total amount of tokens in circulation. + /// + /// # Arguments + /// + /// * `e` - Access to the Soroban environment. + fn total_supply(e: &Env) -> i128 { + Self::ContractType::total_supply(e) + } +} diff --git a/packages/tokens/src/fungible/extensions/total_supply/storage.rs b/packages/tokens/src/fungible/extensions/total_supply/storage.rs new file mode 100644 index 000000000..3fc1ebcb7 --- /dev/null +++ b/packages/tokens/src/fungible/extensions/total_supply/storage.rs @@ -0,0 +1,198 @@ +use soroban_sdk::{contracttype, panic_with_error, Address, Env}; + +use crate::fungible::{ + overrides::{BurnableOverrides, TotalSupplyOverrides}, + Base, ContractOverrides, FungibleTokenError, TOTAL_SUPPLY_EXTEND_AMOUNT, + TOTAL_SUPPLY_TTL_THRESHOLD, +}; + +/// Contract type that layers total supply accounting on top of the vanilla +/// [`Base`] behavior. +/// +/// For combining total supply tracking with the allowlist or blocklist +/// transfer policies, refer to +/// [`crate::fungible::combinations::Compose`], e.g. +/// `Compose<(AllowList, TotalSupply)>`. +pub struct TotalSupply; + +/// Storage keys for the data associated with the total supply extension of +/// `FungibleToken` +#[contracttype] +pub enum TotalSupplyStorageKey { + TotalSupply, +} + +impl TotalSupplyOverrides for TotalSupply {} + +impl ContractOverrides for TotalSupply {} + +impl BurnableOverrides for TotalSupply { + fn burn(e: &Env, from: &Address, amount: i128) { + TotalSupply::burn(e, from, amount); + } + + fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { + TotalSupply::burn_from(e, spender, from, amount); + } +} + +impl TotalSupply { + /// Returns the total amount of tokens in circulation. + /// + /// refer to [`total_supply`] for the inline documentation. + pub fn total_supply(e: &Env) -> i128 { + total_supply(e) + } + + /// Creates `amount` of tokens and assigns them to `to`, increasing the + /// total supply accordingly. + /// + /// refer to [`mint`] for the inline documentation. + pub fn mint(e: &Env, to: &Address, amount: i128) { + mint(e, to, amount); + } + + /// Destroys `amount` of tokens from `from` and decreases the total supply + /// accordingly. + /// + /// refer to [`crate::fungible::Base::burn`] and + /// [`decrease_total_supply`] for the inline documentation. + pub fn burn(e: &Env, from: &Address, amount: i128) { + Base::burn(e, from, amount); + decrease_total_supply(e, amount); + } + + /// Destroys `amount` of tokens from `from` using the allowance mechanism + /// and decreases the total supply accordingly. + /// + /// refer to [`crate::fungible::Base::burn_from`] and + /// [`decrease_total_supply`] for the inline documentation. + pub fn burn_from(e: &Env, spender: &Address, from: &Address, amount: i128) { + Base::burn_from(e, spender, from, amount); + decrease_total_supply(e, amount); + } +} + +// ################## QUERY STATE ################## + +/// Returns the total amount of tokens in circulation. Defaults to `0` if no +/// supply is recorded. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +pub fn total_supply(e: &Env) -> i128 { + let key = TotalSupplyStorageKey::TotalSupply; + if let Some(supply) = e.storage().persistent().get::<_, i128>(&key) { + e.storage().persistent().extend_ttl( + &key, + TOTAL_SUPPLY_TTL_THRESHOLD, + TOTAL_SUPPLY_EXTEND_AMOUNT, + ); + supply + } else { + 0 + } +} + +// ################## CHANGE STATE ################## + +/// Creates `amount` of tokens and assigns them to `to`, increasing the total +/// supply accordingly. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +/// * `to` - The address receiving the new tokens. +/// * `amount` - The amount of tokens to mint. +/// +/// # Errors +/// +/// * [`FungibleTokenError::MathOverflow`] - When the total supply overflows. +/// * refer to [`crate::fungible::Base::update`] errors. +/// +/// # Events +/// +/// * topics - `["mint", to: Address]` +/// * data - `[amount: i128]` +/// +/// # Security Warning +/// +/// ⚠️ SECURITY RISK: This function has NO AUTHORIZATION CONTROLS ⚠️ +/// +/// It is the responsibility of the implementer to establish appropriate +/// access controls to ensure that only authorized accounts can execute +/// minting operations. Failure to implement proper authorization could +/// lead to security vulnerabilities and unauthorized token creation. +/// +/// The implementation will typically look similar to the following +/// (pseudo-code): +/// +/// ```ignore +/// let admin = read_administrator(e); +/// admin.require_auth(); +/// ``` +pub fn mint(e: &Env, to: &Address, amount: i128) { + increase_total_supply(e, amount); + Base::mint(e, to, amount); +} + +// ################## LOW-LEVEL HELPERS ################## + +/// Adds `amount` to the total supply. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +/// * `amount` - The amount to be added to the total supply. +/// +/// # Errors +/// +/// * [`FungibleTokenError::LessThanZero`] - When `amount < 0`. +/// * [`FungibleTokenError::MathOverflow`] - When the total supply overflows. +/// +/// # Notes +/// +/// This is a raw accounting helper: it does not move any balances and does +/// not emit events. [`mint`] should be preferred unless a custom minting +/// flow is being composed. +pub fn increase_total_supply(e: &Env, amount: i128) { + if amount < 0 { + panic_with_error!(e, FungibleTokenError::LessThanZero); + } + let Some(new_total_supply) = total_supply(e).checked_add(amount) else { + panic_with_error!(e, FungibleTokenError::MathOverflow); + }; + e.storage().persistent().set(&TotalSupplyStorageKey::TotalSupply, &new_total_supply); +} + +/// Subtracts `amount` from the total supply. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +/// * `amount` - The amount to be subtracted from the total supply. +/// +/// # Errors +/// +/// * [`FungibleTokenError::LessThanZero`] - When `amount < 0`. +/// * [`FungibleTokenError::MathOverflow`] - When `amount` exceeds the recorded +/// total supply. +/// +/// # Notes +/// +/// This is a raw accounting helper: it does not move any balances and does +/// not emit events. `amount` can only exceed the recorded supply when +/// balances were created without supply accounting (e.g. via +/// [`crate::fungible::Base::mint`]); mixing tracked and untracked flows in +/// the same contract is a bug. +pub fn decrease_total_supply(e: &Env, amount: i128) { + if amount < 0 { + panic_with_error!(e, FungibleTokenError::LessThanZero); + } + let supply = total_supply(e); + if amount > supply { + panic_with_error!(e, FungibleTokenError::MathOverflow); + } + e.storage().persistent().set(&TotalSupplyStorageKey::TotalSupply, &(supply - amount)); +} diff --git a/packages/tokens/src/fungible/extensions/total_supply/test.rs b/packages/tokens/src/fungible/extensions/total_supply/test.rs new file mode 100644 index 000000000..1d60bf57e --- /dev/null +++ b/packages/tokens/src/fungible/extensions/total_supply/test.rs @@ -0,0 +1,155 @@ +extern crate std; + +use soroban_sdk::{contract, testutils::Address as _, Address, Env, MuxedAddress, String}; + +use crate::fungible::{ + extensions::total_supply::{ + decrease_total_supply, increase_total_supply, mint, total_supply, TotalSupply, + TotalSupplyOverrides, + }, + overrides::BurnableOverrides, + Base, ContractOverrides, +}; + +#[contract] +struct MockContract; + +#[test] +fn initial_supply_is_zero() { + let e = Env::default(); + let address = e.register(MockContract, ()); + e.as_contract(&address, || { + assert_eq!(total_supply(&e), 0); + assert_eq!(::total_supply(&e), 0); + }); +} + +#[test] +fn mint_increases_supply_and_balance() { + let e = Env::default(); + let address = e.register(MockContract, ()); + let account = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &account, 100); + assert_eq!(Base::balance(&e, &account), 100); + assert_eq!(total_supply(&e), 100); + + mint(&e, &account, 50); + assert_eq!(Base::balance(&e, &account), 150); + assert_eq!(total_supply(&e), 150); + }); +} + +#[test] +fn burn_decreases_supply() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let account = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &account, 100); + ::burn(&e, &account, 40); + assert_eq!(Base::balance(&e, &account), 60); + assert_eq!(total_supply(&e), 60); + }); +} + +#[test] +fn burn_from_decreases_supply() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let owner = Address::generate(&e); + let spender = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &owner, 100); + Base::approve(&e, &owner, &spender, 40, e.ledger().sequence() + 100); + ::burn_from(&e, &spender, &owner, 40); + assert_eq!(Base::balance(&e, &owner), 60); + assert_eq!(total_supply(&e), 60); + }); +} + +#[test] +fn contract_type_delegates_token_operations() { + let e = Env::default(); + e.mock_all_auths(); + let address = e.register(MockContract, ()); + let from = Address::generate(&e); + let recipient = Address::generate(&e); + e.as_contract(&address, || { + mint(&e, &from, 100); + TotalSupply::transfer(&e, &from, &MuxedAddress::from(recipient.clone()), 30); + assert_eq!(TotalSupply::balance(&e, &from), 70); + assert_eq!(TotalSupply::balance(&e, &recipient), 30); + // transfers leave the supply untouched + assert_eq!(total_supply(&e), 100); + }); + + // separate invocation, so that `from` can authorize again + e.as_contract(&address, || { + TotalSupply::approve(&e, &from, &recipient, 20, e.ledger().sequence() + 100); + assert_eq!(TotalSupply::allowance(&e, &from, &recipient), 20); + TotalSupply::transfer_from(&e, &recipient, &from, &recipient, 20); + assert_eq!(TotalSupply::balance(&e, &recipient), 50); + assert_eq!(total_supply(&e), 100); + + Base::set_metadata(&e, 7, String::from_str(&e, "My Token"), String::from_str(&e, "TKN")); + assert_eq!(TotalSupply::decimals(&e), 7); + assert_eq!(TotalSupply::name(&e), String::from_str(&e, "My Token")); + assert_eq!(TotalSupply::symbol(&e), String::from_str(&e, "TKN")); + }); +} + +#[test] +fn increase_and_decrease_total_supply_work() { + let e = Env::default(); + let address = e.register(MockContract, ()); + e.as_contract(&address, || { + increase_total_supply(&e, 100); + assert_eq!(total_supply(&e), 100); + decrease_total_supply(&e, 60); + assert_eq!(total_supply(&e), 40); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #103)")] +fn increase_total_supply_rejects_negative_amount() { + let e = Env::default(); + let address = e.register(MockContract, ()); + e.as_contract(&address, || { + increase_total_supply(&e, -1); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #103)")] +fn decrease_total_supply_rejects_negative_amount() { + let e = Env::default(); + let address = e.register(MockContract, ()); + e.as_contract(&address, || { + decrease_total_supply(&e, -1); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #104)")] +fn increase_total_supply_overflow_panics() { + let e = Env::default(); + let address = e.register(MockContract, ()); + e.as_contract(&address, || { + increase_total_supply(&e, i128::MAX); + increase_total_supply(&e, 1); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #104)")] +fn decrease_total_supply_underflow_panics() { + let e = Env::default(); + let address = e.register(MockContract, ()); + e.as_contract(&address, || { + decrease_total_supply(&e, 1); + }); +} diff --git a/packages/tokens/src/fungible/extensions/votes/storage.rs b/packages/tokens/src/fungible/extensions/votes/storage.rs index be656a6d9..d5db6a923 100644 --- a/packages/tokens/src/fungible/extensions/votes/storage.rs +++ b/packages/tokens/src/fungible/extensions/votes/storage.rs @@ -1,10 +1,23 @@ -use soroban_sdk::{Address, Env, MuxedAddress}; -use stellar_governance::votes::transfer_voting_units; +use soroban_sdk::{panic_with_error, Address, Env, MuxedAddress}; +use stellar_governance::votes::{get_total_supply, transfer_voting_units}; -use crate::fungible::{overrides::BurnableOverrides, Base, ContractOverrides}; +use crate::fungible::{ + overrides::BurnableOverrides, total_supply::TotalSupplyOverrides, Base, ContractOverrides, + FungibleTokenError, +}; pub struct FungibleVotes; +// The voting checkpoints already track the total supply; the query is served +// from them instead of a separate supply entry. +impl TotalSupplyOverrides for FungibleVotes { + fn total_supply(e: &Env) -> i128 { + get_total_supply(e) + .try_into() + .unwrap_or_else(|_| panic_with_error!(e, FungibleTokenError::MathOverflow)) + } +} + impl ContractOverrides for FungibleVotes { fn transfer(e: &Env, from: &Address, to: &MuxedAddress, amount: i128) { FungibleVotes::transfer(e, from, to, amount); diff --git a/packages/tokens/src/fungible/extensions/votes/test.rs b/packages/tokens/src/fungible/extensions/votes/test.rs index 1963e9d27..12067691a 100644 --- a/packages/tokens/src/fungible/extensions/votes/test.rs +++ b/packages/tokens/src/fungible/extensions/votes/test.rs @@ -3,7 +3,9 @@ extern crate std; use soroban_sdk::{contract, testutils::Address as _, Address, Env, MuxedAddress}; use stellar_governance::votes::{delegate, get_delegate, get_votes, get_voting_units}; -use crate::fungible::{extensions::votes::FungibleVotes, Base, ContractOverrides}; +use crate::fungible::{ + extensions::votes::FungibleVotes, total_supply::TotalSupplyOverrides, Base, ContractOverrides, +}; #[contract] struct MockContract; @@ -24,7 +26,7 @@ fn mint_updates_voting_units() { FungibleVotes::mint(&e, &alice, 100); assert_eq!(Base::balance(&e, &alice), 100); - assert_eq!(Base::total_supply(&e), 100); + assert_eq!(::total_supply(&e), 100); assert_eq!(get_voting_units(&e, &alice), 100); }); } @@ -55,7 +57,7 @@ fn burn_updates_voting_units() { FungibleVotes::burn(&e, &alice, 30); assert_eq!(Base::balance(&e, &alice), 70); - assert_eq!(Base::total_supply(&e), 70); + assert_eq!(::total_supply(&e), 70); assert_eq!(get_voting_units(&e, &alice), 70); }); } diff --git a/packages/tokens/src/fungible/mod.rs b/packages/tokens/src/fungible/mod.rs index 22f9cef31..3252ab783 100644 --- a/packages/tokens/src/fungible/mod.rs +++ b/packages/tokens/src/fungible/mod.rs @@ -3,7 +3,7 @@ //! Implements utilities for handling fungible tokens in a Soroban contract. //! //! This module provides essential storage functionalities required for managing -//! balances, allowances, and total supply of fungible tokens. +//! balances and allowances of fungible tokens. //! //! ## Design Overview //! @@ -27,16 +27,16 @@ //! //! The base module includes: //! -//! - Total supply management //! - Transfers and allowances //! //! The following optional extensions are available: //! //! - Metadata: Provides additional information about the token, such as name, //! symbol, and decimals. -//! - Burnable: Enables token holders to destroy their tokens, reducing the -//! total supply. -//! - Capped: Enables the contract to set a maximum limit on the total supply. +//! - Burnable: Enables token holders to destroy their tokens. +//! - TotalSupply: Tracks and exposes the total amount of tokens in circulation. +//! - Capped: Enables the contract to set a maximum limit on the total supply +//! (requires the TotalSupply extension). //! //! ## Compatibility and Compliance //! @@ -75,7 +75,8 @@ mod utils; mod test; pub use extensions::{ - allowlist, blocklist, burnable, capped, combinations, combinations::Compose, votes, + allowlist, blocklist, burnable, capped, combinations, combinations::Compose, total_supply, + votes, }; pub use overrides::{Base, ContractOverrides}; use soroban_sdk::{ @@ -89,8 +90,10 @@ pub use utils::{sac_admin_generic, sac_admin_wrapper}; /// The `FungibleToken` trait defines the core functionality for fungible /// tokens, adhering to SEP-41. It provides a standard interface for managing /// balances, allowances, and metadata associated with fungible tokens. -/// Additionally, this trait includes the `total_supply()` function, which is -/// not part of SEP-41 but is commonly used in token contracts. +/// +/// Tracking the total supply is not part of SEP-41 and is not provided by +/// this trait; it is available as the opt-in +/// [`crate::fungible::total_supply::FungibleTotalSupply`] extension. /// /// To fully comply with the SEP-41 specification one has to implement the /// `FungibleBurnable` trait in addition to this one. SEP-41 mandates support @@ -115,15 +118,22 @@ pub use utils::{sac_admin_generic, sac_admin_wrapper}; /// incompatible with [`crate::fungible::allowlist::AllowList`] trait and /// [`crate::rwa::RWA`] trait. /// * [`crate::rwa::RWA`] (enabling the compatibility and overrides for -/// [`crate::rwa::RWAToken`]) trait, incompatible with -/// [`crate::fungible::allowlist::AllowList`] trait and +/// [`crate::rwa::RWAToken`] and +/// [`crate::fungible::total_supply::FungibleTotalSupply`]) traits, +/// incompatible with [`crate::fungible::allowlist::AllowList`] trait and /// [`crate::fungible::blocklist::BlockList`] trait. +/// * [`crate::fungible::total_supply::TotalSupply`] (additionally tracking the +/// total supply, enabling the compatibility and overrides for +/// [`crate::fungible::total_supply::FungibleTotalSupply`]) trait. /// * [`crate::vault::Vault`] (enabling the compatibility and overrides for -/// [`crate::vault::FungibleVault`]) trait. +/// [`crate::vault::FungibleVault`] and +/// [`crate::fungible::total_supply::FungibleTotalSupply`]) traits. /// * [`crate::fungible::votes::FungibleVotes`] (enabling the compatibility and -/// overrides for [`stellar_governance::votes::Votes`]) trait. +/// overrides for [`stellar_governance::votes::Votes`] and +/// [`crate::fungible::total_supply::FungibleTotalSupply`]) traits. /// -/// The contract type is selected with +/// Contract types that can work together (e.g. `AllowList` with +/// `TotalSupply`) are combined by listing them in /// [`crate::fungible::combinations::Compose`]; invalid combinations are /// rejected at compile time. /// @@ -140,21 +150,12 @@ pub trait FungibleToken { /// The contract type is selected with /// [`crate::fungible::combinations::Compose`], by listing the contract /// types that override the `Base` behavior: `Compose<(Base,)>` for the - /// vanilla case, `Compose<(AllowList,)>` for an allowlist token, and so - /// on. Extensions that add functionality without overriding behavior - /// (e.g. [`crate::fungible::burnable::FungibleBurnable`]) have no - /// contract type and do not appear in the list. + /// vanilla case, `Compose<(AllowList, TotalSupply)>` for a combination, + /// and so on. Extensions that add functionality without overriding + /// behavior (e.g. [`crate::fungible::burnable::FungibleBurnable`]) have + /// no contract type and do not appear in the list. type ContractType: ContractOverrides; - /// Returns the total amount of tokens in circulation. - /// - /// # Arguments - /// - /// * `e` - Access to the Soroban environment. - fn total_supply(e: &Env) -> i128 { - Self::ContractType::total_supply(e) - } - /// Returns the amount of tokens held by `account`. /// /// # Arguments @@ -302,7 +303,7 @@ pub enum FungibleTokenError { InvalidLiveUntilLedger = 102, /// Indicates an error when an input that must be >= 0 LessThanZero = 103, - /// Indicates overflow when adding two values + /// Indicates an overflow or underflow in an arithmetic operation MathOverflow = 104, /// Indicates access to uninitialized metadata UnsetMetadata = 105, @@ -334,6 +335,8 @@ pub const BALANCE_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; pub const BALANCE_TTL_THRESHOLD: u32 = BALANCE_EXTEND_AMOUNT - DAY_IN_LEDGERS; pub const ALLOW_BLOCK_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; pub const ALLOW_BLOCK_TTL_THRESHOLD: u32 = ALLOW_BLOCK_EXTEND_AMOUNT - DAY_IN_LEDGERS; +pub const TOTAL_SUPPLY_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub const TOTAL_SUPPLY_TTL_THRESHOLD: u32 = TOTAL_SUPPLY_EXTEND_AMOUNT - DAY_IN_LEDGERS; pub const INSTANCE_EXTEND_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; pub const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_EXTEND_AMOUNT - DAY_IN_LEDGERS; diff --git a/packages/tokens/src/fungible/overrides.rs b/packages/tokens/src/fungible/overrides.rs index 7a7bdd44f..cfc62aa8a 100644 --- a/packages/tokens/src/fungible/overrides.rs +++ b/packages/tokens/src/fungible/overrides.rs @@ -1,5 +1,7 @@ use soroban_sdk::{Address, Env, MuxedAddress, String}; +use crate::fungible::extensions::total_supply::total_supply; + /// Internal override hook for [`crate::fungible::FungibleToken`]. /// /// # Note @@ -29,10 +31,6 @@ use soroban_sdk::{Address, Env, MuxedAddress, String}; /// } /// ``` pub trait ContractOverrides { - fn total_supply(e: &Env) -> i128 { - Base::total_supply(e) - } - fn balance(e: &Env, account: &Address) -> i128 { Base::balance(e, account) } @@ -94,3 +92,27 @@ pub trait BurnableOverrides { } impl BurnableOverrides for Base {} + +/// Internal override hook for +/// [`crate::fungible::total_supply::FungibleTotalSupply`]. +/// +/// # Note +/// +/// Like [`ContractOverrides`], this trait is internal plumbing of the +/// library. There is no need to implement or import it: implementing +/// [`crate::fungible::total_supply::FungibleTotalSupply`] with an empty body +/// is enough, and the right behavior is picked based on the contract's +/// `ContractType`. The library ships implementations for its supply-aware +/// contract types ([`crate::fungible::total_supply::TotalSupply`], the +/// combined contract types resolved by +/// [`crate::fungible::combinations::Compose`], `RWA`, `Vault`, +/// `FungibleVotes`). +/// +/// Unlike `BurnableOverrides`, there is deliberately no implementation for +/// [`Base`]: exposing the total supply requires a supply-tracking contract +/// type. +pub trait TotalSupplyOverrides { + fn total_supply(e: &Env) -> i128 { + total_supply(e) + } +} diff --git a/packages/tokens/src/fungible/storage.rs b/packages/tokens/src/fungible/storage.rs index b1f85b676..fb375fd0b 100644 --- a/packages/tokens/src/fungible/storage.rs +++ b/packages/tokens/src/fungible/storage.rs @@ -32,7 +32,6 @@ pub struct Metadata { #[contracttype] pub enum FungibleStorageKey { Meta, - TotalSupply, Balance(Address), Allowance(AllowanceKey), } @@ -40,16 +39,6 @@ pub enum FungibleStorageKey { impl Base { // ################## QUERY STATE ################## - /// Returns the total amount of tokens in circulation. If no supply is - /// recorded, it defaults to `0`. - /// - /// # Arguments - /// - /// * `e` - Access to the Soroban environment. - pub fn total_supply(e: &Env) -> i128 { - e.storage().instance().get(&FungibleStorageKey::TotalSupply).unwrap_or(0) - } - /// Returns the amount of tokens held by `account`. Defaults to `0` if no /// balance is stored. /// @@ -383,8 +372,7 @@ impl Base { } /// Transfers `amount` of tokens from `from` to `to` or alternatively - /// mints (or burns) tokens if `from` (or `to`) is `None`. Updates the total - /// supply accordingly. + /// mints (or burns) tokens if `from` (or `to`) is `None`. /// /// # Arguments /// @@ -398,12 +386,17 @@ impl Base { /// * [`FungibleTokenError::InsufficientBalance`] - When attempting to /// transfer more tokens than `from` current balance. /// * [`FungibleTokenError::LessThanZero`] - When `amount < 0`. - /// * [`FungibleTokenError::MathOverflow`] - When `total_supply` overflows. + /// * [`FungibleTokenError::MathOverflow`] - When the `to` balance + /// overflows. /// /// # Notes /// /// This function does not enforce authorization. Ensure that authorization /// is handled at a higher level. + /// + /// This function does not account for the total supply; supply-aware + /// flows (e.g. [`crate::fungible::total_supply::mint`], the RWA and Vault + /// contract types) layer the supply bookkeeping on top of it. pub fn update(e: &Env, from: Option<&Address>, to: Option<&Address>, amount: i128) { if amount < 0 { panic_with_error!(e, FungibleTokenError::LessThanZero); @@ -418,33 +411,19 @@ impl Base { e.storage() .persistent() .set(&FungibleStorageKey::Balance(account.clone()), &from_balance); - } else { - // `from` is None, so we're minting tokens. - let total_supply = Base::total_supply(e); - let Some(new_total_supply) = total_supply.checked_add(amount) else { - panic_with_error!(e, FungibleTokenError::MathOverflow); - }; - e.storage().instance().set(&FungibleStorageKey::TotalSupply, &new_total_supply); } if let Some(account) = to { - // NOTE: can't overflow because balance + amount is at most total_supply. - let to_balance = Base::balance(e, account) + amount; + let Some(to_balance) = Base::balance(e, account).checked_add(amount) else { + panic_with_error!(e, FungibleTokenError::MathOverflow); + }; e.storage() .persistent() .set(&FungibleStorageKey::Balance(account.clone()), &to_balance); - } else { - // `to` is None, so we're burning tokens. - - // NOTE: can't overflow because amount <= total_supply or amount <= from_balance - // <= total_supply. - let total_supply = Base::total_supply(e) - amount; - e.storage().instance().set(&FungibleStorageKey::TotalSupply, &total_supply); } } - /// Creates `amount` of tokens and assigns them to `to`. Updates - /// the total supply accordingly. + /// Creates `amount` of tokens and assigns them to `to`. /// /// # Arguments /// @@ -461,6 +440,13 @@ impl Base { /// * topics - `["mint", to: Address]` /// * data - `[amount: i128]` /// + /// # Notes + /// + /// This function does not account for the total supply. For tokens that + /// track it (i.e. implementers of + /// [`crate::fungible::total_supply::FungibleTotalSupply`]), + /// [`crate::fungible::total_supply::mint`] should be used instead. + /// /// # Security Warning /// /// ⚠️ SECURITY RISK: This function has NO AUTHORIZATION CONTROLS ⚠️ diff --git a/packages/tokens/src/fungible/test.rs b/packages/tokens/src/fungible/test.rs index 27678a85e..7a4edbd60 100644 --- a/packages/tokens/src/fungible/test.rs +++ b/packages/tokens/src/fungible/test.rs @@ -23,7 +23,6 @@ fn initial_state() { let address = e.register(MockContract, ()); let account = Address::generate(&e); e.as_contract(&address, || { - assert_eq!(Base::total_supply(&e), 0); assert_eq!(Base::balance(&e, &account), 0); }); } @@ -495,7 +494,6 @@ fn update_mints_tokens() { e.as_contract(&address, || { Base::update(&e, None, Some(&to), 100); assert_eq!(Base::balance(&e, &to), 100); - assert_eq!(Base::total_supply(&e), 100); }); } @@ -509,7 +507,6 @@ fn update_burns_tokens() { Base::mint(&e, &from, 100); Base::update(&e, Some(&from), None, 50); assert_eq!(Base::balance(&e, &from), 50); - assert_eq!(Base::total_supply(&e), 50); }); } @@ -762,7 +759,6 @@ fn mint_works() { e.as_contract(&address, || { Base::mint(&e, &account, 100); assert_eq!(Base::balance(&e, &account), 100); - assert_eq!(Base::total_supply(&e), 100); let mut event_assert = EventAssertion::new(&e, address.clone()); event_assert.assert_event_count(1); diff --git a/packages/tokens/src/rwa/mod.rs b/packages/tokens/src/rwa/mod.rs index dc4231ec5..d2a99b545 100644 --- a/packages/tokens/src/rwa/mod.rs +++ b/packages/tokens/src/rwa/mod.rs @@ -128,6 +128,11 @@ use crate::fungible::FungibleToken; /// - Freezing mechanisms for regulatory enforcement /// - Recovery system for lost/old account scenarios /// - Administrative controls for token management +/// +/// The `RWA` contract type tracks the total supply (as mandated by T-REX +/// through the ERC-20 interface), and RWA contracts are expected to also +/// implement [`crate::fungible::total_supply::FungibleTotalSupply`] to +/// expose it. #[contracttrait] pub trait RWAToken: Pausable + FungibleToken { // ################## CORE TOKEN FUNCTIONS ################## diff --git a/packages/tokens/src/rwa/storage.rs b/packages/tokens/src/rwa/storage.rs index 076a45e8d..4ba4c0f10 100644 --- a/packages/tokens/src/rwa/storage.rs +++ b/packages/tokens/src/rwa/storage.rs @@ -2,7 +2,11 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, MuxedAddress, St use stellar_contract_utils::pausable::{paused, PausableError}; use crate::{ - fungible::{emit_transfer, Base, ContractOverrides}, + fungible::{ + emit_transfer, + total_supply::{decrease_total_supply, increase_total_supply, TotalSupplyOverrides}, + Base, ContractOverrides, + }, rwa::{ compliance::{AccountSnapshot, ComplianceClient, TransferKind}, emit_address_frozen, emit_burn, emit_compliance_set, emit_identity_verifier_set, emit_mint, @@ -41,6 +45,10 @@ impl ContractOverrides for RWA { } } +// T-REX (ERC-3643) tokens expose the total supply, so the RWA contract type +// is inherently supply-aware. +impl TotalSupplyOverrides for RWA {} + impl RWA { // ################## QUERY STATE ################## @@ -319,6 +327,7 @@ impl RWA { // Snapshot before the mint so the hook observes the pre-mint balance. let to_snapshot = Self::account_snapshot(e, to); + increase_total_supply(e, amount); Base::update(e, None, Some(to), amount); let compliance_addr = Self::compliance(e); @@ -374,6 +383,7 @@ impl RWA { } Base::update(e, Some(user_address), None, amount); + decrease_total_supply(e, amount); let compliance_addr = Self::compliance(e); let compliance_client = ComplianceClient::new(e, &compliance_addr); diff --git a/packages/tokens/src/rwa/test.rs b/packages/tokens/src/rwa/test.rs index 15f91081e..154f04477 100644 --- a/packages/tokens/src/rwa/test.rs +++ b/packages/tokens/src/rwa/test.rs @@ -8,7 +8,7 @@ use stellar_contract_utils::pausable; use stellar_event_assertion::EventAssertion; use crate::{ - fungible::ContractOverrides, + fungible::{total_supply::TotalSupplyOverrides, ContractOverrides}, rwa::{ compliance::{AccountSnapshot, TransferKind}, storage::RWAStorageKey, diff --git a/packages/tokens/src/vault/mod.rs b/packages/tokens/src/vault/mod.rs index 39cebeb99..8353ef807 100644 --- a/packages/tokens/src/vault/mod.rs +++ b/packages/tokens/src/vault/mod.rs @@ -17,6 +17,10 @@ use crate::fungible::FungibleToken; /// /// The vault maintains a conversion rate between shares and assets based on /// the total supply of shares and total assets held by the vault contract. +/// The `Vault` contract type is therefore inherently supply-aware, and vault +/// contracts are expected to also implement +/// [`crate::fungible::total_supply::FungibleTotalSupply`] to expose the +/// supply of shares. /// /// # Design Overview /// diff --git a/packages/tokens/src/vault/storage.rs b/packages/tokens/src/vault/storage.rs index ecc3d8332..12656f31a 100644 --- a/packages/tokens/src/vault/storage.rs +++ b/packages/tokens/src/vault/storage.rs @@ -2,7 +2,10 @@ use soroban_sdk::{contracttype, panic_with_error, token, Address, Env}; use stellar_contract_utils::math::{i128_fixed_point::mul_div_with_rounding, Rounding}; use crate::{ - fungible::{Base, ContractOverrides}, + fungible::{ + total_supply::{decrease_total_supply, increase_total_supply, TotalSupplyOverrides}, + Base, ContractOverrides, + }, vault::{emit_deposit, emit_withdraw, VaultTokenError, MAX_DECIMALS_OFFSET}, }; @@ -14,6 +17,10 @@ impl ContractOverrides for Vault { } } +// The vault's share math requires the total supply of shares, so the vault +// contract type is inherently supply-aware. +impl TotalSupplyOverrides for Vault {} + /// Storage keys for the data associated with the vault extension #[contracttype] pub enum VaultStorageKey { @@ -744,6 +751,7 @@ impl Vault { token_client.transfer_from(operator, from, &e.current_contract_address(), &assets); } + increase_total_supply(e, shares); Base::update(e, None, Some(receiver), shares); } @@ -787,6 +795,7 @@ impl Vault { Base::spend_allowance(e, owner, operator, shares); } Base::update(e, Some(owner), None, shares); + decrease_total_supply(e, shares); let token_client = token::Client::new(e, &Self::query_asset(e)); // `safeTransfer` mechanism is not present in the base module, (will be provided // as an extension) diff --git a/packages/tokens/src/vault/test.rs b/packages/tokens/src/vault/test.rs index e19751a84..7d08df03c 100644 --- a/packages/tokens/src/vault/test.rs +++ b/packages/tokens/src/vault/test.rs @@ -3,7 +3,7 @@ extern crate std; use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; use crate::{ - fungible::Base, + fungible::{total_supply::total_supply, Base}, vault::{Vault, MAX_DECIMALS_OFFSET}, }; @@ -219,7 +219,7 @@ fn deposit_functionality() { // Check results assert_eq!(Base::balance(&e, &user), shares_minted); - assert_eq!(Base::total_supply(&e), shares_minted); + assert_eq!(total_supply(&e), shares_minted); assert_eq!(Vault::total_assets(&e), deposit_amount); // For first deposit, shares should equal assets with offset @@ -249,7 +249,7 @@ fn mint_functionality() { Vault::mint(&e, shares_to_mint, user.clone(), user.clone(), user.clone()); assert_eq!(Base::balance(&e, &user), shares_to_mint); - assert_eq!(Base::total_supply(&e), shares_to_mint); + assert_eq!(total_supply(&e), shares_to_mint); assert_eq!(assets_deposited, required_assets); }); } @@ -312,7 +312,7 @@ fn redeem_functionality() { // Check results assert_eq!(Base::balance(&e, &user), shares_minted - shares_to_redeem); - assert_eq!(Base::total_supply(&e), shares_minted - shares_to_redeem); + assert_eq!(total_supply(&e), shares_minted - shares_to_redeem); // Should receive approximately half the original deposit let expected_assets = deposit_amount / 2; @@ -544,7 +544,7 @@ fn deposit_transfer_from() { // Check results assert_eq!(Base::balance(&e, &user), shares_minted); - assert_eq!(Base::total_supply(&e), shares_minted); + assert_eq!(total_supply(&e), shares_minted); assert_eq!(Vault::total_assets(&e), deposit_amount); // For first deposit, shares should equal assets with offset @@ -611,7 +611,7 @@ fn mint_transfer_from() { // Check results assert_eq!(Base::balance(&e, &user), shares_to_mint); - assert_eq!(Base::total_supply(&e), shares_to_mint); + assert_eq!(total_supply(&e), shares_to_mint); assert_eq!(assets_deposited, required_assets); }); }