OpenZeppelin / OpenZeppelin/stellar-contracts

🏗️ [Core Feature]: Route `mint` through the contract type via a `MintOverrides` trait

Open
#890 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
95
Forks
68
Avg merge
4d 42m
Merged PRs (30d)
20

Description

What is the key feature we're aiming to implement?

Follow-up to #884 / #885 and the discussion around the capped extension.

Problem

mint is the only state-changing token operation that is not part of any trait, so there is no single dispatch point for it. Today ten contract types carry an inherent mint(e, &to, amount) (Base, TotalSupply, FungibleVotes, RWA, the three votes combinations and the three supply combinations), while AllowList, BlockList and AllowBlockList have none. Consequences:

  • Self::ContractType::mint compiles for some Compose lists and not for others. The examples disagree accordingly: fungible-blocklist calls Base::mint, fungible-allowlist calls <Self as FungibleToken>::ContractType::mint.
  • Calling Base::mint on a supply-tracking contract type silently drifts the total supply; a later burn then panics with MathOverflow and the tokens cannot be burned.
  • The cap cannot be enforced by the type system. Capped::mint is a free-standing path the author has to remember to call, and Compose<(Vault, Capped, TotalSupply)> compiles while enforcing nothing.
Proposal

1. MintOverrides trait in fungible/overrides.rs, a sibling of BurnableOverrides:

pub trait MintOverrides {
    fn mint(e: &Env, to: &Address, amount: i128);
}
  • Required method, no default body. Forgetting the override on a supply-aware type is a liveness bug (burns panic), unlike the burn override where the failure is benign. One line per impl.
  • Implemented for every contract type except Vault. Vault has an inherent ERC-4626 mint with five arguments; an inherent item shadows a same-named trait item in path syntax, so a three-argument override on Vault would be an arity error, and a free share mint is wrong for a vault anyway.
  • Not on ContractOverrides, precisely because Vault implements that trait.

2. Capped becomes a decorator contract type Capped<T>:

  • MintOverrides for Capped<T>: check_cap(e, amount, T::total_supply(e)), then T::mint(e, to, amount).
  • ContractOverrides, BurnableOverrides, TotalSupplyOverrides, AllowListContractType, BlockListContractType delegate to / forward from T.
  • Compose<(AllowList, Capped, TotalSupply)> resolves to Capped<TotalSupplyAllowList>; the fold wraps the finalized type, order-insensitively. The bound T: TotalSupplyOverrides + MintOverrides replaces Capped's NeedsSupply<Nil> contribution and its dedicated error rows.
  • New marker CappedContractType, implemented for Capped<T>, and FungibleCapped: FungibleTotalSupply + FungibleToken<ContractType: CappedContractType>, so cap() can only be exposed by a contract type that enforces it.
  • Capped::mint is removed. set_cap, query_cap, check_cap stay as free functions.

3. Contract author usage becomes uniform:

use stellar_tokens::fungible::MintOverrides;

#[only_owner]
pub fn mint(e: &Env, to: Address, amount: i128) {
    <Self as FungibleToken>::ContractType::mint(e, &to, amount);
}

The import is required: a trait-only method on Self::ContractType does not resolve from an inherent impl without the trait in scope (verified). The overrides docs currently state "no need to import it"; that line goes, and the fungible-pausable example already imports ContractOverrides for the same reason.

4. Docs and examples: fungible-blocklist and fungible-capped switch to the contract-type path; Base::mint and total_supply module docs point to Self::ContractType::mint as the only recommended minting path.

Non-goals
  • mint on the public FungibleToken trait. The entry-point signature legitimately varies per contract (RWAToken::mint takes operator, vaults mint against assets); only the internal primitive is uniform.
  • Capped<RWA>. RWA::batch_mint mints through its own path, so a cap on RWA would be bypassable; RWA keeps the explicit check_cap in the body.
  • Capping vault deposits. Vault has no MintOverrides, so any list with Vault and Capped is rejected at compile time.
Acceptance criteria
  • Self::ContractType::mint compiles for every valid Compose list without Vault.
  • Compose<(Capped, TotalSupply)>, (AllowList, Capped, TotalSupply), (BlockList, Capped, TotalSupply) and (AllowList, BlockList, Capped, TotalSupply) resolve to Capped<…> in any order and enforce the cap on mint (tests per combination).
  • Compose<(Capped,)>, (FungibleVotes, Capped) and (Vault, Capped, TotalSupply) are rejected at compile time with the existing dedicated messages.
  • FungibleCapped cannot be implemented on a contract type that is not Capped<_>.
  • NeedsSupply rows specific to Capped are removed; RWA/Vault handling is unchanged by this issue.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with fungible/overrides.rs and trace MintOverrides, Capped, Compose, and the existing contract-type implementations. Review the acceptance criteria and test each valid combination for cap enforcement, then verify the listed invalid combinations fail at compile time with the expected messages. Done means mint dispatch is uniform, capped compositions enforce limits, and the documented examples use the contract-type path.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
blockchain
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.