Skip to content

Instantly share code, notes, and snippets.

@larrythecucumber321
Last active July 11, 2024 09:39
Show Gist options
  • Save larrythecucumber321/a836249cc3cea498625668dfbf1ac606 to your computer and use it in GitHub Desktop.
Save larrythecucumber321/a836249cc3cea498625668dfbf1ac606 to your computer and use it in GitHub Desktop.
Proof of Liquidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol";
import { Utils } from "../../libraries/Utils.sol";
import { IBeraChef } from "../interfaces/IBeraChef.sol";
import { IBerachainRewardsVault } from "../interfaces/IBerachainRewardsVault.sol";
import { FactoryOwnable } from "../../base/FactoryOwnable.sol";
import { StakingRewards } from "../../base/StakingRewards.sol";
/// @title Berachain Rewards Vault
/// @author Berachain Team
/// @notice This contract is the vault for the Berachain rewards, it handles the staking and rewards accounting of BGT.
/// @dev This contract is taken from the stable and tested:
/// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol
/// We are using this model instead of 4626 because we want to incentivize staying in the vault for x period of time to
/// to be considered a 'miner' and not a 'trader'.
contract BerachainRewardsVault is
PausableUpgradeable,
ReentrancyGuardUpgradeable,
FactoryOwnable,
StakingRewards,
IBerachainRewardsVault
{
using Utils for bytes4;
using SafeTransferLib for address;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice Struct to hold delegate stake data.
/// @param delegateTotalStaked The total amount staked by delegates.
/// @param stakedByDelegate The mapping of the amount staked by each delegate.
struct DelegateStake {
uint256 delegateTotalStaked;
mapping(address delegate => uint256 amount) stakedByDelegate;
}
/// @notice Struct to hold an incentive data.
/// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission.
/// @param incentiveRate The amount of the token to incentivize per BGT emission.
/// @param amountRemaining The amount of the token remaining to incentivize.
struct Incentive {
uint256 minIncentiveRate;
uint256 incentiveRate;
uint256 amountRemaining;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice The maximum count of incentive tokens that can be stored.
uint8 public maxIncentiveTokensCount;
/// @notice The address of the distributor contract.
address public distributor;
/// @notice The Berachef contract.
IBeraChef public beraChef;
mapping(address account => DelegateStake) internal _delegateStake;
/// @notice The mapping of accounts to their operators.
mapping(address account => address operator) internal _operators;
/// @notice the mapping of incentive token to its incentive data.
mapping(address token => Incentive incentives) public incentives;
/// @notice The list of whitelisted tokens.
address[] public whitelistedTokens;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/// @inheritdoc IBerachainRewardsVault
function initialize(
address _berachef,
address _bgt,
address _distributor,
address _stakingToken
)
external
initializer
{
__FactoryOwnable_init(msg.sender);
__StakingRewards_init(_stakingToken, _bgt, 7 days);
maxIncentiveTokensCount = 3;
// slither-disable-next-line missing-zero-check
distributor = _distributor;
beraChef = IBeraChef(_berachef);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
modifier onlyDistributor() {
if (msg.sender != distributor) NotDistributor.selector.revertWith();
_;
}
modifier onlyOperatorOrUser(address account) {
if (msg.sender != account) {
if (msg.sender != _operators[account]) NotOperator.selector.revertWith();
}
_;
}
modifier checkSelfStakedBalance(address account, uint256 amount) {
_checkSelfStakedBalance(account, amount);
_;
}
modifier onlyWhitelistedToken(address token) {
if (incentives[token].minIncentiveRate == 0) TokenNotWhitelisted.selector.revertWith();
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADMIN FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBerachainRewardsVault
function setDistributor(address _rewardDistribution) external onlyFactoryOwner {
if (_rewardDistribution == address(0)) ZeroAddress.selector.revertWith();
distributor = _rewardDistribution;
emit DistributorSet(_rewardDistribution);
}
/// @inheritdoc IBerachainRewardsVault
function notifyRewardAmount(address coinbase, uint256 reward) external onlyDistributor {
_notifyRewardAmount(reward);
_processIncentives(coinbase, reward);
}
/// @inheritdoc IBerachainRewardsVault
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyFactoryOwner {
if (tokenAddress == address(STAKE_TOKEN)) CannotRecoverStakingToken.selector.revertWith();
tokenAddress.safeTransfer(factoryOwner(), tokenAmount);
emit Recovered(tokenAddress, tokenAmount);
}
/// @inheritdoc IBerachainRewardsVault
function setRewardsDuration(uint256 _rewardsDuration) external onlyFactoryOwner {
_setRewardsDuration(_rewardsDuration);
}
/// @inheritdoc IBerachainRewardsVault
function whitelistIncentiveToken(address token, uint256 minIncentiveRate) external onlyFactoryOwner {
if (minIncentiveRate == 0) MinIncentiveRateIsZero.selector.revertWith();
Incentive storage incentive = incentives[token];
if (whitelistedTokens.length == maxIncentiveTokensCount || incentive.minIncentiveRate != 0) {
TokenAlreadyWhitelistedOrLimitReached.selector.revertWith();
}
whitelistedTokens.push(token);
//set the incentive rate to the minIncentiveRate.
incentive.incentiveRate = minIncentiveRate;
incentive.minIncentiveRate = minIncentiveRate;
emit IncentiveTokenWhitelisted(token, minIncentiveRate);
}
/// @inheritdoc IBerachainRewardsVault
function removeIncentiveToken(address token) external onlyFactoryOwner onlyWhitelistedToken(token) {
delete incentives[token];
// delete the token from the list.
_deleteWhitelistedTokenFromList(token);
emit IncentiveTokenRemoved(token);
}
/// @inheritdoc IBerachainRewardsVault
function setMaxIncentiveTokensCount(uint8 _maxIncentiveTokensCount) external onlyFactoryOwner {
if (_maxIncentiveTokensCount < whitelistedTokens.length) {
InvalidMaxIncentiveTokensCount.selector.revertWith();
}
maxIncentiveTokensCount = _maxIncentiveTokensCount;
emit MaxIncentiveTokensCountUpdated(_maxIncentiveTokensCount);
}
/// @inheritdoc IBerachainRewardsVault
function pause(bool _paused) external onlyFactoryOwner {
if (_paused) {
_pause();
} else {
_unpause();
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* GETTERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBerachainRewardsVault
function operator(address account) external view returns (address) {
return _operators[account];
}
/// @inheritdoc IBerachainRewardsVault
function getWhitelistedTokensCount() external view returns (uint256) {
return whitelistedTokens.length;
}
/// @inheritdoc IBerachainRewardsVault
function getWhitelistedTokens() public view returns (address[] memory) {
return whitelistedTokens;
}
/// @inheritdoc IBerachainRewardsVault
function getTotalDelegateStaked(address account) external view returns (uint256) {
return _delegateStake[account].delegateTotalStaked;
}
/// @inheritdoc IBerachainRewardsVault
function getDelegateStake(address account, address delegate) external view returns (uint256) {
return _delegateStake[account].stakedByDelegate[delegate];
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* WRITES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBerachainRewardsVault
function stake(uint256 amount) external nonReentrant whenNotPaused {
_stake(msg.sender, amount);
}
/// @inheritdoc IBerachainRewardsVault
function delegateStake(address account, uint256 amount) external nonReentrant whenNotPaused {
if (msg.sender == account) NotDelegate.selector.revertWith();
_stake(account, amount);
unchecked {
DelegateStake storage info = _delegateStake[account];
// Overflow is not possible for `delegateTotalStaked` as it is bounded by the `totalSupply` which has
// been incremented in `_stake`.
info.delegateTotalStaked += amount;
// If the total staked by all delegates does not overflow, this increment is safe.
info.stakedByDelegate[msg.sender] += amount;
}
emit DelegateStaked(account, msg.sender, amount);
}
/// @inheritdoc IBerachainRewardsVault
function withdraw(uint256 amount) external nonReentrant checkSelfStakedBalance(msg.sender, amount) {
_withdraw(msg.sender, amount);
}
/// @inheritdoc IBerachainRewardsVault
function delegateWithdraw(address account, uint256 amount) external nonReentrant {
if (msg.sender == account) NotDelegate.selector.revertWith();
unchecked {
DelegateStake storage info = _delegateStake[account];
uint256 stakedByDelegate = info.stakedByDelegate[msg.sender];
if (stakedByDelegate < amount) InsufficientDelegateStake.selector.revertWith();
info.stakedByDelegate[msg.sender] = stakedByDelegate - amount;
// underflow not impossible because `info.delegateTotalStaked` >= `stakedByDelegate` >= `amount`
info.delegateTotalStaked -= amount;
}
_withdraw(account, amount);
emit DelegateWithdrawn(account, msg.sender, amount);
}
/// @inheritdoc IBerachainRewardsVault
/// @dev The operator only handles BGT, not STAKING_TOKEN.
/// @dev If the operator is the one calling this method, the reward will be credited to their address.
function getReward(address account) external nonReentrant onlyOperatorOrUser(account) returns (uint256) {
return _getReward(account, msg.sender);
}
/// @inheritdoc IBerachainRewardsVault
/// @dev Only the account holder can call this function, not the operator.
function exit() external nonReentrant {
uint256 amount = _accountInfo[msg.sender].balance;
_checkSelfStakedBalance(msg.sender, amount);
_withdraw(msg.sender, amount);
_getReward(msg.sender, msg.sender);
}
/// @inheritdoc IBerachainRewardsVault
function setOperator(address _operator) external {
_operators[msg.sender] = _operator;
emit OperatorSet(msg.sender, _operator);
}
/// @inheritdoc IBerachainRewardsVault
function addIncentive(address token, uint256 amount, uint256 incentiveRate) external onlyWhitelistedToken(token) {
Incentive storage incentive = incentives[token];
(uint256 minIncentiveRate, uint256 incentiveRateStored, uint256 amountRemaining) =
(incentive.minIncentiveRate, incentive.incentiveRate, incentive.amountRemaining);
// The incentive amount should be equal to or greater than the `minIncentiveRate` to avoid DDOS attacks.
// If the `minIncentiveRate` is 100 USDC/BGT, the amount should be at least 100 USDC.
if (amount < minIncentiveRate) AmountLessThanMinIncentiveRate.selector.revertWith();
token.safeTransferFrom(msg.sender, address(this), amount);
incentive.amountRemaining = amountRemaining + amount;
// Allows updating the incentive rate if the remaining incentive is less than the `minIncentiveRate` and
// the `incentiveRate` is greater than or equal to the `minIncentiveRate`.
// This will leave some dust but will allow updating the incentive rate without waiting for the
// `amountRemaining` to become 0.
if (amountRemaining <= minIncentiveRate && incentiveRate >= minIncentiveRate) {
incentive.incentiveRate = incentiveRate;
}
// Allows increasing the incentive rate, provided the `amount` suffices to incentivize the same amount of BGT.
// If the current rate is 100 USDC/BGT and the amount remaining is 50 USDC, incentivizing 0.5 BGT,
// then for a new rate of 150 USDC/BGT, the input amount should be at least 0.5 * (150 - 100) = 25 USDC,
// ensuring that it will still incentivize 0.5 BGT.
else if (incentiveRate >= incentiveRateStored) {
uint256 rateDelta;
unchecked {
rateDelta = incentiveRate - incentiveRateStored;
}
if (amount >= FixedPointMathLib.mulDiv(amountRemaining, rateDelta, incentiveRateStored)) {
incentive.incentiveRate = incentiveRate;
}
}
emit IncentiveAdded(token, msg.sender, amount, incentive.incentiveRate);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Check if the account has enough self-staked balance.
/// @param account The account to check the self-staked balance for.
/// @param amount The amount being withdrawn.
function _checkSelfStakedBalance(address account, uint256 amount) internal view {
unchecked {
uint256 balance = _accountInfo[account].balance;
uint256 delegateTotalStaked = _delegateStake[account].delegateTotalStaked;
uint256 selfStaked = balance - delegateTotalStaked;
if (selfStaked < amount) InsufficientSelfStake.selector.revertWith();
}
}
/// @dev The Distributor grants this contract the allowance to transfer the BGT in its balance.
function _safeTransferRewardToken(address to, uint256 amount) internal override {
address(REWARD_TOKEN).safeTransferFrom(distributor, to, amount);
}
// Ensure the provided reward amount is not more than the balance in the contract.
// This keeps the reward rate in the right range, preventing overflows due to
// very high values of rewardRate in the earned and rewardsPerToken functions;
// Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
function _checkRewardSolvency() internal view override {
uint256 allowance = REWARD_TOKEN.allowance(distributor, address(this));
// TODO: change accounting
if (undistributedRewards > allowance) InsolventReward.selector.revertWith();
}
/// @notice process the incentives for a coinbase.
/// @param coinbase The coinbase to process the incentives for.
/// @param bgtEmitted The amount of BGT emitted by the validator.
function _processIncentives(address coinbase, uint256 bgtEmitted) internal {
// If the coinbase has set an operator, the operator will receive the incentives.
// This could be a smart contract or EOA where they can distribute to their delegators or keep if solo.
// This data is stored in the Berachef contract.
// If its not set then the coinbase will receive the incentives.
address _operator = beraChef.getOperator(coinbase);
if (_operator == address(0)) {
_operator = coinbase;
}
uint256 whitelistedTokensCount = whitelistedTokens.length;
unchecked {
for (uint256 i; i < whitelistedTokensCount; ++i) {
address token = whitelistedTokens[i];
Incentive storage incentive = incentives[token];
uint256 amount = FixedPointMathLib.mulDiv(bgtEmitted, incentive.incentiveRate, PRECISION);
uint256 amountRemaining = incentive.amountRemaining;
amount = FixedPointMathLib.min(amount, amountRemaining);
incentive.amountRemaining = amountRemaining - amount;
// slither-disable-next-line arbitrary-send-erc20
token.safeTransfer(_operator, amount); // Transfer the incentive to the operator.
// TODO: avoid emitting events in a loop.
emit IncentivesProcessed(coinbase, token, bgtEmitted, amount);
}
}
}
function _deleteWhitelistedTokenFromList(address token) internal {
uint256 activeTokens = whitelistedTokens.length;
// The length of `whitelistedTokens` cannot be 0 because the `onlyWhitelistedToken` check has already been
// performed.
unchecked {
for (uint256 i; i < activeTokens; ++i) {
if (token == whitelistedTokens[i]) {
whitelistedTokens[i] = whitelistedTokens[activeTokens - 1];
whitelistedTokens.pop();
return;
}
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { LibClone } from "solady/src/utils/LibClone.sol";
import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol";
import { Utils } from "../../libraries/Utils.sol";
import { IBerachainRewardsVaultFactory } from "../interfaces/IBerachainRewardsVaultFactory.sol";
import { BerachainRewardsVault } from "./BerachainRewardsVault.sol";
/// @title BerachainRewardsVaultFactory
/// @author Berachain Team
/// @notice Factory contract for creating BerachainRewardsVaults and keeping track of them.
contract BerachainRewardsVaultFactory is IBerachainRewardsVaultFactory, OwnableUpgradeable, UUPSUpgradeable {
using Utils for bytes4;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice The beacon address.
address public beacon;
/// @notice The BGT token address.
address public bgt;
/// @notice The distributor address.
address public distributor;
/// @notice The Berachef address.
address public berachef;
/// @notice Mapping of staking token to vault address.
mapping(address stakingToken => address vault) public getVault;
/// @notice Array of all vaults that have been created.
address[] public allVaults;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
address _bgt,
address _distributor,
address _berachef,
address _governance,
address _vaultImpl
)
external
initializer
{
__Ownable_init(_governance);
// slither-disable-next-line missing-zero-check
bgt = _bgt;
// slither-disable-next-line missing-zero-check
distributor = _distributor;
// slither-disable-next-line missing-zero-check
berachef = _berachef;
beacon = address(new UpgradeableBeacon(_governance, _vaultImpl));
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADMIN */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VAULT CREATION */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBerachainRewardsVaultFactory
function createRewardsVault(address stakingToken) external returns (address) {
if (getVault[stakingToken] != address(0)) VaultAlreadyExists.selector.revertWith();
// Use solady library to deploy deterministic beacon proxy.
bytes32 salt;
assembly ("memory-safe") {
mstore(0, stakingToken)
salt := keccak256(0, 0x20)
}
address vault = LibClone.deployDeterministicERC1967BeaconProxy(beacon, salt);
// Store the vault in the mapping and array.
getVault[stakingToken] = vault;
allVaults.push(vault);
emit VaultCreated(stakingToken, vault);
// Initialize the vault.
BerachainRewardsVault(vault).initialize(berachef, bgt, distributor, stakingToken);
return vault;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* READS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBerachainRewardsVaultFactory
function predictRewardsVaultAddress(address stakingToken) external view returns (address) {
bytes32 salt;
assembly ("memory-safe") {
mstore(0, stakingToken)
salt := keccak256(0, 0x20)
}
return LibClone.predictDeterministicAddressERC1967BeaconProxy(beacon, salt, address(this));
}
/// @inheritdoc IBerachainRewardsVaultFactory
function allVaultsLength() external view returns (uint256) {
return allVaults.length;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { Utils } from "../../libraries/Utils.sol";
import { IBeraChef } from "../interfaces/IBeraChef.sol";
/// @title BeraChef
/// @author Berachain Team
/// @notice The BeraChef contract is responsible for managing the cutting boards, operators of
/// the validators and the friends of the chef.
/// @dev It should be owned by the governance module.
contract BeraChef is IBeraChef, OwnableUpgradeable, UUPSUpgradeable {
using Utils for bytes4;
/// @dev Represents 100%. Chosen to be less granular.
uint96 internal constant ONE_HUNDRED_PERCENT = 1e4;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice The address of the distributor contract.
address public distributor;
/// @notice The delay in blocks before a new cutting board can go into effect.
uint64 public cuttingBoardBlockDelay;
/// @dev The maximum number of weights per cutting board.
uint8 public maxNumWeightsPerCuttingBoard;
/// @dev Mapping of validator coinbase address to active cutting board.
mapping(address valCoinbase => CuttingBoard) internal activeCuttingBoards;
/// @dev Mapping of validator coinbase address to queued cutting board.
mapping(address valCoinbase => CuttingBoard) internal queuedCuttingBoards;
/// @dev Mapping of validator coinbase address to their operator address.
mapping(address valCoinbase => address operator) internal validatorOperator;
/// @notice Mapping of receiver address to whether they are white-listed as a friend of the chef.
mapping(address receiver => bool) public isFriendOfTheChef;
/// @notice The Default cutting board is used when a validator does not have a cutting board.
CuttingBoard public defaultCuttingBoard;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
address _distributor,
address _governance,
uint8 _maxNumWeightsPerCuttingBoard
)
external
initializer
{
__Ownable_init(_governance);
// slither-disable-next-line missing-zero-check
distributor = _distributor;
if (_maxNumWeightsPerCuttingBoard == 0) {
MaxNumWeightsPerCuttingBoardIsZero.selector.revertWith();
}
emit MaxNumWeightsPerCuttingBoardSet(_maxNumWeightsPerCuttingBoard);
maxNumWeightsPerCuttingBoard = _maxNumWeightsPerCuttingBoard;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
modifier onlyDistributor() {
if (msg.sender != distributor) {
NotDistributor.selector.revertWith();
}
_;
}
modifier onlyValidatorOrOperator(address valCoinbase) {
if (msg.sender != valCoinbase) {
if (msg.sender != validatorOperator[valCoinbase]) {
NotValidatorOrOperator.selector.revertWith();
}
}
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADMIN FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBeraChef
function setMaxNumWeightsPerCuttingBoard(uint8 _maxNumWeightsPerCuttingBoard) external onlyOwner {
if (_maxNumWeightsPerCuttingBoard == 0) {
MaxNumWeightsPerCuttingBoardIsZero.selector.revertWith();
}
maxNumWeightsPerCuttingBoard = _maxNumWeightsPerCuttingBoard;
emit MaxNumWeightsPerCuttingBoardSet(_maxNumWeightsPerCuttingBoard);
}
/// @inheritdoc IBeraChef
function setCuttingBoardBlockDelay(uint64 _cuttingBoardBlockDelay) external onlyOwner {
cuttingBoardBlockDelay = _cuttingBoardBlockDelay;
emit CuttingBoardBlockDelaySet(_cuttingBoardBlockDelay);
}
/// @inheritdoc IBeraChef
function updateFriendsOfTheChef(address receiver, bool isFriend) external onlyOwner {
isFriendOfTheChef[receiver] = isFriend;
emit FriendsOfTheChefUpdated(receiver, isFriend);
}
/// @inheritdoc IBeraChef
function setDefaultCuttingBoard(CuttingBoard calldata cb) external onlyOwner {
// validate if the weights are valid.
_validateWeights(cb.weights);
emit SetDefaultCuttingBoard(cb);
defaultCuttingBoard = cb;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* SETTERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBeraChef
function queueNewCuttingBoard(
address valCoinbase,
uint64 startBlock,
Weight[] calldata weights
)
external
onlyValidatorOrOperator(valCoinbase)
{
// adds a delay before a new cutting board can go into effect
if (startBlock <= block.number + cuttingBoardBlockDelay) {
InvalidStartBlock.selector.revertWith();
}
// validate if the weights are valid.
_validateWeights(weights);
// delete the existing queued cutting board
CuttingBoard storage qcb = queuedCuttingBoards[valCoinbase];
delete qcb.weights;
// queue the new cutting board
qcb.startBlock = startBlock;
Weight[] storage storageWeights = qcb.weights;
for (uint256 i; i < weights.length;) {
storageWeights.push(weights[i]);
unchecked {
++i;
}
}
emit QueueCuttingBoard(valCoinbase, startBlock, weights);
}
/// @inheritdoc IBeraChef
function activateQueuedCuttingBoard(address valCoinbase, uint256 blockNumber) external onlyDistributor {
CuttingBoard storage qcb = queuedCuttingBoards[valCoinbase];
uint64 startBlock = qcb.startBlock;
if (startBlock == 0) {
QueuedCuttingBoardNotFound.selector.revertWith();
}
if (startBlock > blockNumber) {
QueuedCuttingBoardNotReady.selector.revertWith();
}
activeCuttingBoards[valCoinbase] = qcb;
emit ActivateCuttingBoard(valCoinbase, startBlock, qcb.weights);
// delete the queued cutting board
delete queuedCuttingBoards[valCoinbase];
}
/// @inheritdoc IBeraChef
function setOperator(address operatorAddress) external {
validatorOperator[msg.sender] = operatorAddress;
emit SetOperator(msg.sender, operatorAddress);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* GETTERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBeraChef
/// @dev Returns the active cutting board if the weights are still valid, otherwise the default cutting board.
/// @dev Returns empty cutting board if the validator does not have a cutting board.
function getActiveCuttingBoard(address valCoinbase) external view returns (CuttingBoard memory) {
CuttingBoard memory acb = activeCuttingBoards[valCoinbase];
// check if the weights are still valid.
if (_checkIfStillValid(acb.weights)) {
return acb;
}
// If we reach here, the weights are not valid, return the default cutting board.
// @dev The validator or its operator need to update their cutting board to a valid one for them to direct
// the block rewards.
return defaultCuttingBoard;
}
/// @inheritdoc IBeraChef
function getQueuedCuttingBoard(address valCoinbase) external view returns (CuttingBoard memory) {
return queuedCuttingBoards[valCoinbase];
}
/// @inheritdoc IBeraChef
function getOperator(address valCoinbase) external view returns (address) {
return validatorOperator[valCoinbase];
}
/// @inheritdoc IBeraChef
function getDefaultCuttingBoard() external view returns (CuttingBoard memory) {
return defaultCuttingBoard;
}
/// @inheritdoc IBeraChef
function isQueuedCuttingBoardReady(address valCoinbase, uint256 blockNumber) external view returns (bool) {
uint64 startBlock = queuedCuttingBoards[valCoinbase].startBlock;
return startBlock != 0 && startBlock <= blockNumber;
}
/// @inheritdoc IBeraChef
function isReady() external view returns (bool) {
// return that the default cutting board is set.
return defaultCuttingBoard.weights.length > 0;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/**
* @notice Validates the weights of a cutting board.
* @param weights The weights of the cutting board.
*/
function _validateWeights(Weight[] calldata weights) internal view {
if (weights.length > maxNumWeightsPerCuttingBoard) {
TooManyWeights.selector.revertWith();
}
// ensure that the total weight is 100%.
uint96 totalWeight;
for (uint256 i; i < weights.length;) {
Weight calldata weight = weights[i];
// ensure that all receivers are approved for every weight in the cutting board.
if (!isFriendOfTheChef[weight.receiver]) {
NotFriendOfTheChef.selector.revertWith();
}
totalWeight += weight.percentageNumerator;
unchecked {
++i;
}
}
if (totalWeight != ONE_HUNDRED_PERCENT) {
InvalidCuttingBoardWeights.selector.revertWith();
}
}
/**
* @notice Checks if the weights of a cutting board are still valid.
* @notice This method is used to check if the weights of a cutting board are still valid in flight.
* @param weights The weights of the cutting board.
* @return True if the weights are still valid, otherwise false.
*/
function _checkIfStillValid(Weight[] memory weights) internal view returns (bool) {
uint256 length = weights.length;
for (uint256 i; i < length;) {
// At the first occurrence of a receiver that is not a friend of the chef, return false.
if (!isFriendOfTheChef[weights[i].receiver]) {
return false;
}
unchecked {
++i;
}
}
// If all receivers are friends of the chef, return true.
return true;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// chosen to use an initializer instead of a constructor
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
// chosen not to use Solady because EIP-2612 is not needed
import {
ERC20Upgradeable,
IERC20,
IERC20Metadata
} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import { ERC20VotesUpgradeable } from
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol";
import { Multicallable } from "solady/src/utils/Multicallable.sol";
import { Utils } from "../libraries/Utils.sol";
import { IBGT } from "./interfaces/IBGT.sol";
import { IBeraChef } from "./interfaces/IBeraChef.sol";
import { BGTStaker } from "./BGTStaker.sol";
/// @title Bera Governance Token
/// @author Berachain Team
/// @dev Should be owned by the governance module.
/// @dev Only allows minting BGT by the BlockRewardController contract.
/// @dev It's not upgradable even though it inherits from `ERC20VotesUpgradeable` and `OwnableUpgradeable`.
/// @dev This contract inherits from `Multicallable` to allow for batch calls for `activateBoost` by a third party.
contract BGT is IBGT, ERC20VotesUpgradeable, OwnableUpgradeable, Multicallable {
using Utils for bytes4;
string private constant NAME = "Bera Governance Token";
string private constant SYMBOL = "BGT";
/// @dev The length of the history buffer.
uint32 private constant HISTORY_BUFFER_LENGTH = 8191;
/// @dev Represents 100%. Chosen to be less granular.
uint256 private constant ONE_HUNDRED_PERCENT = 1e4;
/// @dev Represents 10%.
uint256 private constant TEN_PERCENT = 1e3;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice The address of the BlockRewardController contract.
address internal _blockRewardController;
/// @notice The BeraChef contract that we are getting the operators for coinbase validators from.
IBeraChef public beraChef;
BGTStaker public staker;
/// @notice The struct of queued boosts
/// @param blockNumberLast The last block number boost balance was queued
/// @param balance The queued BGT balance to boost with
struct QueuedBoost {
uint32 blockNumberLast;
uint128 balance;
}
/// @notice The struct of user boosts
/// @param boost The boost balance being used by the user
/// @param queuedBoost The queued boost balance to be used by the user
struct UserBoost {
uint128 boost;
uint128 queuedBoost;
}
/// @notice The struct of validator commissions
/// @param blockNumberLast The last block number commission rate was updated
/// @param rate The commission rate for the validator
struct Commission {
uint32 blockNumberLast;
uint224 rate;
}
/// @notice Total amount of BGT used for validator boosts
uint128 public totalBoosts;
/// @notice The mapping of queued boosts on a validator by an account
mapping(address account => mapping(address validator => QueuedBoost)) public boostedQueue;
/// @notice The mapping of balances used to boost validator rewards by an account
mapping(address account => mapping(address validator => uint128)) public boosted;
/// @notice The mapping of boost balances used by an account
mapping(address account => UserBoost) internal userBoosts;
/// @notice The mapping of boost balances for a validator
mapping(address validator => uint128) public boostees;
/// @notice The mapping of validator commission rates charged on new block rewards
mapping(address validator => Commission) public commissions;
/// @notice The mapping of approved senders.
mapping(address sender => bool) public isWhitelistedSender;
/// @notice Initializes the BGT contract.
/// @dev Should be called only once by the deployer in the same transaction.
/// @dev Used instead of a constructor to make the `CREATE2` address independent of constructor arguments.
function initialize(address owner) external initializer {
__Ownable_init(owner);
__ERC20_init(NAME, SYMBOL);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ACCESS CONTROL */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Throws if called by any account other than BlockRewardController.
modifier onlyBlockRewardController() {
if (msg.sender != _blockRewardController) NotBlockRewardController.selector.revertWith();
_;
}
/// @dev Throws if the caller is not an approved sender.
modifier onlyApprovedSender(address sender) {
if (!isWhitelistedSender[sender]) NotApprovedSender.selector.revertWith();
_;
}
/// @dev Throws if sender available unboosted balance less than amount
modifier checkUnboostedBalance(address sender, uint256 amount) {
_checkUnboostedBalance(sender, amount);
_;
}
/// @notice check the invariant of the contract after the write operation
modifier invariantCheck() {
/// Run the method.
_;
/// Ensure that the contract is in a valid state after the write operation.
_invariantCheck();
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADMIN FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBGT
function whitelistSender(address sender, bool approved) external onlyOwner {
isWhitelistedSender[sender] = approved;
emit SenderWhitelisted(sender, approved);
}
/// @inheritdoc IBGT
function setMinter(address _minter) external onlyOwner {
if (_minter == address(0)) InvalidMinter.selector.revertWith();
emit MinterChanged(_blockRewardController, _minter);
_blockRewardController = _minter;
}
/// @inheritdoc IBGT
function mint(address distributor, uint256 amount) external onlyBlockRewardController invariantCheck {
super._mint(distributor, amount);
}
/// @inheritdoc IBGT
function setBeraChef(address _beraChef) external onlyOwner {
if (_beraChef == address(0)) ZeroAddress.selector.revertWith();
emit BeraChefChanged(address(beraChef), _beraChef);
beraChef = IBeraChef(_beraChef);
}
function setStaker(address _staker) external onlyOwner {
if (_staker == address(0)) ZeroAddress.selector.revertWith();
// emit StakerChanged(address(staker), _staker);
staker = BGTStaker(_staker);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VALIDATOR BOOSTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBGT
function queueBoost(address validator, uint128 amount) external checkUnboostedBalance(msg.sender, amount) {
userBoosts[msg.sender].queuedBoost += amount;
unchecked {
QueuedBoost storage qb = boostedQueue[msg.sender][validator];
// `userBoosts[msg.sender].queuedBoost` >= `qb.balance`
// if the former doesn't overflow, the latter won't
uint128 balance = qb.balance + amount;
(qb.balance, qb.blockNumberLast) = (balance, uint32(block.number));
}
emit QueueBoost(msg.sender, validator, amount);
}
/// @inheritdoc IBGT
function cancelBoost(address validator, uint128 amount) external {
QueuedBoost storage qb = boostedQueue[msg.sender][validator];
qb.balance -= amount;
unchecked {
// `userBoosts[msg.sender].queuedBoost` >= `qb.balance`
// if the latter doesn't underflow, the former won't
userBoosts[msg.sender].queuedBoost -= amount;
}
emit CancelBoost(msg.sender, validator, amount);
}
/// @inheritdoc IBGT
function activateBoost(address user, address validator) external {
QueuedBoost storage qb = boostedQueue[user][validator];
(uint32 blockNumberLast, uint128 amount) = (qb.blockNumberLast, qb.balance);
// `amount` zero will revert as it will fail with stake amount being zero at line 224.
_checkEnoughTimePassed(blockNumberLast);
totalBoosts += amount;
unchecked {
// `totalBoosts` >= `boostees[validator]` >= `boosted[msg.sender][validator]`
boostees[validator] += amount;
boosted[user][validator] += amount;
UserBoost storage userBoost = userBoosts[user];
(uint128 boost, uint128 _queuedBoost) = (userBoost.boost, userBoost.queuedBoost);
// `totalBoosts` >= `userBoosts[msg.sender].boost`
// `userBoosts[msg.sender].queuedBoost` >= `boostedQueue[msg.sender][validator].balance`
(userBoost.boost, userBoost.queuedBoost) = (boost + amount, _queuedBoost - amount);
}
delete boostedQueue[user][validator];
staker.stake(user, amount);
emit ActivateBoost(msg.sender, user, validator, amount);
}
/// @inheritdoc IBGT
function dropBoost(address validator, uint128 amount) external {
// `amount` should be greater than zero to avoid reverting at line 241 as
// `withdraw` will fail with zero amount.
boosted[msg.sender][validator] -= amount;
unchecked {
// `totalBoosts` >= `userBoosts[msg.sender].boost` >= `boosted[msg.sender][validator]`
totalBoosts -= amount;
userBoosts[msg.sender].boost -= amount;
// `totalBoosts` >= `boostees[validator]` >= `boosted[msg.sender][validator]`
boostees[validator] -= amount;
}
staker.withdraw(msg.sender, amount);
emit DropBoost(msg.sender, validator, amount);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VALIDATOR COMMISSIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBGT
function setCommission(address validator, uint256 rate) external {
if (msg.sender != validator) {
if (msg.sender != beraChef.getOperator(validator)) NotValidatorOrOperator.selector.revertWith();
}
if (rate > TEN_PERCENT) InvalidCommission.selector.revertWith();
Commission storage c = commissions[validator];
(uint32 blockNumberLast, uint224 currentRate) = (c.blockNumberLast, c.rate);
if (blockNumberLast > 0) _checkEnoughTimePassed(blockNumberLast);
(c.blockNumberLast, c.rate) = (uint32(block.number), uint224(rate));
emit UpdateCommission(validator, currentRate, rate);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC20 FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IERC20
/// @dev Only allows approve if the caller is an approved sender.
function approve(
address spender,
uint256 amount
)
public
override(IERC20, ERC20Upgradeable)
onlyApprovedSender(msg.sender)
returns (bool)
{
return super.approve(spender, amount);
}
/// @inheritdoc IERC20
/// @dev Only allows transfer if the caller is an approved sender and has enough unboosted balance.
function transfer(
address to,
uint256 amount
)
public
override(IERC20, ERC20Upgradeable)
onlyApprovedSender(msg.sender)
checkUnboostedBalance(msg.sender, amount)
returns (bool)
{
return super.transfer(to, amount);
}
/// @inheritdoc IERC20
/// @dev Only allows transferFrom if the from address is an approved sender and has enough unboosted balance.
/// @dev It spends the allowance of the caller.
function transferFrom(
address from,
address to,
uint256 amount
)
public
override(IERC20, ERC20Upgradeable)
onlyApprovedSender(from)
checkUnboostedBalance(from, amount)
returns (bool)
{
return super.transferFrom(from, to, amount);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* WRITES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBGT
function redeem(
address receiver,
uint256 amount
)
external
invariantCheck
checkUnboostedBalance(msg.sender, amount)
{
/// Burn the BGT token from the msg.sender account and reduce the total supply.
super._burn(msg.sender, amount);
/// Transfer the Native token to the receiver.
SafeTransferLib.safeTransferETH(receiver, amount);
emit Redeem(msg.sender, receiver, amount);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* GETTERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBGT
function minter() external view returns (address) {
return _blockRewardController;
}
/// @inheritdoc IBGT
function boostedRewardRate(address validator, uint256 rewardRate) external view returns (uint256) {
if (totalBoosts == 0) return 0;
return FixedPointMathLib.fullMulDiv(rewardRate, boostees[validator], totalBoosts);
}
/// @inheritdoc IBGT
function boosts(address account) external view returns (uint128) {
return userBoosts[account].boost;
}
/// @inheritdoc IBGT
function queuedBoost(address account) external view returns (uint128) {
return userBoosts[account].queuedBoost;
}
/// @inheritdoc IBGT
function commissionRewardRate(address validator, uint256 rewardRate) external view returns (uint256) {
return FixedPointMathLib.fullMulDiv(rewardRate, commissions[validator].rate, ONE_HUNDRED_PERCENT);
}
/// @inheritdoc IERC20Metadata
function name() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) {
return NAME;
}
/// @inheritdoc IERC20Metadata
function symbol() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) {
return SYMBOL;
}
//. @inheritdoc IBGT
function unboostedBalanceOf(address account) public view returns (uint256) {
UserBoost storage userBoost = userBoosts[account];
(uint128 boost, uint128 _queuedBoost) = (userBoost.boost, userBoost.queuedBoost);
return balanceOf(account) - boost - _queuedBoost;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
function _checkUnboostedBalance(address sender, uint256 amount) private view {
if (unboostedBalanceOf(sender) < amount) NotEnoughBalance.selector.revertWith();
}
function _checkEnoughTimePassed(uint32 blockNumberLast) private view {
unchecked {
uint32 delta = uint32(block.number) - blockNumberLast;
if (delta <= HISTORY_BUFFER_LENGTH) NotEnoughTime.selector.revertWith();
}
}
function _invariantCheck() internal view {
if (address(this).balance < totalSupply()) InvariantCheckFailed.selector.revertWith();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { Utils } from "../../libraries/Utils.sol";
import { IBlockRewardController } from "../interfaces/IBlockRewardController.sol";
import { IBeraChef } from "../interfaces/IBeraChef.sol";
import { BGT } from "../BGT.sol";
/// @title BlockRewardController
/// @author Berachain Team
/// @notice The BlockRewardController contract is responsible for managing the reward rate of BGT.
/// @dev It should be owned by the governance module.
/// @dev It should also be the only contract that can mint the BGT token.
/// @dev The invariants that should hold true are:
/// - processRewards() is called every block().
/// - processRewards() is only called once per block.
contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UUPSUpgradeable {
using Utils for bytes4;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice The BGT token contract that we are minting to the distributor.
BGT public bgt;
/// @notice The BeraChef contract to check the coinbase -> operator relationship.
IBeraChef public beraChef;
/// @notice The distributor contract that receives the minted BGT.
address public distributor;
/// @notice The constant base rate for BGT.
uint256 public baseRate;
/// @notice The reward rate for BGT.
uint256 public rewardRate;
/// @notice The minimum reward rate for BGT after accounting for validator boosts.
uint256 public minBoostedRewardRate;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
address _bgt,
address _distributor,
address _beraChef,
address _governance
)
external
initializer
{
__Ownable_init(_governance);
bgt = BGT(_bgt);
emit SetDistributor(_distributor);
// slither-disable-next-line missing-zero-check
distributor = _distributor;
// slither-disable-next-line missing-zero-check
beraChef = IBeraChef(_beraChef);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIER */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
modifier onlyDistributor() {
if (msg.sender != distributor) {
NotDistributor.selector.revertWith();
}
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADMIN FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBlockRewardController
function setBaseRate(uint256 _baseRate) external onlyOwner {
emit BaseRateChanged(baseRate, _baseRate);
baseRate = _baseRate;
}
/// @inheritdoc IBlockRewardController
function setRewardRate(uint256 _rewardRate) external onlyOwner {
emit RewardRateChanged(rewardRate, _rewardRate);
rewardRate = _rewardRate;
}
/// @inheritdoc IBlockRewardController
function setMinBoostedRewardRate(uint256 _minBoostedRewardRate) external onlyOwner {
emit MinBoostedRewardRateChanged(minBoostedRewardRate, _minBoostedRewardRate);
minBoostedRewardRate = _minBoostedRewardRate;
}
/// @inheritdoc IBlockRewardController
function setDistributor(address _distributor) external onlyOwner {
if (_distributor == address(0)) {
ZeroAddress.selector.revertWith();
}
emit SetDistributor(_distributor);
distributor = _distributor;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* DISTRIBUTOR FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @inheritdoc IBlockRewardController
function processRewards(address coinbase, uint256 blockNumber) external onlyDistributor returns (uint256) {
uint256 base = baseRate;
uint256 reward = rewardRate;
// Scale the reward rate based on the BGT used to boost the coinbase
reward = bgt.boostedRewardRate(coinbase, reward);
if (reward < minBoostedRewardRate) reward = minBoostedRewardRate;
// Factor in commission rate of the coinbase
uint256 commission = bgt.commissionRewardRate(coinbase, reward);
reward -= commission;
emit BlockRewardProcessed(blockNumber, base, commission, reward);
// Use the beraChef module to check if the coinbase has set a valid operator, if so mint the rewards there.
address operator = beraChef.getOperator(coinbase);
if (operator == address(0)) operator = coinbase;
if (base + commission > 0) bgt.mint(operator, base + commission);
// Mint the scaled rewards BGT for coinbase cutting board to the distributor.
if (reward > 0) bgt.mint(distributor, reward);
return reward;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol";
import { Utils } from "../../libraries/Utils.sol";
import { IBeraChef } from "../interfaces/IBeraChef.sol";
import { IBlockRewardController } from "../interfaces/IBlockRewardController.sol";
import { IDistributor } from "../interfaces/IDistributor.sol";
import { IBerachainRewardsVault } from "../interfaces/IBerachainRewardsVault.sol";
import { RootHelper } from "../RootHelper.sol";
/// @title Distributor
/// @author Berachain Team
/// @notice The Distributor contract is responsible for distributing the block rewards from the reward controller
/// and the cutting board weights, to the cutting board receivers.
/// @dev Each coinbase has its own cutting board, if it does not exist, a default cutting board is used.
/// And if governance has not set the default cutting board, the rewards are not minted and distributed.
contract Distributor is IDistributor, RootHelper, OwnableUpgradeable, UUPSUpgradeable {
using Utils for bytes4;
using Utils for address;
/// @dev Represents 100%. Chosen to be less granular.
uint96 internal constant ONE_HUNDRED_PERCENT = 1e4;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice The BeraChef contract that we are getting the cutting board from.
IBeraChef public beraChef;
/// @notice The rewards controller contract that we are getting the rewards rate from.
/// @dev And is responsible for minting the BGT token.
IBlockRewardController public blockRewardController;
/// @notice The BGT token contract that we are distributing to the cutting board receivers.
address public bgt;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(
address _berachef,
address _bgt,
address _blockRewardController,
address _governance,
address _prover
)
external
initializer
{
__Ownable_init(_governance);
beraChef = IBeraChef(_berachef);
bgt = _bgt;
blockRewardController = IBlockRewardController(_blockRewardController);
// slither-disable-next-line missing-zero-check
prover = _prover;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ADMIN FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
function resetCount(uint256 _block) public override onlyOwner {
super.resetCount(_block);
}
function setProver(address _prover) public override onlyOwner {
super.setProver(_prover);
}
/// @inheritdoc IDistributor
function distributeFor(address coinbase, uint256 blockNumber) external onlyProver {
uint256 nextActionableBlock = getNextActionableBlock();
// Check if next block is actionable, revert if not.
if (blockNumber != nextActionableBlock) {
NotActionableBlock.selector.revertWith();
}
_distributeFor(coinbase, blockNumber);
_incrementBlock(nextActionableBlock);
}
function _distributeFor(address coinbase, uint256 blockNumber) internal {
// If the berachef module is not ready, skip the distribution.
if (!beraChef.isReady()) {
return;
}
// Process the rewards with the block rewards controller for the specified block number.
// Its dependent on the beraChef being ready, if not it will return zero rewards for the current block.
uint256 rewardRate = blockRewardController.processRewards(coinbase, blockNumber);
if (rewardRate == 0) {
return; // No rewards to distribute, skip. This will skip since there is no default cutting board.
}
// Activate the queued cutting board if it is ready.
if (beraChef.isQueuedCuttingBoardReady(coinbase, blockNumber)) {
beraChef.activateQueuedCuttingBoard(coinbase, blockNumber);
}
// Get the active cutting board for the validator.
IBeraChef.CuttingBoard memory cb = beraChef.getActiveCuttingBoard(coinbase);
// If the validator does not have a cutting board, use the default cutting board.
if (cb.startBlock == 0) {
// This should never return a cutting board with no weights since we check earlier from isReady().
cb = beraChef.getDefaultCuttingBoard();
}
IBeraChef.Weight[] memory weights = cb.weights;
uint256 length = weights.length;
for (uint256 i; i < length;) {
IBeraChef.Weight memory weight = weights[i];
address receiver = weight.receiver;
// Calculate the reward for the receiver: (rewards * weightPercentage / ONE_HUNDRED_PERCENT).
uint256 rewardAmount =
FixedPointMathLib.fullMulDiv(rewardRate, weight.percentageNumerator, ONE_HUNDRED_PERCENT);
// The reward vault will pull the rewards from this contract so we can keep the approvals for the
// soul bound token BGT clean.
bgt.safeIncreaseAllowance(receiver, rewardAmount);
// Notify the receiver of the reward.
IBerachainRewardsVault(receiver).notifyRewardAmount(coinbase, rewardAmount);
emit Distributed(coinbase, blockNumber, receiver, rewardAmount);
unchecked {
++i;
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol";
import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol";
import { Utils } from "../libraries/Utils.sol";
import { IStakingRewards } from "./IStakingRewards.sol";
/// @title StakingRewards
/// @author Berachain Team
/// @notice This is a minimal implementation of staking rewards logic to be inherited.
/// @dev This contract is modified and abstracted from the stable and tested:
/// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol
abstract contract StakingRewards is Initializable, IStakingRewards {
using Utils for bytes4;
using SafeTransferLib for address;
/// @notice Struct to hold account data.
/// @param balance The balance of the staked tokens.
/// @param unclaimedReward The amount of unclaimed rewards.
/// @param rewardsPerTokenPaid The amount of rewards per token paid, scaled by PRECISION.
struct Info {
uint256 balance;
uint256 unclaimedReward;
uint256 rewardsPerTokenPaid;
}
uint256 internal constant PRECISION = 1e18;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice ERC20 token which users stake to earn rewards.
IERC20 public STAKE_TOKEN;
/// @notice ERC20 token in which rewards are denominated and distributed.
IERC20 public REWARD_TOKEN;
/// @notice The reward rate for the current reward period scaled by PRECISION.
uint256 public rewardRate;
/// @notice The amount of undistributed rewards.
uint256 public undistributedRewards;
/// @notice The last updated reward per token scaled by PRECISION.
uint256 public rewardPerTokenStored;
/// @notice The total supply of the staked tokens.
uint256 public totalSupply;
// TODO: use smaller types.
/// @notice The end of the current reward period, where we need to start a new one.
uint256 public periodFinish;
/// @notice The time over which the rewards will be distributed. Current default is 7 days.
uint256 public rewardsDuration;
/// @notice The last time the rewards were updated.
uint256 public lastUpdateTime;
/// @notice The mapping of accounts to their data.
mapping(address account => Info) internal _accountInfo;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INITIALIZER */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Must be called by the initializer of the inheriting contract.
/// @param _stakingToken The address of the token that users will stake.
/// @param _rewardToken The address of the token that will be distributed as rewards.
/// @param _rewardsDuration The duration of the rewards cycle.
function __StakingRewards_init(
address _stakingToken,
address _rewardToken,
uint256 _rewardsDuration
)
internal
onlyInitializing
{
STAKE_TOKEN = IERC20(_stakingToken);
REWARD_TOKEN = IERC20(_rewardToken);
rewardsDuration = _rewardsDuration;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
modifier updateReward(address account) {
_updateReward(account);
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STATE MUTATING FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice Notifies the staking contract of a new reward transfer.
/// @param reward The quantity of reward tokens being notified.
/// @dev Only authorized notifiers should call this method to avoid griefing or false notifications.
function _notifyRewardAmount(uint256 reward) internal virtual updateReward(address(0)) {
uint256 _periodFinish = periodFinish; // cache storage read
if (block.timestamp < _periodFinish) {
uint256 remainingTime;
unchecked {
remainingTime = _periodFinish - block.timestamp;
}
uint256 leftover = FixedPointMathLib.fullMulDiv(remainingTime, rewardRate, PRECISION);
reward += leftover;
}
undistributedRewards += reward;
_checkRewardSolvency();
if (totalSupply != 0) {
_setRewardRate(undistributedRewards);
lastUpdateTime = block.timestamp;
}
emit RewardAdded(reward);
}
/// @notice Check if the rewards are solvent.
/// @dev Inherited contracts may override this function to implement custom solvency checks.
function _checkRewardSolvency() internal view virtual {
if (undistributedRewards > REWARD_TOKEN.balanceOf(address(this))) InsolventReward.selector.revertWith();
}
/// @notice Claims the reward for a specified account and transfers it to the specified recipient.
/// @param account The account to claim the reward for.
/// @param recipient The account to receive the reward.
/// @return The amount of the reward claimed.
function _getReward(address account, address recipient) internal virtual updateReward(account) returns (uint256) {
Info storage info = _accountInfo[account];
uint256 reward = info.unclaimedReward; // get the rewards owed to the account
if (reward != 0) {
info.unclaimedReward = 0;
_safeTransferRewardToken(recipient, reward);
emit RewardPaid(account, recipient, reward);
}
return reward;
}
/// @notice Safely transfers the reward tokens to the specified recipient.
/// @dev Inherited contracts may override this function to implement custom transfer logic.
/// @param to The recipient address.
/// @param amount The amount of reward tokens to transfer.
function _safeTransferRewardToken(address to, uint256 amount) internal virtual {
address(REWARD_TOKEN).safeTransfer(to, amount);
}
/// @notice Stakes tokens in the vault for a specified account.
/// @param account The account to stake the tokens for.
/// @param amount The amount of tokens to stake.
function _stake(address account, uint256 amount) internal virtual {
if (amount == 0) StakeAmountIsZero.selector.revertWith();
// set the reward rate after the first stake if there are undistributed rewards
if (totalSupply == 0 && undistributedRewards > 0) {
_setRewardRate(undistributedRewards);
}
// update the rewards for the account after `rewardRate` is updated
_updateReward(account);
unchecked {
uint256 totalSupplyBefore = totalSupply; // cache storage read
uint256 totalSupplyAfter = totalSupplyBefore + amount;
// `<=` and `<` are equivalent here but the former is cheaper
if (totalSupplyAfter <= totalSupplyBefore) TotalSupplyOverflow.selector.revertWith();
totalSupply = totalSupplyAfter;
// `totalSupply` would have overflowed first because `totalSupplyBefore` >= `_accountInfo[account].balance`
_accountInfo[account].balance += amount;
}
_safeTransferFromStakeToken(msg.sender, amount);
emit Staked(account, amount);
}
/// @notice Safely transfers staking tokens from the sender to the contract.
/// @dev Inherited contracts may override this function to implement custom transfer logic.
/// @param from The address to transfer the tokens from.
/// @param amount The amount of tokens to transfer.
function _safeTransferFromStakeToken(address from, uint256 amount) internal virtual {
address(STAKE_TOKEN).safeTransferFrom(from, address(this), amount);
}
/// @notice Withdraws staked tokens from the vault for a specified account.
/// @param account The account to withdraw the tokens for.
/// @param amount The amount of tokens to withdraw.
function _withdraw(address account, uint256 amount) internal virtual {
if (amount == 0) WithdrawAmountIsZero.selector.revertWith();
// update the rewards for the account before the balance is updated
_updateReward(account);
unchecked {
Info storage info = _accountInfo[account];
uint256 balanceBefore = info.balance; // cache storage read
if (balanceBefore < amount) InsufficientStake.selector.revertWith();
info.balance = balanceBefore - amount;
// underflow not impossible because `totalSupply` >= `balanceBefore` >= `amount`
totalSupply -= amount;
}
_safeTransferStakeToken(msg.sender, amount);
emit Withdrawn(account, amount);
}
/// @notice Safely transfers staking tokens to the specified recipient.
/// @param to The recipient address.
/// @param amount The amount of tokens to transfer.
function _safeTransferStakeToken(address to, uint256 amount) internal virtual {
address(STAKE_TOKEN).safeTransfer(to, amount);
}
function _setRewardRate(uint256 reward) internal virtual {
uint256 _rewardsDuration = rewardsDuration; // cache storage read
uint256 _rewardRate = FixedPointMathLib.fullMulDiv(reward, PRECISION, _rewardsDuration);
rewardRate = _rewardRate;
periodFinish = block.timestamp + _rewardsDuration;
// TODO: remove undistributedRewards
undistributedRewards -= FixedPointMathLib.fullMulDiv(_rewardRate, _rewardsDuration, PRECISION);
}
function _updateReward(address account) internal virtual {
uint256 _rewardPerToken = rewardPerToken(); // cache result
rewardPerTokenStored = _rewardPerToken;
// record the last time the rewards were updated
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
Info storage info = _accountInfo[account];
(info.unclaimedReward, info.rewardsPerTokenPaid) = (earned(account), _rewardPerToken);
}
}
function _setRewardsDuration(uint256 _rewardsDuration) internal virtual {
// TODO: allow setting the rewards duration before the period finishes.
if (_rewardsDuration == 0) RewardsDurationIsZero.selector.revertWith();
if (block.timestamp <= periodFinish) RewardCycleNotEnded.selector.revertWith();
rewardsDuration = _rewardsDuration;
emit RewardsDurationUpdated(_rewardsDuration);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* GETTERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
function balanceOf(address account) public view virtual returns (uint256) {
return _accountInfo[account].balance;
}
function rewards(address account) public view virtual returns (uint256) {
return _accountInfo[account].unclaimedReward;
}
function userRewardPerTokenPaid(address account) public view virtual returns (uint256) {
return _accountInfo[account].rewardsPerTokenPaid;
}
function lastTimeRewardApplicable() public view virtual returns (uint256) {
return FixedPointMathLib.min(block.timestamp, periodFinish);
}
function rewardPerToken() public view virtual returns (uint256) {
uint256 _totalSupply = totalSupply; // cache storage read
if (_totalSupply == 0) return rewardPerTokenStored;
uint256 timeDelta;
unchecked {
timeDelta = lastTimeRewardApplicable() - lastUpdateTime;
}
return rewardPerTokenStored + FixedPointMathLib.fullMulDiv(timeDelta, rewardRate, _totalSupply);
}
function earned(address account) public view virtual returns (uint256) {
Info storage info = _accountInfo[account];
(uint256 balance, uint256 unclaimedReward, uint256 rewardsPerTokenPaid) =
(info.balance, info.unclaimedReward, info.rewardsPerTokenPaid);
uint256 rewardPerTokenDelta;
unchecked {
rewardPerTokenDelta = rewardPerToken() - rewardsPerTokenPaid;
}
return unclaimedReward + FixedPointMathLib.fullMulDiv(balance, rewardPerTokenDelta, PRECISION);
}
function getRewardForDuration() public view virtual returns (uint256) {
return FixedPointMathLib.fullMulDiv(rewardRate, rewardsDuration, PRECISION);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment