Minter
Purpose
Minter
is the bridge/orchestrator that:
Accepts baseAsset (e.g., WBTC) deposits and mints the synthetic token (
hilSyntheticToken
) 1:1.Burns
hilSyntheticToken
and pays back the baseAsset on redemption.Distributes vault yield by minting
hilSyntheticToken
to itself and letting theStakingVault
pull it (via prior approval) into the vault, where it vests into the share price.Enforces compliance (via
Whitelist
+IComplianceChecker
), role-gated ops, and pausing.
Imports
import {IMinter} from "../interfaces/minter/IMinter.sol";
import {IStakingVault} from "../interfaces/vault/IStakingVault.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20//ERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IMintableERC20} from "../interfaces/token/IMintableERC20.sol";
import {Whitelist} from "../helpers/Whitelist.sol";
import {IVerificationSBT} from "../interfaces/galactica/IVerificationSBT.sol";
import {IComplianceChecker} from "../interfaces/galactica/IComplianceChecker.sol";
Roles
bytes32 public constant CUSTODIAN_ROLE = keccak256("CUSTODIAN_ROLE");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant DISTRIBUTOR_ROLE= keccak256("DISTRIBUTOR_ROLE");
DEFAULT_ADMIN_ROLE – master admin; can rotate role addresses and update the compliance checker and manual whitelist.
OPERATOR_ROLE – allowed to move baseAsset from the Minter to
custodian
(transferToCustody
).PAUSER_ROLE – can
pause()
andunpause()
.DISTRIBUTOR_ROLE – can initiate vault yield distributions.
CUSTODIAN_ROLE – address identity for custody (not used as a caller gate in current functions, but rotated and emitted).
Storage
IERC20 public immutable baseAsset;
IMintableERC20 public immutable hilSyntheticToken;
address public custodian;
address public operator;
address public pauser;
address public distributor;
IStakingVault public stakingVault;
uint256 public totalDeposits; // accounting var: ++ on mint/ownerMint/distributeYield, -- on redeem
Notes on storage semantics
Decimals check (constructor):
IERC20Metadata(hilSyntheticToken).decimals() == IERC20Metadata(baseAsset).decimals()
is enforced.Approval: In the constructor, Minter calls
IERC20(_hilSyntheticToken).approve(address(_stakingVault), type(uint256).max);
so the vault can pull minted yield from the Minter during
StakingVault.distributeYield(...)
.totalDeposits
accounting:Increments on:
mint
,ownerMint
,distributeYield
.Decrements on:
redeem
.Not changed by
transferToCustody
. This variable reflects net issues/burns from Minter’s perspective, not the live baseAsset balance held at the contract.
Constructor
constructor(
address _initialAdmin,
address _baseAsset,
address _hilSyntheticToken,
address _complianceChecker,
address _custodian,
address _operator,
address _pauser,
address _distributor,
IStakingVault _stakingVault
)
Requirements
All addresses must be non-zero; else
AddressCantBeZero()
.decimals(hilSyntheticToken) == decimals(baseAsset)
; elseIncompatibleDecimals()
.
Effects
Sets immutable token refs, installs compliance checker, role holders, and
stakingVault
.Grants roles:
DEFAULT_ADMIN_ROLE
→_initialAdmin
CUSTODIAN_ROLE
→_custodian
OPERATOR_ROLE
→_operator
PAUSER_ROLE
→_pauser
DISTRIBUTOR_ROLE
→_distributor
Approves the vault to spend unlimited
hilSyntheticToken
from the Minter (used during yield distribution).
Events (from IMinter
and Whitelist
)
IMinter
and Whitelist
)event Minted(address indexed to, uint256 amount);
event Redeemed(address indexed user, uint256 amount);
event FundsTransferredToCustody(uint256 amount, address indexed custodian);
event AddressWhitelisted(address indexed user, bool allowed);
(emitted byWhitelist
)event ComplianceCheckerUpdated(address indexed newChecker);
(emitted byWhitelist
)
Errors
AddressCantBeZero()
IncompatibleDecimals()
AddressNotWhitelisted()
(fromWhitelist.onlyWhitelisted
)
Public / External
ownerMint
function ownerMint(address addressTo, uint256 amount)
external onlyRole(DEFAULT_ADMIN_ROLE)
What it does
Mints amount
of hilSyntheticToken
directly to addressTo
.
totalDeposits += amount;
Emits Minted(addressTo, amount)
.
When to use Admin pathway for mints associated with off-chain baseAsset inflows (e.g., native BTC deposit processed by operations).
distributeYield
function distributeYield(uint256 amount, uint256 timestamp)
external onlyRole(DISTRIBUTOR_ROLE)
What it does
Mints
amount
ofhilSyntheticToken
to this Minter.totalDeposits += amount;
Calls
stakingVault.distributeYield(amount, timestamp)
.
How vault pulls
Inside the vault, distributeYield
performs:
IERC20(asset()).safeTransferFrom(msg.sender, address(this), yieldAmount);
Because the Minter pre-approved the vault in the constructor, the vault pulls amount
from the Minter during this call.
Vesting behavior in vault
The vault then sets its internal vesting window (8h) starting at timestamp
(see StakingVault docs). The yield is excluded from totalAssets()
until it vests linearly.
Emits
Minted(address(stakingVault), amount)
(from Minter)
pause / unpause
function pause() public onlyRole(PAUSER_ROLE)
function unpause() public onlyRole(PAUSER_ROLE)
Pausing blocks
mint
,redeem
, andtransferToCustody
(viawhenNotPaused
).
setOperator / setPauser / setCustodian
function setOperator(address newOperator) external onlyRole(DEFAULT_ADMIN_ROLE)
function setPauser(address newPauser) external onlyRole(DEFAULT_ADMIN_ROLE)
function setCustodian(address newCustodian) external onlyRole(DEFAULT_ADMIN_ROLE)
Rotate role addresses. Validates non-zero; revokes old role; grants new role; updates stored address.
whitelistAddress (manual whitelist toggle)
function whitelistAddress(address user, bool allowed)
external onlyRole(DEFAULT_ADMIN_ROLE)
Delegates to _whitelistAddress
in Whitelist
. Emits AddressWhitelisted(user, allowed)
.
setComplianceChecker
function setComplianceChecker(address _complianceChecker)
external onlyRole(DEFAULT_ADMIN_ROLE)
Updates the compliance backend used by Whitelist
. Emits ComplianceCheckerUpdated(_complianceChecker)
.
transferToCustody
function transferToCustody(uint256 amount)
external onlyRole(OPERATOR_ROLE) whenNotPaused
Transfers amount
of baseAsset from the Minter to custodian
.
Emits FundsTransferredToCustody(amount, custodian)
.
mint (user deposit)
function mint(address to, uint256 amount)
external
onlyWhitelisted(msg.sender)
onlyWhitelisted(to)
nonReentrant
whenNotPaused
Flow
Pull
amount
of baseAsset frommsg.sender
→this
.Mint
amount
ofhilSyntheticToken
toto
.totalDeposits += amount;
emit Minted(to, amount);
Preconditions
Caller and
to
must passWhitelist.isAddressWhitelisted
:true if
manualWhitelist[user]
is set, elsecomplianceChecker.isCompliant(user)
.
redeem (user redemption)
function redeem(uint256 amount)
external onlyWhitelisted(msg.sender) nonReentrant whenNotPaused
Flow
Burn
amount
ofhilSyntheticToken
frommsg.sender
viaburnFrom
.Caller must have set allowance for Minter if token enforces allowance on burnFrom.
Transfer
amount
of baseAsset tomsg.sender
.totalDeposits -= amount;
emit Redeemed(msg.sender, amount);
Last updated