This document is the practical, technical guide for how the BBS Coin Foundation
actually operates contracts on-chain.
No DAO theory, no governance hype — just how adults run smart contracts with a multisig.
On-chain, the Foundation is the Safe.
There is no special Foundation contract, role, or magic address.
Whoever can execute transactions from the Safe controls Foundation authority.
Key implication:
- If the Safe owns BbsTreasury, then only Safe-approved transactions can:
- mint tokens
- deploy distributors
- fund distributors
- enable / disable distributors
- perform emergency actions
Everything else is off-chain process and human coordination.
A Safe transaction has four real stages:
to, value, and dataImportant:
- Creating a transaction ≠ executing it
- Approving ≠ executing
- Nothing happens on-chain until execution
BbsTreasuryThose responsibilities belong to sysops and their tooling.
A transaction is:
- to address
- value (usually 0)
- data (ABI-encoded function call)
Call data is just the encoded function call.
Safe executes transactions — not “functions”.
You must always know:
- what contract you are calling
- which function
- with what parameters
- what state change that causes
There are two supported paths.
Use Safe’s Contract Interaction feature:
- Paste or select contract ABI
- Choose function
- Enter parameters
- Safe builds the call data
Pros:
- No scripting
- Harder to screw up encoding
Cons:
- Manual
- Slower at scale
Use ethers to generate call data locally, then paste into Safe.
Example pattern:
import { ethers } from "ethers";
const abi = [
"function fundDistributor(address distributor, uint256 amount)"
];
const iface = new ethers.Interface(abi);
const data = iface.encodeFunctionData(
"fundDistributor",
["0xDistributor", 500n * 10n**18n]
);
console.log(data);
Safe inputs:
- to: BbsTreasury address
- value: 0
- data: output from script
Pros:
- Auditable
- Repeatable
- Commit scripts to Git
Below are real actions the Foundation will take, mapped directly to Solidity.
Contract:
- BbsTreasury
Function:
function setToken(address token_)
When:
- After deploying BbsToken
Safe transaction:
- to: BbsTreasury
- value: 0
- data: setToken(tokenAddress)
Failure conditions:
- Reverts if already set
Contract:
- BbsToken
Functions:
function MINTER_ROLE() external view returns (bytes32)
function grantRole(bytes32 role, address account)
When:
- After Treasury deployment
- Before minting
Effect:
- Allows Treasury to mint tokens
Contract:
- BbsTreasury
Function:
function mintToTreasury(uint256 amount)
When:
- Treasury needs supply to fund distributors
Important:
- amount is in base units (18 decimals)
Contract:
- BbsTreasury
Function:
function deployDistributor(
address sysopOwner,
string bbsId,
string metadataURI
)
What this does:
- Deploys BbsMerkleDistributor
- Registers it in DistributorRegistry
- Enables it for funding
Output:
- Distributor address emitted in DistributorDeployed event
Contract:
- BbsTreasury
Function:
function fundDistributor(address distributor, uint256 amount)
Checks:
- Distributor must be enabled
- Treasury must have sufficient balance
Contract:
- BbsTreasury
Function:
function setDistributorEnabled(address distributor, bool enabled)
Use cases:
- Sysop key compromised
- Abuse
- Temporary suspension
Contract:
- BbsTreasury → BbsMerkleDistributor
Function chain:
BbsTreasury.revokeFromDistributor(...)
→ BbsMerkleDistributor.revokeToTreasury(...)
Purpose:
- Pull unclaimed tokens back to Treasury
Before approving a Safe transaction:
to address matches canonical contractvalue == 0If any box is unchecked, do not approve.
If these blur, bugs and blame follow.
Maintain a simple log per chain:
- Date
- Safe tx ID
- On-chain tx hash
- Operation type
- Parameters
- Link to explorer
- Notes
This matters more than clever tooling.
The Safe is not a convenience.
It is the constitution of the Foundation.
If it’s not executed by the Safe,
it didn’t happen.