diff --git a/.github/workflows/generic.yml b/.github/workflows/generic.yml index c3ebd3e18..d31e2d19b 100644 --- a/.github/workflows/generic.yml +++ b/.github/workflows/generic.yml @@ -47,7 +47,7 @@ jobs: - name: install rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: stable, nightly + toolchain: nightly components: clippy, rustfmt, llvm-tools-preview target: wasm32v1-none @@ -55,7 +55,7 @@ jobs: run: cargo install cargo-llvm-cov - name: Check format - run: cargo +nightly fmt --all -- --check + run: cargo fmt --all -- --check - name: Check clippy run: cargo clippy --release --locked --all-targets -- -D warnings diff --git a/Architecture.md b/Architecture.md index 87533b46f..b7e539304 100644 --- a/Architecture.md +++ b/Architecture.md @@ -33,69 +33,13 @@ stellar-contracts/ ### 1. Trait-Based Design with Associated Types -The library extensively uses Rust traits to define standard interfaces and behaviors, with a sophisticated approach to enable method overriding, and enforce mutually exclusive extensions through associated types: +Polymorphism is not directly possible in Rust. Instead, we achieve polymorphism-like behavior through **associated types** and **trait bounds**. -#### Enforcing Mutually Exclusive Extensions - -One of the most sophisticated aspects of this architecture is how it prevents incompatible extensions from being used together. This is achieved through **associated types** and **trait bounds**: - -```rust -// Core trait with associated type -trait NonFungibleToken { - type ContractType: ContractOverrides; - - fn transfer(e: &Env, from: Address, to: Address, token_id: u32) { - Self::ContractType::transfer(e, from, to, token_id); - } - // ... other methods -} - -// Contract type markers -pub struct Base; // Default implementation -pub struct Enumerable; // For enumeration features -pub struct Consecutive; // For batch minting optimization -``` - -#### Extension Trait Constraints - -Extensions are constrained to specific contract types using associated type bounds: - -```rust -// Enumerable can only be used with Enumerable contract type -trait NonFungibleEnumerable: NonFungibleToken { - fn total_supply(e: &Env) -> u32; - fn get_owner_token_id(e: &Env, owner: Address, index: u32) -> u32; - // ... -} - -// Consecutive can only be used with Consecutive contract type -trait NonFungibleConsecutive: NonFungibleToken { - // Batch minting functionality -} -``` +The library extensively uses Rust traits to define standard interfaces and behaviors, with a sophisticated approach to enable method overriding. -#### Mutual Exclusivity Enforcement +#### Override Mechanism Through *Overrides* Traits -This design makes it **impossible** to implement conflicting extensions: - -```rust -// ✅ This works - using Enumerable -impl NonFungibleToken for MyContract { - type ContractType = Enumerable; - // ... implementations -} -impl NonFungibleEnumerable for MyContract { - // ... enumerable methods -} - -// ❌ This CANNOT compile - Consecutive requires different ContractType -// impl NonFungibleConsecutive for MyContract { ... } -// ^^^ Error: expected `Consecutive`, found `Enumerable` -``` - -#### Override Mechanism Through ContractOverrides - -The `ContractOverrides` trait provides the actual implementations that vary by contract type: +For example, the `ContractOverrides` trait provides the actual implementations that vary by contract type: ```rust trait ContractOverrides { @@ -126,6 +70,16 @@ impl ContractOverrides for Consecutive { } ``` +#### Enforcing Mutually Exclusive Extensions + +We are using nightly feature `#![feature(negative_impls)]` to enforce mutually exclusive extensions. + +For example, Consecutive and Enumerable extensions for NonFungibleToken trait are mutually exclusive. If a contract implements both extensions, the compiler will generate an error. We achieve this through negative trait bounds: + +```rust +impl !NonFungibleConsecutive for T {} +``` + #### Benefits of This Approach 1. **Compile-Time Safety**: Incompatible extensions cannot be combined @@ -134,8 +88,6 @@ impl ContractOverrides for Consecutive { 4. **Automatic Behavior Override**: Methods automatically use the correct implementation based on contract type 5. **Modular Design**: Extensions can be developed and maintained independently -This pattern represents a novel solution to the challenge of providing both type safety and developer ergonomics in a trait-based extension system, avoiding the need for runtime checks or complex generic constraints. - ### 2. Dual-Layer Architecture The library provides two levels of abstraction: diff --git a/packages/tokens/src/fungible/extensions/allowlist/mod.rs b/packages/tokens/src/fungible/extensions/allowlist/mod.rs index a50040166..ebeb543e6 100644 --- a/packages/tokens/src/fungible/extensions/allowlist/mod.rs +++ b/packages/tokens/src/fungible/extensions/allowlist/mod.rs @@ -36,7 +36,7 @@ use crate::fungible::FungibleToken; /// authorization should be configured. Not providing a default implementation /// for this trait is a reminder for the implementor to provide the /// authorization logic for this trait. -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 254e618b2..d3ccff55d 100644 --- a/packages/tokens/src/fungible/extensions/blocklist/mod.rs +++ b/packages/tokens/src/fungible/extensions/blocklist/mod.rs @@ -36,7 +36,7 @@ use crate::fungible::FungibleToken; /// authorization should be configured. Not providing a default implementation /// for this trait is a reminder for the implementor to provide the /// authorization logic for this trait. -pub trait FungibleBlockList: FungibleToken { +pub trait FungibleBlockList: FungibleToken { /// Returns the blocked status of an account. /// /// # Arguments diff --git a/packages/tokens/src/fungible/extensions/mod.rs b/packages/tokens/src/fungible/extensions/mod.rs index dc364e5ad..6fbbf26c2 100644 --- a/packages/tokens/src/fungible/extensions/mod.rs +++ b/packages/tokens/src/fungible/extensions/mod.rs @@ -2,3 +2,8 @@ pub mod allowlist; pub mod blocklist; pub mod burnable; pub mod capped; + +// disable allowlist and blocklist together +use crate::fungible::extensions::{allowlist::FungibleAllowList, blocklist::FungibleBlockList}; + +impl !FungibleBlockList for T {} diff --git a/packages/tokens/src/lib.rs b/packages/tokens/src/lib.rs index a2907b1d7..c829586f1 100644 --- a/packages/tokens/src/lib.rs +++ b/packages/tokens/src/lib.rs @@ -12,6 +12,7 @@ //! working with the respective token type. #![no_std] +#![feature(negative_impls)] pub mod fungible; pub mod non_fungible; diff --git a/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs b/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs index 89ef5d190..a0faec77d 100644 --- a/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs @@ -74,7 +74,7 @@ use crate::non_fungible::NonFungibleToken; /// The `consecutive` extension provides its own business logic for creating and /// destroying tokens. Therefore, this trait is INCOMPATIBLE with the /// `Enumerable` extension. -pub trait NonFungibleConsecutive: NonFungibleToken {} +pub trait NonFungibleConsecutive: NonFungibleToken {} #[cfg(test)] mod test; diff --git a/packages/tokens/src/non_fungible/extensions/enumerable/mod.rs b/packages/tokens/src/non_fungible/extensions/enumerable/mod.rs index c7f1e4327..f312629cb 100644 --- a/packages/tokens/src/non_fungible/extensions/enumerable/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/enumerable/mod.rs @@ -41,7 +41,7 @@ use crate::non_fungible::NonFungibleToken; /// exists for the use-cases where the enumeration is required as an on-chain /// operation. #[contracttrait] -pub trait NonFungibleEnumerable: NonFungibleToken { +pub trait NonFungibleEnumerable: NonFungibleToken { /// Returns the total amount of tokens stored by the contract. /// /// # Arguments diff --git a/packages/tokens/src/non_fungible/extensions/mod.rs b/packages/tokens/src/non_fungible/extensions/mod.rs index a66ab502e..5cdd8091a 100644 --- a/packages/tokens/src/non_fungible/extensions/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/mod.rs @@ -2,3 +2,8 @@ pub mod burnable; pub mod consecutive; pub mod enumerable; pub mod royalties; + +// Negative trait bounds +use crate::non_fungible::{consecutive::NonFungibleConsecutive, enumerable::NonFungibleEnumerable}; + +impl !NonFungibleConsecutive for T {} diff --git a/packages/tokens/src/rwa/mod.rs b/packages/tokens/src/rwa/mod.rs index c38e40248..a69a9dcff 100644 --- a/packages/tokens/src/rwa/mod.rs +++ b/packages/tokens/src/rwa/mod.rs @@ -124,7 +124,7 @@ use crate::fungible::FungibleToken; /// - Freezing mechanisms for regulatory enforcement /// - Recovery system for lost/old account scenarios /// - Administrative controls for token management -pub trait RWAToken: Pausable + FungibleToken { +pub trait RWAToken: Pausable + FungibleToken { // ################## CORE TOKEN FUNCTIONS ################## /// Forces a transfer of tokens between two whitelisted wallets. diff --git a/packages/tokens/src/vault/mod.rs b/packages/tokens/src/vault/mod.rs index 39cebeb99..bf4746485 100644 --- a/packages/tokens/src/vault/mod.rs +++ b/packages/tokens/src/vault/mod.rs @@ -40,7 +40,7 @@ use crate::fungible::FungibleToken; /// providing familiar interfaces for Ethereum developers while leveraging /// Stellar's unique capabilities. #[contracttrait] -pub trait FungibleVault: FungibleToken { +pub trait FungibleVault: FungibleToken { /// Returns the address of the underlying asset that the vault manages. /// /// # Arguments @@ -52,7 +52,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::VaultAssetAddressNotSet`] - When the /// vault's underlying asset address has not been initialized. fn query_asset(e: &Env) -> Address { - Self::ContractType::query_asset(e) + Vault::query_asset(e) } /// Returns the total amount of underlying assets held by the vault. @@ -69,7 +69,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::VaultAssetAddressNotSet`] - When the /// vault's underlying asset address has not been initialized. fn total_assets(e: &Env) -> i128 { - Self::ContractType::total_assets(e) + Vault::total_assets(e) } /// Converts an amount of underlying assets to the equivalent amount of @@ -87,7 +87,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn convert_to_shares(e: &Env, assets: i128) -> i128 { - Self::ContractType::convert_to_shares(e, assets) + Vault::convert_to_shares(e, assets) } /// Converts an amount of vault shares to the equivalent amount of @@ -105,7 +105,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn convert_to_assets(e: &Env, shares: i128) -> i128 { - Self::ContractType::convert_to_assets(e, shares) + Vault::convert_to_assets(e, shares) } /// Returns the maximum amount of underlying assets that can be deposited @@ -116,7 +116,7 @@ pub trait FungibleVault: FungibleToken { /// * `e` - Access to the Soroban environment. /// * `receiver` - The address that would receive the vault shares. fn max_deposit(e: &Env, receiver: Address) -> i128 { - Self::ContractType::max_deposit(e, receiver) + Vault::max_deposit(e, receiver) } /// Simulates and returns the amount of vault shares that would be minted @@ -134,7 +134,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn preview_deposit(e: &Env, assets: i128) -> i128 { - Self::ContractType::preview_deposit(e, assets) + Vault::preview_deposit(e, assets) } /// Deposits underlying assets into the vault and mints vault shares @@ -170,7 +170,7 @@ pub trait FungibleVault: FungibleToken { /// /// Authorization for the operator must be handled at a higher level. fn deposit(e: &Env, assets: i128, receiver: Address, from: Address, operator: Address) -> i128 { - Self::ContractType::deposit(e, assets, receiver, from, operator) + Vault::deposit(e, assets, receiver, from, operator) } /// Returns the maximum amount of vault shares that can be minted @@ -181,7 +181,7 @@ pub trait FungibleVault: FungibleToken { /// * `e` - Access to the Soroban environment. /// * `receiver` - The address that would receive the vault shares. fn max_mint(e: &Env, receiver: Address) -> i128 { - Self::ContractType::max_mint(e, receiver) + Vault::max_mint(e, receiver) } /// Simulates and returns the amount of underlying assets required to mint @@ -199,7 +199,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn preview_mint(e: &Env, shares: i128) -> i128 { - Self::ContractType::preview_mint(e, shares) + Vault::preview_mint(e, shares) } /// Mints a specific amount of vault shares to the receiver by depositing @@ -236,7 +236,7 @@ pub trait FungibleVault: FungibleToken { /// /// Authorization for the operator must be handled at a higher level. fn mint(e: &Env, shares: i128, receiver: Address, from: Address, operator: Address) -> i128 { - Self::ContractType::mint(e, shares, receiver, from, operator) + Vault::mint(e, shares, receiver, from, operator) } /// Returns the maximum amount of underlying assets that can be @@ -254,7 +254,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn max_withdraw(e: &Env, owner: Address) -> i128 { - Self::ContractType::max_withdraw(e, owner) + Vault::max_withdraw(e, owner) } /// Simulates and returns the amount of vault shares that would be burned @@ -272,7 +272,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn preview_withdraw(e: &Env, assets: i128) -> i128 { - Self::ContractType::preview_withdraw(e, assets) + Vault::preview_withdraw(e, assets) } /// Withdraws a specific amount of underlying assets from the vault @@ -311,7 +311,7 @@ pub trait FungibleVault: FungibleToken { owner: Address, operator: Address, ) -> i128 { - Self::ContractType::withdraw(e, assets, receiver, owner, operator) + Vault::withdraw(e, assets, receiver, owner, operator) } /// Returns the maximum amount of vault shares that can be redeemed @@ -322,7 +322,7 @@ pub trait FungibleVault: FungibleToken { /// * `e` - Access to the Soroban environment. /// * `owner` - The address that owns the vault shares. fn max_redeem(e: &Env, owner: Address) -> i128 { - Self::ContractType::max_redeem(e, owner) + Vault::max_redeem(e, owner) } /// Simulates and returns the amount of underlying assets that would be @@ -340,7 +340,7 @@ pub trait FungibleVault: FungibleToken { /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical /// operations result in overflow. fn preview_redeem(e: &Env, shares: i128) -> i128 { - Self::ContractType::preview_redeem(e, shares) + Vault::preview_redeem(e, shares) } /// Redeems a specific amount of vault shares for underlying assets, @@ -376,7 +376,7 @@ pub trait FungibleVault: FungibleToken { /// /// Authorization for the operator must be handled at a higher level. fn redeem(e: &Env, shares: i128, receiver: Address, owner: Address, operator: Address) -> i128 { - Self::ContractType::redeem(e, shares, receiver, owner, operator) + Vault::redeem(e, shares, receiver, owner, operator) } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 11bcc00a7..86a00fe07 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "stable" +channel = "nightly" targets = ["wasm32v1-none"] components = ["rustfmt", "clippy", "rust-src"]