Skip to main content

YB token

Overview​

YB is the ERC-20 governance token of YieldBasis. The contract adds reserve-based emissions with exponential decay to the standard ERC-20 functions. A minter calls emit(), and the contract mints an amount set by the elapsed time and a rate factor. The rate factor (rate_factor) is the fraction of the maximum emission rate, from 0 to 1. The code stores it on a 1e18 scale (10^18 = 100%).

Minting works through two paths:

  • emit() mints only from reserve, and it reduces reserve by the same amount.
  • mint(owner, amount), exported from snekmate erc20, lets any minter mint. It is not bound by reserve.

The GaugeController is the contract that distributes YB emissions to gauges by veYB vote. Ownership is renounced, so no new minter can be granted. The GaugeController holds the minter role and mints only through emit(), which is bound by the reserve. Verify the full minter set from the token's RoleMinterChanged events. To check one address, call is_minter(address).

For the supply identity (pre-allocations plus the emission reserve, 1,000,000,000 YB), read Supply structure in User: YB token. If no other minter exists, reserve() plus totalSupply() stays at that cap.

The emission clock starts at the first call by the owner to renounce_ownership() or start_emissions(). Ownership is renounced, so the clock has started.

Public variables​

The contract has these emission-state variables:

  • reserve() (uint256) - Remaining tokens that emit() can mint, in raw YB units (18 decimals).
  • last_minted() (uint256) - Unix timestamp of the last mint through emit(). It is 0 until the emission clock starts.
  • max_mint_rate() (uint256, immutable) - Fraction of the reserve emitted per second at rate_factor = 1.0, where 1e18 = 100%.

The constructor computes max_mint_rate from deployment arguments as max_rate × 10^18 // reserve. On mainnet the constructor arguments give max_mint_rate ≈ 7.93 × 10⁻⁹ per second. At rate_factor = 1, the reserve decays with a 4-year time constant: about 22% of the remaining reserve each year. Read the deployed value with YB.max_mint_rate(). For example, a raw value of 8,000,000,000 reads as 8 × 10⁻⁹ of the reserve per second.

Functions​

Each function section lists its own revert strings. The shared revert table for all yb-core contracts is in Error codes.

Ownership and minting functions​

The ownership and minting functions come from snekmate ownable and erc20. After renounce_ownership(), owner() is the zero address, so every owner-only call reverts. In mint(owner, amount), owner is the address that gets the tokens, not the contract owner.

The table gives who can call each function and its revert strings:

FunctionWho can callReverts
owner()Anyone (view)None
is_minter(address)Anyone (view). Returns True if the address holds the minter role.None
mint(owner, amount)A mintererc20: access is denied (caller is not a minter); erc20: mint to the zero address
set_minter(minter, status)The ownerownable: caller is not the owner; erc20: minter is the zero address; erc20: minter is owner address
transfer_ownership(new_owner)The ownerownable: caller is not the owner; erc20: new owner is the zero address

_emissions() (internal)​

Internal function that calculates the amount of tokens to emit from the elapsed time and the rate factor. It uses an exponential decay formula, which Exponential decay formula gives.

The function returns the amount of tokens to emit, as a uint256.

The function takes these inputs:

InputTypeDescription
tuint256Target timestamp for emission calculation
rate_factoruint256Rate factor (100% = 10^18)

_emissions() reverts without a message when rate_factor is above 10^18.

Contract source: _emissions()

The contract source gives this function body:

