Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/generic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ 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

- name: Install cargo-llvm-cov
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
Expand Down
76 changes: 14 additions & 62 deletions Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = Enumerable> {
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<ContractType = Consecutive> {
// 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 {
Expand Down Expand Up @@ -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<T: NonFungibleEnumerable> !NonFungibleConsecutive for T {}
```

#### Benefits of This Approach

1. **Compile-Time Safety**: Incompatible extensions cannot be combined
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion packages/tokens/src/fungible/extensions/allowlist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = AllowList> {
pub trait FungibleAllowList: FungibleToken {
/// Returns the allowed status of an account.
///
/// # Arguments
Expand Down
2 changes: 1 addition & 1 deletion packages/tokens/src/fungible/extensions/blocklist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = BlockList> {
pub trait FungibleBlockList: FungibleToken {
/// Returns the blocked status of an account.
///
/// # Arguments
Expand Down
5 changes: 5 additions & 0 deletions packages/tokens/src/fungible/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: FungibleAllowList> !FungibleBlockList for T {}
1 change: 1 addition & 0 deletions packages/tokens/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//! working with the respective token type.

#![no_std]
#![feature(negative_impls)]

pub mod fungible;
pub mod non_fungible;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = Consecutive> {}
pub trait NonFungibleConsecutive: NonFungibleToken {}

#[cfg(test)]
mod test;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = Enumerable> {
pub trait NonFungibleEnumerable: NonFungibleToken {
/// Returns the total amount of tokens stored by the contract.
///
/// # Arguments
Expand Down
5 changes: 5 additions & 0 deletions packages/tokens/src/non_fungible/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: NonFungibleEnumerable> !NonFungibleConsecutive for T {}
2 changes: 1 addition & 1 deletion packages/tokens/src/rwa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = RWA> {
pub trait RWAToken: Pausable + FungibleToken {
// ################## CORE TOKEN FUNCTIONS ##################

/// Forces a transfer of tokens between two whitelisted wallets.
Expand Down
34 changes: 17 additions & 17 deletions packages/tokens/src/vault/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContractType = Vault> {
pub trait FungibleVault: FungibleToken {
/// Returns the address of the underlying asset that the vault manages.
///
/// # Arguments
Expand All @@ -52,7 +52,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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.
Expand All @@ -69,7 +69,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand All @@ -87,7 +87,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand All @@ -105,7 +105,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand All @@ -116,7 +116,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * `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
Expand All @@ -134,7 +134,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand Down Expand Up @@ -170,7 +170,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
///
/// 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
Expand All @@ -181,7 +181,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * `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
Expand All @@ -199,7 +199,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand Down Expand Up @@ -236,7 +236,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
///
/// 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
Expand All @@ -254,7 +254,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand All @@ -272,7 +272,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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
Expand Down Expand Up @@ -311,7 +311,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
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
Expand All @@ -322,7 +322,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * `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
Expand All @@ -340,7 +340,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
/// * [`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,
Expand Down Expand Up @@ -376,7 +376,7 @@ pub trait FungibleVault: FungibleToken<ContractType = Vault> {
///
/// 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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "stable"
channel = "nightly"
targets = ["wasm32v1-none"]
components = ["rustfmt", "clippy", "rust-src"]
Loading