Created
September 22, 2024 23:50
-
-
Save larrythecucumber321/0e6399e980a70cf6a012af4719554e46 to your computer and use it in GitHub Desktop.
PoL Contracts (Sep 22)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { IBeaconDeposit } from "../interfaces/IBeaconDeposit.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; | |
} | |
uint256 private constant MAX_INCENTIVE_RATE = 1e36; // for 18 decimal token, this will mean 1e18 incentiveTokens | |
// per BGT emission. | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* 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 BeaconDeposit contract. | |
IBeaconDeposit public beaconDepositContract; | |
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 __beaconDepositContract, | |
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; | |
beaconDepositContract = IBeaconDeposit(__beaconDepositContract); | |
emit DistributorSet(_distributor); | |
emit MaxIncentiveTokensCountUpdated(3); | |
} | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* 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(bytes calldata pubkey, uint256 reward) external onlyDistributor { | |
_notifyRewardAmount(reward); | |
_processIncentives(pubkey, reward); | |
} | |
/// @inheritdoc IBerachainRewardsVault | |
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyFactoryOwner { | |
if (tokenAddress == address(stakeToken)) CannotRecoverStakingToken.selector.revertWith(); | |
if (incentives[tokenAddress].minIncentiveRate != 0) CannotRecoverIncentiveToken.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 { | |
// validate `minIncentiveRate` value | |
if (minIncentiveRate == 0) MinIncentiveRateIsZero.selector.revertWith(); | |
if (minIncentiveRate > MAX_INCENTIVE_RATE) IncentiveRateTooHigh.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() external onlyFactoryOwner { | |
_pause(); | |
} | |
/// @inheritdoc IBerachainRewardsVault | |
function unpause() external onlyFactoryOwner { | |
_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 | |
function getReward( | |
address account, | |
address recipient | |
) | |
external | |
nonReentrant | |
onlyOperatorOrUser(account) | |
returns (uint256) | |
{ | |
return _getReward(account, recipient); | |
} | |
/// @inheritdoc IBerachainRewardsVault | |
function exit(address recipient) external nonReentrant { | |
// self-staked amount | |
uint256 amount = _accountInfo[msg.sender].balance - _delegateStake[msg.sender].delegateTotalStaked; | |
_withdraw(msg.sender, amount); | |
_getReward(msg.sender, recipient); | |
} | |
/// @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) { | |
if (incentiveRate > MAX_INCENTIVE_RATE) IncentiveRateTooHigh.selector.revertWith(); | |
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 selfStaked = _accountInfo[account].balance - _delegateStake[account].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(rewardToken).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 = rewardToken.allowance(distributor, address(this)); | |
// TODO: change accounting | |
if (undistributedRewards > allowance) InsolventReward.selector.revertWith(); | |
} | |
/// @notice process the incentives for a coinbase. | |
/// @param pubkey The pubkey of the validator to process the incentives for. | |
/// @param bgtEmitted The amount of BGT emitted by the validator. | |
function _processIncentives(bytes calldata pubkey, uint256 bgtEmitted) internal { | |
// Validator's operator corresponding to the pubkey receives the incentives. | |
// The pubkey -> operator relationship is maintained by the BeaconDeposit contract. | |
address _operator = beaconDepositContract.getOperator(pubkey); | |
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(pubkey, 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; | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 BeaconDeposit contract address. | |
address public beaconDepositContract; | |
/// @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 _beaconDepositContract, | |
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 | |
beaconDepositContract = _beaconDepositContract; | |
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(beaconDepositContract, 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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { IBeaconDeposit } from "../interfaces/IBeaconDeposit.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; | |
uint64 public constant MAX_CUTTING_BOARD_BLOCK_DELAY = 1_315_000; // with 2 second block time, this is ~30 days. | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* STORAGE */ | |
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | |
/// @notice The address of the distributor contract. | |
address public distributor; | |
IBeaconDeposit public beaconDepositContract; | |
/// @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(bytes valPubkey => CuttingBoard) internal activeCuttingBoards; | |
/// @dev Mapping of validator coinbase address to queued cutting board. | |
mapping(bytes valPubkey => CuttingBoard) internal queuedCuttingBoards; | |
/// @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 internal defaultCuttingBoard; | |
/// @custom:oz-upgrades-unsafe-allow constructor | |
constructor() { | |
_disableInitializers(); | |
} | |
function initialize( | |
address _distributor, | |
address _governance, | |
address _beaconDepositContract, | |
uint8 _maxNumWeightsPerCuttingBoard | |
) | |
external | |
initializer | |
{ | |
__Ownable_init(_governance); | |
// slither-disable-next-line missing-zero-check | |
distributor = _distributor; | |
// slither-disable-next-line missing-zero-check | |
beaconDepositContract = IBeaconDeposit(_beaconDepositContract); | |
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 onlyOperator(bytes calldata valPubkey) { | |
if (msg.sender != beaconDepositContract.getOperator(valPubkey)) { | |
NotOperator.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 { | |
if (_cuttingBoardBlockDelay > MAX_CUTTING_BOARD_BLOCK_DELAY) { | |
CuttingBoardBlockDelayTooLarge.selector.revertWith(); | |
} | |
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( | |
bytes calldata valPubkey, | |
uint64 startBlock, | |
Weight[] calldata weights | |
) | |
external | |
onlyOperator(valPubkey) | |
{ | |
// 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[valPubkey]; | |
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(valPubkey, startBlock, weights); | |
} | |
/// @inheritdoc IBeraChef | |
function activateReadyQueuedCuttingBoard(bytes calldata valPubkey, uint256 blockNumber) external onlyDistributor { | |
if (!isQueuedCuttingBoardReady(valPubkey, blockNumber)) return; | |
CuttingBoard storage qcb = queuedCuttingBoards[valPubkey]; | |
uint64 startBlock = qcb.startBlock; | |
activeCuttingBoards[valPubkey] = qcb; | |
emit ActivateCuttingBoard(valPubkey, startBlock, qcb.weights); | |
// delete the queued cutting board | |
delete queuedCuttingBoards[valPubkey]; | |
} | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* GETTERS */ | |
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | |
/// @inheritdoc IBeraChef | |
/// @dev Returns the active cutting board if validator has a cutting board and the weights are still valid, | |
/// otherwise the default cutting board. | |
function getActiveCuttingBoard(bytes calldata valPubkey) external view returns (CuttingBoard memory) { | |
CuttingBoard memory acb = activeCuttingBoards[valPubkey]; | |
// check if the weights are still valid. | |
if (acb.startBlock > 0 && _checkIfStillValid(acb.weights)) { | |
return acb; | |
} | |
// If we reach here, either the weights are not valid or validator does not have any cutting board, 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(bytes calldata valPubkey) external view returns (CuttingBoard memory) { | |
return queuedCuttingBoards[valPubkey]; | |
} | |
/// @inheritdoc IBeraChef | |
function getSetActiveCuttingBoard(bytes calldata valPubkey) external view returns (CuttingBoard memory) { | |
return activeCuttingBoards[valPubkey]; | |
} | |
/// @inheritdoc IBeraChef | |
function getDefaultCuttingBoard() external view returns (CuttingBoard memory) { | |
return defaultCuttingBoard; | |
} | |
/// @inheritdoc IBeraChef | |
function isQueuedCuttingBoardReady(bytes calldata valPubkey, uint256 blockNumber) public view returns (bool) { | |
uint64 startBlock = queuedCuttingBoards[valPubkey].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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { IBeaconDeposit } from "./interfaces/IBeaconDeposit.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 BeaconDeposit contract that we are getting the operators for validators from. | |
IBeaconDeposit public beaconDepositContract; | |
/// @notice The BGTStaker contract that we are using to stake and withdraw BGT. | |
/// @dev This contract is used to distribute dapp fees to BGT delegators. | |
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's queued commissions | |
/// @param blockNumberLast The last block number commission rate was queued | |
/// @param rate The commission rate for the validator | |
struct QueuedCommission { | |
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(bytes pubkey => QueuedBoost)) public boostedQueue; | |
/// @notice The mapping of balances used to boost validator rewards by an account | |
mapping(address account => mapping(bytes pubkey => 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(bytes pubkey => uint128) public boostees; | |
/// @notice The mapping of validator queued commission rates charged on new block rewards | |
mapping(bytes pubkey => QueuedCommission) public queuedCommissions; | |
/// @notice The mapping of validator commission rates charged on new block rewards. | |
mapping(bytes pubkey => uint224 rate) 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(); | |
} | |
/// @dev Throws if the caller is not the operator of the validator. | |
modifier onlyOperator(bytes calldata pubkey) { | |
_onlyOperator(pubkey); | |
_; | |
} | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* 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)) ZeroAddress.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 setBeaconDepositContract(address _beaconDepositContract) external onlyOwner { | |
if (_beaconDepositContract == address(0)) ZeroAddress.selector.revertWith(); | |
emit BeaconDepositContractChanged(address(beaconDepositContract), _beaconDepositContract); | |
beaconDepositContract = IBeaconDeposit(_beaconDepositContract); | |
} | |
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(bytes calldata pubkey, uint128 amount) external checkUnboostedBalance(msg.sender, amount) { | |
userBoosts[msg.sender].queuedBoost += amount; | |
unchecked { | |
QueuedBoost storage qb = boostedQueue[msg.sender][pubkey]; | |
// `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, pubkey, amount); | |
} | |
/// @inheritdoc IBGT | |
function cancelBoost(bytes calldata pubkey, uint128 amount) external { | |
QueuedBoost storage qb = boostedQueue[msg.sender][pubkey]; | |
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, pubkey, amount); | |
} | |
/// @inheritdoc IBGT | |
function activateBoost(address user, bytes calldata pubkey) external { | |
QueuedBoost storage qb = boostedQueue[user][pubkey]; | |
(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[user][validator]` | |
boostees[pubkey] += amount; | |
boosted[user][pubkey] += amount; | |
UserBoost storage userBoost = userBoosts[user]; | |
(uint128 boost, uint128 _queuedBoost) = (userBoost.boost, userBoost.queuedBoost); | |
// `totalBoosts` >= `userBoosts[user].boost` | |
// `userBoosts[user].queuedBoost` >= `boostedQueue[user][validator].balance` | |
(userBoost.boost, userBoost.queuedBoost) = (boost + amount, _queuedBoost - amount); | |
} | |
delete boostedQueue[user][pubkey]; | |
staker.stake(user, amount); | |
emit ActivateBoost(msg.sender, user, pubkey, amount); | |
} | |
/// @inheritdoc IBGT | |
function dropBoost(bytes calldata pubkey, 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][pubkey] -= amount; | |
unchecked { | |
// `totalBoosts` >= `userBoosts[msg.sender].boost` >= `boosted[msg.sender][validator]` | |
totalBoosts -= amount; | |
userBoosts[msg.sender].boost -= amount; | |
// boostees[validator]` >= `boosted[msg.sender][validator]` | |
boostees[pubkey] -= amount; | |
} | |
staker.withdraw(msg.sender, amount); | |
emit DropBoost(msg.sender, pubkey, amount); | |
} | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* VALIDATOR COMMISSIONS */ | |
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | |
/// @inheritdoc IBGT | |
function queueCommissionChange(bytes calldata pubkey, uint256 rate) external onlyOperator(pubkey) { | |
if (rate > TEN_PERCENT) InvalidCommission.selector.revertWith(); | |
QueuedCommission storage c = queuedCommissions[pubkey]; | |
(c.blockNumberLast, c.rate) = (uint32(block.number), uint224(rate)); | |
emit QueueCommissionChange(pubkey, commissions[pubkey], rate); | |
} | |
/// @inheritdoc IBGT | |
function cancelCommissionChange(bytes calldata pubkey) external onlyOperator(pubkey) { | |
delete queuedCommissions[pubkey]; | |
emit CancelCommissionChange(pubkey); | |
} | |
/// @inheritdoc IBGT | |
function activateCommissionChange(bytes calldata pubkey) external { | |
QueuedCommission storage c = queuedCommissions[pubkey]; | |
(uint32 blockNumberLast, uint224 rate) = (c.blockNumberLast, c.rate); | |
// check if the commission is queued, if not revert with error | |
if (blockNumberLast == 0) CommissionNotQueued.selector.revertWith(); | |
_checkEnoughTimePassed(blockNumberLast); | |
commissions[pubkey] = rate; | |
delete queuedCommissions[pubkey]; | |
emit ActivateCommissionChange(pubkey, 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(bytes calldata pubkey, uint256 rewardRate) external view returns (uint256) { | |
if (totalBoosts == 0) return 0; | |
return FixedPointMathLib.fullMulDiv(rewardRate, boostees[pubkey], 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(bytes calldata pubkey, uint256 rewardRate) external view returns (uint256) { | |
return FixedPointMathLib.fullMulDiv(rewardRate, commissions[pubkey], 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() private view { | |
if (address(this).balance < totalSupply()) InvariantCheckFailed.selector.revertWith(); | |
} | |
function _onlyOperator(bytes calldata pubkey) private view { | |
if (msg.sender != beaconDepositContract.getOperator(pubkey)) NotOperator.selector.revertWith(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { IBeaconDeposit } from "../interfaces/IBeaconDeposit.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 Beacon deposit contract to check the pubkey -> operator relationship. | |
IBeaconDeposit public beaconDepositContract; | |
/// @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 _beaconDepositContract, | |
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 | |
beaconDepositContract = IBeaconDeposit(_beaconDepositContract); | |
} | |
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(bytes calldata pubkey, 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(pubkey, reward); | |
if (reward < minBoostedRewardRate) reward = minBoostedRewardRate; | |
// Factor in commission rate of the coinbase | |
uint256 commission = bgt.commissionRewardRate(pubkey, reward); | |
reward -= commission; | |
emit BlockRewardProcessed(blockNumber, base, commission, reward); | |
// Use the beaconDepositContract to fetch the operator, Its gauranteed to return a valid address. | |
// Beacon Deposit contract will enforce validators to set an operator. | |
address operator = beaconDepositContract.getOperator(pubkey); | |
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { Multicallable } from "solady/src/utils/Multicallable.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 { IBeaconVerifier } from "../interfaces/IBeaconVerifier.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, Multicallable { | |
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; | |
// address of beacon verifier contract | |
IBeaconVerifier public beaconVerifier; | |
/// @custom:oz-upgrades-unsafe-allow constructor | |
constructor() { | |
_disableInitializers(); | |
} | |
function initialize( | |
address _berachef, | |
address _bgt, | |
address _blockRewardController, | |
address _governance, | |
address _beaconVerifier | |
) | |
external | |
initializer | |
{ | |
__Ownable_init(_governance); | |
beraChef = IBeraChef(_berachef); | |
bgt = _bgt; | |
blockRewardController = IBlockRewardController(_blockRewardController); | |
beaconVerifier = IBeaconVerifier(_beaconVerifier); | |
emit BeaconVerifierSet(_beaconVerifier); | |
} | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* ADMIN FUNCTIONS */ | |
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | |
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } | |
function resetCount(uint256 _block) public override onlyOwner { | |
super.resetCount(_block); | |
} | |
function setBeaconVerifier(address _beaconVerifier) public onlyOwner { | |
if (_beaconVerifier == address(0)) { | |
ZeroAddress.selector.revertWith(); | |
} | |
beaconVerifier = IBeaconVerifier(_beaconVerifier); | |
emit BeaconVerifierSet(_beaconVerifier); | |
} | |
/// @inheritdoc IDistributor | |
function distributeFor( | |
uint64 timestamp, | |
uint64 blockNumber, | |
uint64 proposerIndex, | |
bytes calldata pubkey, | |
bytes32[] calldata pubkeyProof, | |
bytes32[] calldata blockNumberProof | |
) | |
external | |
{ | |
// Verify the pubkey and execution number. | |
beaconVerifier.verifyBeaconBlockProposer(timestamp, proposerIndex, pubkey, pubkeyProof); | |
beaconVerifier.verifyExecutionNumber(timestamp, blockNumber, blockNumberProof); | |
// Distribute the rewards. | |
uint256 nextActionableBlock = getNextActionableBlock(); | |
// Check if next block is actionable, revert if not. | |
if (blockNumber != nextActionableBlock) { | |
NotActionableBlock.selector.revertWith(); | |
} | |
_distributeFor(pubkey, blockNumber); | |
_incrementBlock(nextActionableBlock); | |
} | |
function _distributeFor(bytes calldata pubkey, 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(pubkey, 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. | |
beraChef.activateReadyQueuedCuttingBoard(pubkey, blockNumber); | |
// Get the active cutting board for the validator. | |
// This will return the default cutting board if the validator does not have an active cutting board. | |
IBeraChef.CuttingBoard memory cb = beraChef.getActiveCuttingBoard(pubkey); | |
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(pubkey, rewardAmount); | |
emit Distributed(pubkey, blockNumber, receiver, rewardAmount); | |
unchecked { | |
++i; | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 stakeToken; | |
/// @notice ERC20 token in which rewards are denominated and distributed. | |
IERC20 public rewardToken; | |
/// @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 | |
{ | |
stakeToken = IERC20(_stakingToken); | |
rewardToken = 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)) { | |
if (totalSupply != 0 && block.timestamp < periodFinish) { | |
reward += _computeLeftOverReward(); | |
} | |
undistributedRewards += reward; | |
_checkRewardSolvency(); | |
if (totalSupply != 0) { | |
_setRewardRate(); | |
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 > rewardToken.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(rewardToken).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(); | |
} | |
// 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(stakeToken).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 possible because `totalSupply` >= `balanceBefore` >= `amount` | |
totalSupply -= amount; | |
} | |
if (totalSupply == 0 && block.timestamp < periodFinish) { | |
undistributedRewards += _computeLeftOverReward(); | |
} | |
_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(stakeToken).safeTransfer(to, amount); | |
} | |
function _setRewardRate() internal virtual { | |
uint256 _rewardsDuration = rewardsDuration; // cache storage read | |
uint256 _rewardRate = FixedPointMathLib.fullMulDiv(undistributedRewards, 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); | |
} | |
function _computeLeftOverReward() internal view returns (uint256 leftOver) { | |
uint256 remainingTime; | |
unchecked { | |
remainingTime = periodFinish - block.timestamp; | |
} | |
leftOver = FixedPointMathLib.fullMulDiv(remainingTime, rewardRate, PRECISION); | |
} | |
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | |
/* 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; | |
} | |
// computes reward per token by rounding it down to avoid reverting '_getReward' with insufficient rewards | |
uint256 _newRewardPerToken = | |
FixedPointMathLib.divWad(FixedPointMathLib.mulWad(rewardRate, timeDelta), _totalSupply); | |
return rewardPerTokenStored + _newRewardPerToken; | |
} | |
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