Skip to content
Open
Show file tree
Hide file tree
Changes from all 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