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 the StakingVault 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() and unpause().

  • 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); else IncompatibleDecimals().

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)

  • 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 by Whitelist)

  • event ComplianceCheckerUpdated(address indexed newChecker); (emitted by Whitelist)

Errors

  • AddressCantBeZero()

  • IncompatibleDecimals()

  • AddressNotWhitelisted() (from Whitelist.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

  1. Mints amount of hilSyntheticToken to this Minter.

  2. totalDeposits += amount;

  3. 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, and transferToCustody (via whenNotPaused).


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

  1. Pull amount of baseAsset from msg.senderthis.

  2. Mint amount of hilSyntheticToken to to.

  3. totalDeposits += amount;

  4. emit Minted(to, amount);

Preconditions

  • Caller and to must pass Whitelist.isAddressWhitelisted:

    • true if manualWhitelist[user] is set, else complianceChecker.isCompliant(user).


redeem (user redemption)

function redeem(uint256 amount)
  external onlyWhitelisted(msg.sender) nonReentrant whenNotPaused

Flow

  1. Burn amount of hilSyntheticToken from msg.sender via burnFrom.

    • Caller must have set allowance for Minter if token enforces allowance on burnFrom.

  2. Transfer amount of baseAsset to msg.sender.

  3. totalDeposits -= amount;

  4. emit Redeemed(msg.sender, amount);

Last updated