Last active
July 11, 2024 09:39
-
-
Save larrythecucumber321/a836249cc3cea498625668dfbf1ac606 to your computer and use it in GitHub Desktop.
Proof of Liquidity
This file contains 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 { 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; | |
} | |
} | |
} | |
} | |
} |
This file contains 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 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; | |
} | |
} |
This file contains 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 { 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; | |
} | |
} |
This file contains 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 { 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(); | |
} | |
} |
This file contains 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 { 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; | |
} | |
} |
This file contains 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 { 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; | |
} | |
} | |
} | |
} |
This file contains 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 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