Join our community of builders on

Telegram!Telegram

SAC Admin Generic

Source Code

Overview

The Stellar Asset Contract (SAC) Admin Generic module provides a way to implement custom administrative functionality for Stellar Asset Contracts (SACs) using the generic approach. This approach leverages the __check_auth function to handle authentication and authorization logic while maintaining a unified interface for both user-facing and admin functions.

Key Concepts

When a classic Stellar asset is ported to Soroban, it is represented by a SAC - a smart contract that provides both user-facing and administrative functions for asset management. SACs expose standard functions for handling fungible tokens, such as transfer, approve, burn, etc. Additionally, they include administrative functions (mint, clawback, set_admin, set_authorized) that are initially restricted to the issuer (a G-account).

The set_admin function enables transferring administrative control to a custom contract, allowing for more complex authorization logic. This flexibility opens up possibilities for implementing custom rules, such as role-based access control, two-step admin transfers, mint rate limits, and upgradeability.

Generic Approach

The Generic approach to SAC Admin implementation:

  • Leverages the __check_auth function to handle authentication and authorization logic
  • Maintains a unified interface for both user-facing and admin functions
  • Allows for injecting any custom authorization logic
  • Requires a more sophisticated authorization mechanism

Example Implementation

Here’s a simplified example of a SAC Admin Generic contract:

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum SACAdminGenericError {
    Unauthorized = 1,
    InvalidContext = 2,
    MintingLimitExceeded = 3,
}

#[contracttype]
#[derive(Clone)]
pub struct Signature {
    pub public_key: BytesN<32>,
    pub signature: BytesN<64>,
}

#[contracttype]
pub enum SacDataKey {
    Chief,
    Operator(BytesN<32>),     // -> true/false
    MintingLimit(BytesN<32>), // -> (max_limit, curr)
}

#[contract]
pub struct SacAdminExampleContract;

#[contractimpl]
impl SacAdminExampleContract {
    pub fn __constructor(e: Env, sac: Address, chief: BytesN<32>, operator: BytesN<32>) {
        set_sac_address(&e, &sac);
        e.storage().instance().set(&SacDataKey::Chief, &chief);
        e.storage().instance().set(&SacDataKey::Operator(operator.clone()), &true);
        e.storage()
            .instance()
            .set(&SacDataKey::MintingLimit(operator), &(1_000_000_000i128, 0i128));
    }

    pub fn get_sac_address(e: &Env) -> Address {
        get_sac_address(e)
    }
}

Custom Authorization Logic

The key feature of the Generic approach is the ability to implement custom authorization logic in the __check_auth function:

use soroban_sdk::{
    auth::Context, CustomAccountInterface,
    contract, contracterror, contractimpl, contracttype,
    crypto::Hash,
    Address, BytesN, Env, IntoVal, Val, Vec,
};

#[contractimpl]
impl CustomAccountInterface for SacAdminExampleContract {
    type Error = SACAdminGenericError;
    type Signature = Signature;

    fn __check_auth(
        e: Env,
        payload: Hash<32>,
        signature: Self::Signature,
        auth_context: Vec<Context>,
    ) -> Result<(), SACAdminGenericError> {
        // authenticate
        e.crypto().ed25519_verify(
            &signature.public_key,
            &payload.clone().into(),
            &signature.signature,
        );
        let caller = signature.public_key.clone();

        // extract from context and check required permissions for every function
        for ctx in auth_context.iter() {
            let context = match ctx {
                Context::Contract(c) => c,
                _ => return Err(SACAdminGenericError::InvalidContext),
            };

            match extract_sac_contract_context(&e, &context) {
                SacFn::Mint(amount) => {
                    // ensure caller has required permissions
                    ensure_caller_operator(&e, &SacDataKey::Operator(caller.clone()))?;
                    // ensure operator has minting limit
                    ensure_minting_limit(&e, &caller, amount)?;
                }
                SacFn::Clawback(_amount) => {
                    // ensure caller has required permissions
                    ensure_caller_operator(&e, &SacDataKey::Operator(caller.clone()))?;
                }
                SacFn::SetAuthorized(_) => {
                    // ensure caller has required permissions
                    ensure_caller_operator(&e, &SacDataKey::Operator(caller.clone()))?;
                }
                SacFn::SetAdmin => {
                    // ensure caller has required permissions
                    ensure_caller_chief(&e, &caller, &SacDataKey::Chief)?;
                }
                SacFn::Unknown => {
                    // ensure only chief can call other functions
                    ensure_caller_chief(&e, &caller, &SacDataKey::Chief)?
                }
            }
        }

        Ok(())
    }
}

// Helper functions
fn ensure_caller_chief<K: IntoVal<Env, Val>>(
    e: &Env,
    caller: &BytesN<32>,
    key: &K,
) -> Result<(), SACAdminGenericError> {
    let operator: BytesN<32> = e.storage().instance().get(key).expect("chief or operator not set");
    if *caller != operator {
        return Err(SACAdminGenericError::Unauthorized);
    }
    Ok(())
}

fn ensure_caller_operator<K: IntoVal<Env, Val>>(
    e: &Env,
    key: &K,
) -> Result<(), SACAdminGenericError> {
    match e.storage().instance().get::<_, bool>(key) {
        Some(is_op) if is_op => Ok(()),
        _ => Err(SACAdminGenericError::Unauthorized),
    }
}

Benefits and Trade-offs

Benefits

  • Maintains a unified interface for both user-facing and admin functions
  • Allows for complex authorization logic
  • Provides flexibility in implementing custom rules

Trade-offs

  • Requires a more sophisticated authorization mechanism
  • More complex to implement compared to the wrapper approach
  • Requires understanding of the Soroban authorization system

Full Example

A complete example implementation can be found in the sac-admin-generic example.

See Also