SAC Admin Generic
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_authfunction 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.