@internal
@view
def _emissions(t: uint256, rate_factor: uint256) -> uint256:
assert rate_factor <= 10**18
last_minted: uint256 = self.last_minted
if last_minted == 0 or t <= last_minted:
return 0
else:
dt: int256 = convert(t - last_minted, int256)
rate_36: int256 = convert(max_mint_rate * rate_factor, int256)
reserve: int256 = convert(self.reserve, int256)
return convert(
reserve * (10**18 - math._wad_exp(-dt * rate_36 // 10**18)) // 10**18,
uint256)

preview_emissions()​

View function that previews the emission for a timestamp and a rate factor.

This is the token-level preview_emissions(). The GaugeController also has a preview_emissions(gauge, at_time) function for per-gauge emission previews.

The function returns the preview of the emission, a uint256 in raw YB units (18 decimals). It returns 0 in two cases: before the emission clock starts, and when t is not after last_minted.

This example assumes reserve = 300,000,000 YB, max_mint_rate = 7.93 × 10⁻⁹ per second, rate_factor = 5 * 10**17 (50%), and t one day (86,400 s) after last_minted. The exponent is 86,400 × 7.93 × 10⁻⁹ × 0.5 = 3.4258 × 10⁻⁴. The call returns 300,000,000 × (1 − e^(−3.4258 × 10⁻⁴)) ≈ 102,755 YB, a raw value of about 1.02755 × 10^23.

The function takes these inputs:

InputTypeDescription
tuint256Target timestamp for emission calculation
rate_factoruint256Rate factor (100% = 10^18)

preview_emissions() reverts without a message when rate_factor is above 10^18.

Contract source: preview_emissions()

The contract source gives this function body:

@external
@view
def preview_emissions(t: uint256, rate_factor: uint256) -> uint256:
return self._emissions(t, rate_factor)

start_emissions()​

Starts the emission clock by setting the initial timestamp. Only the owner can call it.

start_emissions() reverts with ownable: caller is not the owner when the caller is not the owner.

Contract source: start_emissions()

The contract source gives this function body:

@external
def start_emissions():
ownable._check_owner()
if self.last_minted == 0:
self.last_minted = block.timestamp

renounce_ownership()​

Renounces ownership. If the emission clock has not started, the function starts it. It also removes the caller as a minter. After this call, nobody can grant the minter role.

The function emits RoleMinterChanged when it removes the caller as a minter.

renounce_ownership() reverts with ownable: caller is not the owner when the caller is not the owner.

Contract source: renounce_ownership()

The contract source gives this function body:

@external
def renounce_ownership():
ownable._check_owner()
# Force-start emissions when renouncing ownership
if self.last_minted == 0:
self.last_minted = block.timestamp
erc20.is_minter[msg.sender] = False
log erc20.RoleMinterChanged(minter=msg.sender, status=False)
ownable._transfer_ownership(empty(address))

emit()​

Mints tokens to the address owner, based on the elapsed time and the rate factor. The input owner is the recipient of the tokens, not the contract owner. Only a minter can call it.

The function returns the amount of tokens minted, a uint256 in raw YB units (18 decimals). It returns 0 before the emission clock starts.

The function emits Transfer for the minted tokens, from address(0) to owner.

The function takes these inputs:

InputTypeDescription
owneraddressAddress to receive the minted tokens
rate_factoruint256Rate factor (100% = 10^18)

emit() reverts in these cases:

  • erc20: access is denied - The caller is not a minter.
  • erc20: mint to the zero address - owner is the zero address and the emission clock has started.
  • Without a message - rate_factor is above 10^18 and the emission clock has started.
Contract source: emit()

The contract source gives this function body:

@external
def emit(owner: address, rate_factor: uint256) -> uint256:
assert erc20.is_minter[msg.sender], "erc20: access is denied"

amount: uint256 = 0

if self.last_minted > 0:
amount = self._emissions(block.timestamp, rate_factor)
self.reserve -= amount
self.last_minted = block.timestamp
erc20._mint(owner, amount)

return amount

Emission mechanics​

Exponential decay formula​

_emissions() calculates each emission with an exponential decay formula. In this form of the formula, rate_factor is a fraction from 0 to 1. The preview_emissions() section gives a worked example.

emission = reserve × (1 − e^(−dt × rate × rate_factor))

The formula uses these terms:

  • dt = time in seconds since the last mint
  • rate = max_mint_rate / 10^18 (max_mint_rate is immutable)
  • rate_factor = fraction of the max rate, from 0 to 1 (the code stores it as 0 to 10^18)
  • reserve = remaining tokens available

In the code, rate_factor ∈ [0, 10^18]. Both max_mint_rate and rate_factor use a 1e18 scale, so the code form of the formula divides each by 10^18:

amount = reserve × (1 − exp(−dt × (max_mint_rate / 10^18) × (rate_factor / 10^18)))

The code passes the exponent to _wad_exp in 1e18 units.

Rate factor​

rate_factor sets the fraction of the maximum rate at which emit() mints. The code stores it on a 1e18 scale:

  • rate_factor = 10^18: 100% of the maximum rate.
  • rate_factor = 5 * 10^17: 50% of the maximum rate.
  • rate_factor = 0: 0% of the maximum rate.

The GaugeController calculates rate_factor from the gauge weights. For the calculation, read Emission distribution in GaugeController.

GaugeController as the minter​

In YieldBasis deployments, GaugeController has the minter role. It mints to itself and then distributes the YB to gauges. It controls the emission rate through the rate_factor input of emit(). The GaugeController does not run proposals. Proposals run on the YB Aragon DAO. The token contract is generic and enforces erc20.is_minter[address] permissions.

Emission timeline​

Emissions go through these stages in order:

  1. At deployment, the constructor sets reserve and max_mint_rate (max_mint_rate is immutable).
  2. The emission clock starts when the owner calls renounce_ownership() or start_emissions().
  3. After the clock starts, minters call emit() to mint tokens.

The emission rate decreases exponentially over time. The reserve approaches 0 but does not reach it. Cumulative emissions approach the reserve set at deployment. If no other minter exists, total supply approaches 1,000,000,000 YB.