Skip to content

Mintlock

Soroban Mintlock
#![no_std]
 
use soroban_sdk::{
    contract, contractimpl, contractclient, contracterror, contracttype, Address, Env, IntoVal, log
};
 
#[allow(dead_code)]
#[contractclient(name = "MintClient")]
trait MintInterface {
    fn mint(env: Env, to: Address, amount: i128);
}
 
#[contracterror]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u32)]
pub enum Error {
    NotAuthorizedMinter = 1,
    DailyLimitInsufficient = 2,
    NegativeAmount = 3,
}
 
#[contracttype]
pub enum StorageKey {
    /// Admin. Value is an Address.
    Admin,
    /// Minters are stored, keyed by the contract and minter
    /// addresses. Value is a MinterConfig.
    Minter(Address, Address),
    /// Minters stats are stored, keyed by the contract and minter
    /// addresses, epoch length, and epoch, which is the ledger
    /// number divided by the number of ledgers in the epoch.
    /// Value is a MinterStats.
    MinterStats(Address, Address, u32, u32),
}
 
#[contracttype]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MinterConfig {
    limit: i128,
    epoch_length: u32,
}
 
#[contracttype]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct MinterStats {
    consumed_limit: i128,
}
 
#[contract]
pub struct Contract;
 
#[contractimpl]
impl Contract {
    /// Set the admin.
    pub fn set_admin(env: Env, new_admin: Address) {
        if let Some(admin) = env
            .storage()
            .instance()
            .get::<_, Address>(&StorageKey::Admin)
        {
            /// @dev should remove logs before deploying smart contracts
            log!(&env, "Admin address: {}", admin);
 
            admin.require_auth();
        };
 
        /// @dev should remove logs before deploying smart contracts
        log!(&env, "Admin address: {}", new_admin);
 
        env.storage().instance().set(&StorageKey::Admin, &new_admin);
    }
 
    /// Return the admin address.
    pub fn admin(env: Env) -> Address {
        env.storage()
            .instance()
            .get::<_, Address>(&StorageKey::Admin)
            .unwrap()
    }
 
    /// Set the config for a minter for the given contract. Requires admin authentication.
    pub fn set_minter(env: Env, contract: Address, minter: Address, config: MinterConfig) {
        Self::admin(env.clone()).require_auth();
 
        env.storage()
            .persistent()
            .set(&StorageKey::Minter(contract, minter), &config);
    }
 
    /// Returns the config, current epoch, and current epoch stats for a minter.
    pub fn minter(
        env: Env,
        contract: Address,
        minter: Address,
    ) -> Result<(MinterConfig, u32, MinterStats), Error> {
        // The config value
        let config = env
            .storage()
            .persistent()
            .get::<_, MinterConfig>(&StorageKey::Minter(contract.clone(), minter.clone()))
            .ok_or(Error::NotAuthorizedMinter)?;
 
        // The current epoch value
        let epoch = env.ledger().sequence() / config.epoch_length;
 
        // The current epoch's stats
        let stats = env
            .storage()
            .temporary()
            .get::<_, MinterStats>(&StorageKey::MinterStats(
                contract.clone(),
                minter.clone(),
                config.epoch_length,
                epoch,
            ))
            .unwrap_or_default();
 
        /// @dev should remove logs before deploying smart contracts
        log!(&env, "Minter stats: {}, Config: {}, Epoch: {}", stats, config, epoch);
 
        Ok((config, epoch, stats))
    }
 
    /// Calls the 'mint' function of the contract with 'to' and 'amount'.
    /// Authorized by the 'minter'. Uses some of the authorized minter's current epoch's limit.
    pub fn mint(
        env: Env,
        contract: Address,
        minter: Address,
        to: Address,
        amount: i128,
    ) -> Result<(), Error> {
        // Verify minter is authenticated, by providing authorizing args.
        minter.require_auth_for_args((&contract, &to, amount).into_val(&env));
 
        // Verify amount is positive.
        if amount < 0 {
            return Err(Error::NegativeAmount);
        }
 
        // Verify minter is authorized by contract.
        let admin = Self::admin(env.clone());
 
        if admin != minter {
            let Some(admin) = env
                .storage()
                .persistent()
                .get::<_, MinterConfig>(&StorageKey::Minter(contract.clone(), minter.clone()))
            else {
                return Err(Error::NotAuthorizedMinter);
            };
 
            // Check and track daily limit.
            let config = env
                .storage()
                .persistent()
                .get::<_, MinterConfig>(&StorageKey::Minter(contract.clone(), minter.clone()))
                .ok_or(Error::NotAuthorizedMinter)?;
 
            let epoch = env.ledger().sequence() / config.epoch_length;
 
            let minter_stats_key = StorageKey::MinterStats(
                contract.clone(),
                minter.clone(),
                config.epoch_length,
                epoch,
            );
 
            let minter_stats = env
                .storage()
                .temporary()
                .get::<_, MinterStats>(&minter_stats_key)
                .unwrap_or_default();
 
            /// @dev should remove logs before deploying smart contracts
            log!(&env, "Minter stats: {}, Config: {}, Epoch: {}", minter_stats, config, epoch);
 
            let new_minter_stats = MinterStats {
                consumed_limit: minter_stats.consumed_limit + amount,
            };
 
            if new_minter_stats.consumed_limit > config.limit {
                return Err(Error::DailyLimitInsufficient);
            };
 
            env.storage()
                .temporary()
                .set::<_, MinterStats>(&minter_stats_key, &new_minter_stats);
 
            env.storage()
                .temporary()
                .extend_ttl(&minter_stats_key, 0, epoch * config.epoch_length);
        }
 
        // Perform the mint.
        let client = MintClient::new(&env, &contract);
        client.mint(&to, &amount);
 
        Ok(())
    }
}

Explanation

#![no_std] This attribute prevents linking to the standard library, making the code lighter and more efficient for Soroban contracts. It's big so we save on size.

use soroban_sdk::{contract, contractimpl, Env, log} Imports stuffs from the Soroban SDK. Env is basic Soroban type, we need it because we can't use the Rust standard library.

MintInterface (marked with #[allow(dead_code)]): This defines an interface for a possible external contract named "MintClient" that can be called for minting functionality.

Error enum: Defines different error codes for the contract, like NotAuthorizedMinter, DailyLimitInsufficient, and NegativeAmount.

StorageKey enum: Defines different keys used to access data in the contract's storage. These include Admin to store the admin address, Minter(contract, minter) to store a minter's configuration for a specific contract, and MinterStats(contract, minter, epoch_length, epoch) to store a minter's stats for a specific epoch.

MinterConfig Defines the configuration for a minter, including their daily limit and the epoch length for tracking usage.

MinterStats Stores the amount a minter has minted within the current epoch.

set_admin This function sets the contract's admin address. Only the current admin can call this function.

admin This function returns the current admin address.

set_minter This function sets the configuration for a minter for a specific contract. Only the admin can call this function.

minter This function retrieves the configuration, current epoch, and current epoch stats for a specific minter.

mint This is the core function for minting. It allows authorized minters to mint a specific amount for a recipient.

Run in Playground

Loading playground...