From 85e6de8ede98c7a401a20b2ab30215848050555b Mon Sep 17 00:00:00 2001 From: kexley <87971154+kexleyBeefy@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:35:37 -0400 Subject: [PATCH] FeeBatchV4 & improve reward pool --- contracts/BIFI/infra/BeefyFeeBatchV4.sol | 364 +++++++++++++ .../BIFI/infra/BeefyOracle/BeefyOracle.sol | 3 +- .../BeefyOracle/BeefyOracleChainlink.sol | 11 +- .../infra/BeefyOracle/BeefyOracleErrors.sol | 25 + .../infra/BeefyOracle/BeefyOracleHelper.sol | 4 +- .../infra/BeefyOracle/BeefyOracleSolidly.sol | 23 +- .../BeefyOracle/BeefyOracleUniswapV2.sol | 23 +- .../BeefyOracle/BeefyOracleUniswapV3.sol | 27 +- contracts/BIFI/infra/BeefyRewardPool.sol | 483 +++++++++++++----- contracts/BIFI/infra/BeefySwapper.sol | 158 ++++-- .../interfaces/beefy/IBeefyRewardPool.sol | 11 + .../BIFI/interfaces/beefy/IBeefySwapper.sol | 24 + .../BIFI/interfaces/common/IWrappedNative.sol | 1 - 13 files changed, 934 insertions(+), 223 deletions(-) create mode 100644 contracts/BIFI/infra/BeefyFeeBatchV4.sol create mode 100644 contracts/BIFI/infra/BeefyOracle/BeefyOracleErrors.sol create mode 100644 contracts/BIFI/interfaces/beefy/IBeefyRewardPool.sol create mode 100644 contracts/BIFI/interfaces/beefy/IBeefySwapper.sol diff --git a/contracts/BIFI/infra/BeefyFeeBatchV4.sol b/contracts/BIFI/infra/BeefyFeeBatchV4.sol new file mode 100644 index 00000000..f4a6612a --- /dev/null +++ b/contracts/BIFI/infra/BeefyFeeBatchV4.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { SafeERC20Upgradeable, IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import { IBeefySwapper } from "../interfaces/beefy/IBeefySwapper.sol"; +import { IBeefyRewardPool } from "../interfaces/beefy/IBeefyRewardPool.sol"; +import { IWrappedNative } from "../interfaces/common/IWrappedNative.sol"; + +/// @title Beefy fee batch +/// @author kexley, Beefy +/// @notice All Beefy fees will flow through to the treasury and the reward pool +/// @dev Wrapped ETH will build up on this contract and will be swapped via the Beefy Swapper to +/// the pre-specified tokens and distributed to the treasury and reward pool +contract BeefyFeeBatchV4 is OwnableUpgradeable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @dev Bundled token information + /// @param tokens Token addresses to swap to + /// @param index Location of a token in the tokens array + /// @param allocPoint Allocation points for this token + /// @param totalAllocPoint Total amount of allocation points assigned to tokens in the array + struct TokenInfo { + address[] tokens; + mapping(address => uint256) index; + mapping(address => uint256) allocPoint; + uint256 totalAllocPoint; + } + + /// @notice Native token (WETH) + IERC20Upgradeable public native; + + /// @notice Treasury address + address public treasury; + + /// @notice Reward pool address + address public rewardPool; + + /// @notice Swapper address to swap all tokens at + address public swapper; + + /// @notice Vault harvester + address public harvester; + + /// @notice Treasury fee of the total native received on the contract (1 = 0.1%) + uint256 public treasuryFee; + + /// @notice Denominator constant + uint256 constant public DIVISOR = 1000; + + /// @notice Duration of reward distributions + uint256 public duration; + + /// @notice Minimum operating gas level on the harvester + uint256 public harvesterMax; + + /// @notice Whether to send gas to the harvester + bool public sendHarvesterGas; + + /// @notice Tokens to be sent to the treasury + TokenInfo public treasuryTokens; + + /// @notice Tokens to be sent to the reward pool + TokenInfo public rewardTokens; + + /// @notice Fees have been harvested + /// @param totalHarvested Total fee amount that has been processed + /// @param timestamp Timestamp of the harvest + event Harvest(uint256 totalHarvested, uint256 timestamp); + /// @notice Harvester has been sent gas + /// @param gas Amount of gas that has been sent + event SendHarvesterGas(uint256 gas); + /// @notice Treasury fee that has been sent + /// @param token Token that has been sent + /// @param amount Amount of the token sent + event DistributeTreasuryFee(address indexed token, uint256 amount); + /// @notice Reward pool has been notified + /// @param token Token used as a reward + /// @param amount Amount of the token used + /// @param duration Duration of the distribution + event NotifyRewardPool(address indexed token, uint256 amount, uint256 duration); + /// @notice Reward pool set + /// @param rewardPool New reward pool address + event SetRewardPool(address rewardPool); + /// @notice Treasury set + /// @param treasury New treasury address + event SetTreasury(address treasury); + /// @notice Whether to send gas to harvester has been set + /// @param send Whether to send gas to harvester + event SetSendHarvesterGas(bool send); + /// @notice Harvester set + /// @param harvester New harvester address + /// @param harvesterMax Minimum operating gas level for the harvester + event SetHarvester(address harvester, uint256 harvesterMax); + /// @notice Swapper set + /// @param swapper New swapper address + event SetSwapper(address swapper); + /// @notice Treasury fee set + /// @param fee New fee split for the treasury + event SetTreasuryFee(uint256 fee); + /// @notice Reward pool duration set + /// @param duration New duration of the reward distribution + event SetDuration(uint256 duration); + /// @notice Set the whitelist status of a manager for the reward pool + /// @param manager Address of the manager + /// @param whitelisted Status of the manager on the whitelist + event SetWhitelistOfRewardPool(address manager, bool whitelisted); + /// @notice Remove a reward from the reward pool distribution + /// @param reward Address of the reward to remove + /// @param recipient Address to send the reward to + event RemoveRewardFromRewardPool(address reward, address recipient); + /// @notice Rescue an unsupported token from the reward pool + /// @param token Address of the token to remove + /// @param recipient Address to send the token to + event RescueTokensFromRewardPool(address token, address recipient); + /// @notice Transfer ownership of the reward pool to a new owner + /// @param owner New owner of the reward pool + event TransferOwnershipOfRewardPool(address owner); + /// @notice Rescue an unsupported token + /// @param token Address of the token + /// @param recipient Address to send the token to + event RescueTokens(address token, address recipient); + + /// @notice Initialize the contract, callable only once + /// @param _native WETH address + /// @param _rewardPool Reward pool address + /// @param _treasury Treasury address + /// @param _swapper Swapper address + /// @param _treasuryFee Treasury fee split + function initialize( + address _native, + address _rewardPool, + address _treasury, + address _swapper, + uint256 _treasuryFee + ) external initializer { + __Ownable_init(); + + native = IERC20Upgradeable(_native); + treasury = _treasury; + rewardPool = _rewardPool; + treasuryFee = _treasuryFee; + swapper = _swapper; + native.forceApprove(swapper, type(uint).max); + duration = 7 days; + } + + /// @notice Distribute the fees to the harvester, treasury and reward pool + function harvest() external { + uint256 totalFees = native.balanceOf(address(this)); + + if (sendHarvesterGas) _sendHarvesterGas(); + _distributeTreasuryFee(); + _notifyRewardPool(); + + emit Harvest(totalFees - native.balanceOf(address(this)), block.timestamp); + } + + /// @dev Unwrap the required amount of native and send to the harvester + function _sendHarvesterGas() private { + uint256 nativeBal = native.balanceOf(address(this)); + + uint256 harvesterBal = harvester.balance + native.balanceOf(harvester); + if (harvesterBal < harvesterMax) { + uint256 gas = harvesterMax - harvesterBal; + if (gas > nativeBal) { + gas = nativeBal; + } + IWrappedNative(address(native)).withdraw(gas); + (bool sent, ) = harvester.call{value: gas}(""); + require(sent, "Failed to send Ether"); + + emit SendHarvesterGas(gas); + } + } + + /// @dev Swap to required treasury tokens and send the treasury fees onto the treasury + function _distributeTreasuryFee() private { + uint256 treasuryFeeAmount = native.balanceOf(address(this)) * treasuryFee / DIVISOR; + + for (uint i; i < treasuryTokens.tokens.length; ++i) { + address token = treasuryTokens.tokens[i]; + uint256 amount = treasuryFeeAmount + * treasuryTokens.allocPoint[token] + / treasuryTokens.totalAllocPoint; + + if (amount == 0) continue; + if (token != address(native)) { + amount = IBeefySwapper(swapper).swap(address(native), token, amount); + if (amount == 0) continue; + } + + IERC20Upgradeable(token).safeTransfer(treasury, amount); + emit DistributeTreasuryFee(token, amount); + } + } + + /// @dev Swap to required reward tokens and notify the reward pool + function _notifyRewardPool() private { + uint256 rewardPoolAmount = native.balanceOf(address(this)); + + for (uint i; i < rewardTokens.tokens.length; ++i) { + address token = rewardTokens.tokens[i]; + uint256 amount = rewardPoolAmount + * rewardTokens.allocPoint[token] + / rewardTokens.totalAllocPoint; + + if (amount == 0) continue; + if (token != address(native)) { + amount = IBeefySwapper(swapper).swap(address(native), token, amount); + if (amount == 0) continue; + } + + IBeefyRewardPool(rewardPool).notifyRewardAmount(token, amount, duration); + emit NotifyRewardPool(token, amount, duration); + } + } + + /* ----------------------------------- VARIABLE SETTERS ----------------------------------- */ + + /// @notice Adjust which tokens and how much the harvest should swap the treasury fee to + /// @param _token Address of the token to send to the treasury + /// @param _allocPoint How much to swap into the particular token from the treasury fee + function setTreasuryAllocPoint(address _token, uint256 _allocPoint) external onlyOwner { + if (treasuryTokens.allocPoint[_token] > 0 && _allocPoint == 0) { + address endToken = treasuryTokens.tokens[treasuryTokens.tokens.length - 1]; + treasuryTokens.index[endToken] = treasuryTokens.index[_token]; + treasuryTokens.tokens[treasuryTokens.index[endToken]] = endToken; + treasuryTokens.tokens.pop(); + } else if (treasuryTokens.allocPoint[_token] == 0 && _allocPoint > 0) { + treasuryTokens.index[_token] = treasuryTokens.tokens.length; + treasuryTokens.tokens.push(_token); + } + + treasuryTokens.totalAllocPoint -= treasuryTokens.allocPoint[_token]; + treasuryTokens.totalAllocPoint += _allocPoint; + treasuryTokens.allocPoint[_token] = _allocPoint; + } + + /// @notice Adjust which tokens and how much the harvest should swap the reward pool fee to + /// @param _token Address of the token to send to the reward pool + /// @param _allocPoint How much to swap into the particular token from the reward pool fee + function setRewardAllocPoint(address _token, uint256 _allocPoint) external onlyOwner { + if (rewardTokens.allocPoint[_token] > 0 && _allocPoint == 0) { + address endToken = rewardTokens.tokens[rewardTokens.tokens.length - 1]; + rewardTokens.index[endToken] = rewardTokens.index[_token]; + rewardTokens.tokens[rewardTokens.index[endToken]] = endToken; + rewardTokens.tokens.pop(); + } else if (rewardTokens.allocPoint[_token] == 0 && _allocPoint > 0) { + rewardTokens.index[_token] = rewardTokens.tokens.length; + rewardTokens.tokens.push(_token); + IERC20Upgradeable(_token).forceApprove(rewardPool, type(uint).max); + } + + rewardTokens.totalAllocPoint -= rewardTokens.allocPoint[_token]; + rewardTokens.totalAllocPoint += _allocPoint; + rewardTokens.allocPoint[_token] = _allocPoint; + } + + /// @notice Set the reward pool + /// @param _rewardPool New reward pool address + function setRewardPool(address _rewardPool) external onlyOwner { + rewardPool = _rewardPool; + emit SetRewardPool(_rewardPool); + } + + /// @notice Set the treasury + /// @param _treasury New treasury address + function setTreasury(address _treasury) external onlyOwner { + treasury = _treasury; + emit SetTreasury(_treasury); + } + + /// @notice Set whether the harvester should be sent gas + /// @param _sendGas Whether the harvester should be sent gas + function setSendHarvesterGas(bool _sendGas) external onlyOwner { + sendHarvesterGas = _sendGas; + emit SetSendHarvesterGas(_sendGas); + } + + /// @notice Set the harvester and the minimum operating gas level of the harvester + /// @param _harvester New harvester address + /// @param _harvesterMax New minimum operating gas level of the harvester + function setHarvesterConfig(address _harvester, uint256 _harvesterMax) external onlyOwner { + harvester = _harvester; + harvesterMax = _harvesterMax; + emit SetHarvester(_harvester, _harvesterMax); + } + + /// @notice Set the swapper + /// @param _swapper New swapper address + function setSwapper(address _swapper) external onlyOwner { + native.approve(swapper, 0); + swapper = _swapper; + native.forceApprove(swapper, type(uint).max); + emit SetSwapper(_swapper); + } + + /// @notice Set the treasury fee + /// @param _treasuryFee New treasury fee split + function setTreasuryFee(uint256 _treasuryFee) external onlyOwner { + if (_treasuryFee > DIVISOR) _treasuryFee = DIVISOR; + treasuryFee = _treasuryFee; + emit SetTreasuryFee(_treasuryFee); + } + + /// @notice Set the duration of the reward distribution + /// @param _duration New duration of the reward distribution + function setDuration(uint256 _duration) external onlyOwner { + duration = _duration; + emit SetDuration(_duration); + } + + /* -------------------------------- REWARD POOL MANAGEMENT -------------------------------- */ + + /// @notice Set the whitelist status of a manager for the reward pool + /// @param _manager Address of the manager + /// @param _whitelisted Status of the manager on the whitelist + function setWhitelistOfRewardPool(address _manager, bool _whitelisted) external onlyOwner { + IBeefyRewardPool(rewardPool).setWhitelist(_manager, _whitelisted); + emit SetWhitelistOfRewardPool(_manager, _whitelisted); + } + + /// @notice Remove a reward from the reward pool distribution + /// @param _reward Address of the reward to remove + /// @param _recipient Address to send the reward to + function removeRewardFromRewardPool(address _reward, address _recipient) external onlyOwner { + IBeefyRewardPool(rewardPool).removeReward(_reward, _recipient); + emit RemoveRewardFromRewardPool(_reward, _recipient); + } + + /// @notice Rescue an unsupported token from the reward pool + /// @param _token Address of the token to remove + /// @param _recipient Address to send the token to + function rescueTokensFromRewardPool(address _token, address _recipient) external onlyOwner { + IBeefyRewardPool(rewardPool).rescueTokens(_token, _recipient); + emit RescueTokensFromRewardPool(_token, _recipient); + } + + /// @notice Transfer ownership of the reward pool to a new owner + /// @param _owner New owner of the reward pool + function transferOwnershipOfRewardPool(address _owner) external onlyOwner { + IBeefyRewardPool(rewardPool).transferOwnership(_owner); + emit TransferOwnershipOfRewardPool(_owner); + } + + /* ------------------------------------- SWEEP TOKENS ------------------------------------- */ + + /// @notice Rescue an unsupported token + /// @param _token Address of the token + /// @param _recipient Address to send the token to + function rescueTokens(address _token, address _recipient) external onlyOwner { + require(_token != address(native), "!safe"); + + uint256 amount = IERC20Upgradeable(_token).balanceOf(address(this)); + IERC20Upgradeable(_token).safeTransfer(_recipient, amount); + emit RescueTokens(_token, _recipient); + } + + /// @notice Support unwrapped native + receive() external payable {} +} diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracle.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracle.sol index 23c1fe29..04b98b89 100644 --- a/contracts/BIFI/infra/BeefyOracle/BeefyOracle.sol +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracle.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + import { ISubOracle } from "../../interfaces/oracle/ISubOracle.sol"; /// @title Beefy Oracle diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracleChainlink.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracleChainlink.sol index f3c53b8e..ef7d27b0 100644 --- a/contracts/BIFI/infra/BeefyOracle/BeefyOracleChainlink.sol +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracleChainlink.sol @@ -1,18 +1,15 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { IChainlink } from "../../interfaces/oracle/IChainlink.sol"; -import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; +import { BeefyOracleHelper, BeefyOracleErrors } from "./BeefyOracleHelper.sol"; /// @title Beefy Oracle using Chainlink /// @author Beefy, @kexley /// @notice On-chain oracle using Chainlink library BeefyOracleChainlink { - /// @dev No response from the Chainlink feed - error NoAnswer(); - /// @notice Fetch price from the Chainlink feed and scale to 18 decimals /// @param _data Payload from the central oracle with the address of the Chainlink feed /// @return price Retrieved price from the Chainlink feed @@ -33,7 +30,7 @@ library BeefyOracleChainlink { address chainlink = abi.decode(_data, (address)); try IChainlink(chainlink).decimals() returns (uint8) { try IChainlink(chainlink).latestAnswer() returns (int256) { - } catch { revert NoAnswer(); } - } catch { revert NoAnswer(); } + } catch { revert BeefyOracleErrors.NoAnswer(); } + } catch { revert BeefyOracleErrors.NoAnswer(); } } } diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracleErrors.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracleErrors.sol new file mode 100644 index 00000000..569c08c8 --- /dev/null +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracleErrors.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +/// @title Beefy Oracle Errors +/// @author Beefy, @kexley +/// @notice Error list for Beefy Oracles +contract BeefyOracleErrors { + + /// @dev No response from the Chainlink feed + error NoAnswer(); + + /// @dev No price for base token + /// @param token Base token + error NoBasePrice(address token); + + /// @dev Token is not present in the pair + /// @param token Input token + /// @param pair Pair token + error TokenNotInPair(address token, address pair); + + /// @dev Array length is not correct + error ArrayLength(); + +} diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracleHelper.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracleHelper.sol index 7c21a579..63ed027a 100644 --- a/contracts/BIFI/infra/BeefyOracle/BeefyOracleHelper.sol +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracleHelper.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; + import { IBeefyOracle } from "../../interfaces/oracle/IBeefyOracle.sol"; +import { BeefyOracleErrors } from "./BeefyOracleErrors.sol"; /// @title Beefy Oracle Helper /// @author Beefy, @kexley diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracleSolidly.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracleSolidly.sol index d689ce1e..88f70ef2 100644 --- a/contracts/BIFI/infra/BeefyOracle/BeefyOracleSolidly.sol +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracleSolidly.sol @@ -1,28 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; -import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; + import { ISolidlyPair} from "../../interfaces/common/ISolidlyPair.sol"; +import { BeefyOracleHelper, IBeefyOracle, BeefyOracleErrors } from "./BeefyOracleHelper.sol"; /// @title Beefy Oracle for Solidly /// @author Beefy, @kexley /// @notice On-chain oracle using Solidly library BeefyOracleSolidly { - /// @dev Array length is not correct - error ArrayLength(); - - /// @dev No price for base token - /// @param token Base token - error NoBasePrice(address token); - - /// @dev Token is not present in the pair - /// @param token Input token - /// @param pair Solidly pair - error TokenNotInPair(address token, address pair); - /// @notice Fetch price from the Solidly pairs using the TWAP observations /// @param _data Payload from the central oracle with the addresses of the token route, pool /// route and TWAP periods counted in 30 minute increments @@ -50,17 +39,17 @@ library BeefyOracleSolidly { abi.decode(_data, (address[], address[], uint256[])); if (tokens.length != pools.length + 1 || tokens.length != twapPeriods.length + 1) { - revert ArrayLength(); + revert BeefyOracleErrors.ArrayLength(); } uint256 basePrice = IBeefyOracle(msg.sender).getPrice(tokens[0]); - if (basePrice == 0) revert NoBasePrice(tokens[0]); + if (basePrice == 0) revert BeefyOracleErrors.NoBasePrice(tokens[0]); for (uint i; i < pools.length; i++) { address token = tokens[i]; address pool = pools[i]; if (token != ISolidlyPair(pool).token0() || token != ISolidlyPair(pool).token1()) { - revert TokenNotInPair(token, pool); + revert BeefyOracleErrors.TokenNotInPair(token, pool); } } } diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV2.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV2.sol index 9dc59966..6c4abaa9 100644 --- a/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV2.sol +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV2.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; + import { IUniswapV2Pair } from "../../interfaces/common/IUniswapV2Pair.sol"; -import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; +import { BeefyOracleHelper, IBeefyOracle, BeefyOracleErrors } from "./BeefyOracleHelper.sol"; /// @title Beefy Oracle for UniswapV2 /// @author Beefy, @kexley @@ -12,18 +13,6 @@ import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; /// @dev Observations are stored here as UniswapV2 pairs do not store historical observations contract BeefyOracleUniswapV2 { - /// @dev Array length is not correct - error ArrayLength(); - - /// @dev No price for base token - /// @param token Base token - error NoBasePrice(address token); - - /// @dev Token is not present in the pair - /// @param token Input token - /// @param pair UniswapV2 pair - error TokenNotInPair(address token, address pair); - /// @dev Struct of stored price averages and the most recent observation of a pair /// @param priceAverage0 Average price of token0 /// @param priceAverage1 Average price of token1 @@ -128,17 +117,17 @@ contract BeefyOracleUniswapV2 { abi.decode(_data, (address[], address[], uint256[])); if (tokens.length != pairs.length + 1 || tokens.length != twapPeriods.length + 1) { - revert ArrayLength(); + revert BeefyOracleErrors.ArrayLength(); } uint256 basePrice = IBeefyOracle(msg.sender).getPrice(tokens[0]); - if (basePrice == 0) revert NoBasePrice(); + if (basePrice == 0) revert BeefyOracleErrors.NoBasePrice(tokens[0]); for (uint i; i < pairs.length; i++) { address token = tokens[i]; address pair = pairs[i]; if (token != IUniswapV2Pair(pair).token0() || token != IUniswapV2Pair(pair).token1()) { - revert TokenNotInPair(); + revert BeefyOracleErrors.TokenNotInPair(token, pair); } } } diff --git a/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV3.sol b/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV3.sol index 159d967c..2dba04b8 100644 --- a/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV3.sol +++ b/contracts/BIFI/infra/BeefyOracle/BeefyOracleUniswapV3.sol @@ -1,28 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; -import { BeefyOracleHelper } from "./BeefyOracleHelper.sol"; -import { UniswapV3OracleLibrary } from "../../utils/UniswapV3OracleLibrary.sol"; + +import { UniswapV3OracleLibrary, IUniswapV3Pool } from "../../utils/UniswapV3OracleLibrary.sol"; +import { BeefyOracleHelper, IBeefyOracle, BeefyOracleErrors } from "./BeefyOracleHelper.sol"; /// @title Beefy Oracle for UniswapV3 /// @author Beefy, @kexley /// @notice On-chain oracle using UniswapV3 library BeefyOracleUniswapV3 { - /// @dev Array length is not correct - error ArrayLength(); - - /// @dev No price for base token - /// @param token Base token - error NoBasePrice(address token); - - /// @dev Token is not present in the pool - /// @param token Input token - /// @param pair UniswapV3 pool - error TokenNotInPool(address token, address pool); - /// @notice Fetch price from the UniswapV3 pools using the TWAP observations /// @param _data Payload from the central oracle with the addresses of the token route, pool /// route and TWAP periods in seconds @@ -61,16 +50,18 @@ library BeefyOracleUniswapV3 { (address[] memory tokens, address[] memory pools, uint256[] memory twapPeriods) = abi.decode(_data, (address[], address[], uint256[])); - if (tokens.length != pools.length + 1 || tokens.length != twapPeriods.length + 1) revert ArrayLength(); + if (tokens.length != pools.length + 1 || tokens.length != twapPeriods.length + 1) { + revert BeefyOracleErrors.ArrayLength(); + } uint256 basePrice = IBeefyOracle(msg.sender).getPrice(tokens[0]); - if (basePrice == 0) revert NoBasePrice(tokens[0]); + if (basePrice == 0) revert BeefyOracleErrors.NoBasePrice(tokens[0]); for (uint i; i < pools.length; i++) { address token = tokens[i]; address pool = pools[i]; if (token != IUniswapV3Pool(pool).token0() || token != IUniswapV3Pool(pool).token1()) { - revert TokenNotInPool(token, pool); + revert BeefyOracleErrors.TokenNotInPair(token, pool); } } } diff --git a/contracts/BIFI/infra/BeefyRewardPool.sol b/contracts/BIFI/infra/BeefyRewardPool.sol index 5ea7cb63..d965fcf9 100644 --- a/contracts/BIFI/infra/BeefyRewardPool.sol +++ b/contracts/BIFI/infra/BeefyRewardPool.sol @@ -2,186 +2,423 @@ pragma solidity 0.8.19; -import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { SafeERC20Upgradeable, IERC20Upgradeable, IERC20PermitUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import "../utils/LPTokenWrapperInitializable.sol"; - -contract BeefyRewardPool is LPTokenWrapperInitializable, OwnableUpgradeable { +/// @title Reward pool for BIFI +/// @author kexley, Beefy +/// @notice Multi-reward staking contract for BIFI +/// @dev Multiple rewards can be added to this contract by the owner. A receipt token is issued for +/// staking and is used for withdrawing the staked BIFI. +contract BeefyRewardPool is ERC20Upgradeable, OwnableUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; + /// @dev Information for a particular reward + /// @param periodFinish End timestamp of reward distribution + /// @param duration Distribution length of time in seconds + /// @param lastUpdateTime Latest timestamp of an update + /// @param rate Distribution speed in wei per second + /// @param rewardPerTokenStored Stored reward value per staked token in 18 decimals + /// @param userRewardPerTokenPaid Stored reward value per staked token in 18 decimals at the + /// last time a user was paid the reward + /// @param earned Value of reward still owed to the user struct RewardInfo { uint256 periodFinish; uint256 duration; uint256 lastUpdateTime; - uint256 rewardRate; + uint256 rate; uint256 rewardPerTokenStored; mapping(address => uint256) userRewardPerTokenPaid; - mapping(address => uint256) rewardsEarned; + mapping(address => uint256) earned; } - mapping(address => RewardInfo) public rewardInfo; - address[] public rewardTokens; - mapping(address => uint256) public rewardTokenIndex; - uint256 public rewardMax; + /// @notice BIFI token address + IERC20Upgradeable public stakedToken; + + /// @notice Array of reward addresses + address[] public rewards; + + /// @notice Whitelist of manager addresses + mapping(address => bool) public whitelisted; + + /// @notice Limit to the number of rewards an owner can add + uint256 private rewardMax; + + /// @notice Location of a reward in the reward array + mapping(address => uint256) private index; + + /// @dev Each reward address has a new unique identifier each time it is initialized. This is + /// to prevent old mappings from being reused when removing and re-adding a reward. + mapping(address => bytes32) private _id; - event RewardAdded(address indexed reward, uint256 amount); + /// @dev Each identifier relates to reward information + mapping(bytes32 => RewardInfo) private _rewardInfo; + + /// @notice User has staked an amount event Staked(address indexed user, uint256 amount); + /// @notice User has withdrawn an amount event Withdrawn(address indexed user, uint256 amount); + /// @notice A reward has been paid to the user event RewardPaid(address indexed user, address indexed reward, uint256 amount); + /// @notice A new reward has been added to be distributed + event AddReward(address reward); + /// @notice More of an existing reward has been added to be distributed + event NotifyReward(address indexed reward, uint256 amount, uint256 duration); + /// @notice A reward has been removed from distribution and sent to the recipient + event RemoveReward(address reward, address recipient); + /// @notice The owner has removed tokens that are not supported by this contract + event RescueTokens(address token, address recipient); + /// @notice An address has been added to or removed from the whitelist + event SetWhitelist(address manager, bool whitelist); - error EmptyStake(); - error EmptyWithdraw(); + /// @notice Caller is not a manager + error NotManager(address caller); + /// @notice The staked token cannot be added as a reward error StakedTokenIsNotAReward(); - error ShortDuration(); + /// @notice The duration is too short to be set + error ShortDuration(uint256 duration); + /// @notice There are already too many rewards error TooManyRewards(); - error OverNotify(); - error RewardNotFound(); + /// @notice The reward has not been found in the array + error RewardNotFound(address reward); + /// @notice The owner cannot withdraw the staked token error WithdrawingStakedToken(); - error WithdrawingRewardToken(); + /// @notice the owner cannot withdraw an existing reward without first removing it from the array + error WithdrawingRewardToken(address reward); - function initialize(address _stakedToken) external initializer { - __LPTokenWrapper_init(_stakedToken); - __Ownable_init(); - rewardMax = 10; - } - - modifier updateReward(address _account) { - for (uint i; i < rewardTokens.length; ++i) { - address reward = rewardTokens[i]; - rewardInfo[reward].rewardPerTokenStored = rewardPerToken(reward); - rewardInfo[reward].lastUpdateTime = lastTimeRewardApplicable(reward); - if (_account != address(0)) { - rewardInfo[reward].rewardsEarned[_account] = earned(_account, reward); - rewardInfo[reward].userRewardPerTokenPaid[_account] = - rewardInfo[reward].rewardPerTokenStored; - } - } + /// @dev Triggers reward updates on every user interaction + /// @param _user Address of the user making an interaction + modifier update(address _user) { + _update(_user); _; } - function lastTimeRewardApplicable(address _reward) public view returns (uint256) { - return - block.timestamp > rewardInfo[_reward].periodFinish - ? rewardInfo[_reward].periodFinish - : block.timestamp; + /// @dev Only a manager can call these modified functions + modifier onlyManager { + if (msg.sender != owner() || whitelisted[msg.sender]) revert NotManager(msg.sender); + _; } - function rewardPerToken(address _reward) public view returns (uint256) { - if (totalSupply() == 0) { - return rewardInfo[_reward].rewardPerTokenStored; - } - return - rewardInfo[_reward].rewardPerTokenStored + ( - (lastTimeRewardApplicable(_reward) - rewardInfo[_reward].lastUpdateTime) - * rewardInfo[_reward].rewardRate - * 1e18 - / totalSupply() - ); + /* ---------------------------------- EXTERNAL FUNCTIONS ---------------------------------- */ + + /// @notice Initialize the contract, callable only once + /// @param _stakedToken BIFI token address + function initialize(address _stakedToken) external initializer { + __ERC20_init("Beefy Reward Pool", "rBIFI"); + __Ownable_init(); + stakedToken = IERC20Upgradeable(_stakedToken); + rewardMax = 100; } - function earned(address _account, address _reward) public view returns (uint256) { - return - rewardInfo[_reward].rewardsEarned[_account] + ( - balanceOf(_account) * - (rewardPerToken(_reward) - rewardInfo[_reward].userRewardPerTokenPaid[_account]) - / 1e18 - ); + /// @notice Stake BIFI tokens + /// @dev An equal number of receipt tokens will be minted to the caller + /// @param _amount Amount of BIFI to stake + function stake(uint256 _amount) external update(msg.sender) { + _stake(_amount); } - function stake(uint256 _amount) public override updateReward(msg.sender) { - if (_amount == 0) revert EmptyStake(); - super.stake(_amount); - emit Staked(msg.sender, _amount); + /// @notice Stake BIFI tokens with a permit + /// @dev An equal number of receipt tokens will be minted to the caller + /// @param _amount Amount of BIFI to stake + /// @param _deadline Timestamp of the deadline after which the permit is invalid + /// @param _v Part of a signature + /// @param _r Part of a signature + /// @param _s Part of a signature + function stakeWithPermit( + uint256 _amount, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external update(msg.sender) { + IERC20PermitUpgradeable(address(stakedToken)).permit( + msg.sender, address(this), _amount, _deadline, _v, _r, _s + ); + _stake(_amount); } - function withdraw(uint256 _amount) public override updateReward(msg.sender) { - if (_amount == 0) revert EmptyWithdraw(); - super.withdraw(_amount); - emit Withdrawn(msg.sender, _amount); + /// @notice Withdraw BIFI tokens + /// @dev Burns an equal number of receipt tokens from the caller + /// @param _amount Amount of BIFI to withdraw + function withdraw(uint256 _amount) external update(msg.sender) { + _withdraw(_amount); } - function exit() external { - withdraw(balanceOf(msg.sender)); - getReward(); + /// @notice Withdraw all of the caller's BIFI tokens and claim rewards + /// @dev Burns all receipt tokens owned by the caller + function exit() external update(msg.sender) { + _withdraw(balanceOf(msg.sender)); + _getReward(); } - function getReward() public updateReward(msg.sender) { - for (uint i; i < rewardTokens.length; ++i) { - address reward = rewardTokens[i]; - uint256 rewardEarned = earned(msg.sender, reward); - if (rewardEarned > 0) { - rewardInfo[reward].rewardsEarned[msg.sender] = 0; - _safeRewardTransfer(reward, msg.sender, rewardEarned); - emit RewardPaid(msg.sender, reward, rewardEarned); - } - } + /// @notice Claim all the caller's earned rewards + function getReward() external update(msg.sender) { + _getReward(); } - function getReward(address _reward) external updateReward(msg.sender) { - uint256 rewardEarned = earned(msg.sender, _reward); - if (rewardEarned > 0) { - rewardInfo[_reward].rewardsEarned[msg.sender] = 0; - _safeRewardTransfer(_reward, msg.sender, rewardEarned); - emit RewardPaid(msg.sender, _reward, rewardEarned); + /// @notice View the amount of rewards earned by the user + /// @param _user User to view the earned rewards for + /// @return rewardTokens Address array of the rewards + /// @return earnedAmounts Amounts of the user's earned rewards + function earned(address _user) external view returns ( + address[] memory rewardTokens, + uint256[] memory earnedAmounts + ) { + uint256 rewardLength = rewards.length; + for (uint i; i < rewardLength;) { + earnedAmounts[i] = _earned(_user, rewards[i]); + unchecked { ++i; } } + rewardTokens = rewards; + } + + /// @notice View the amount of a single reward earned by the user + /// @param _user User to view the earned reward for + /// @param _reward Reward to calculate the earned amount for + /// @return earnedAmount Amount of the user's earned reward + function earned(address _user, address _reward) external view returns (uint256 earnedAmount) { + earnedAmount = _earned(_user, _reward); + } + + /// @notice View the reward information + /// @dev The active reward information is automatically selected from the id mapping + /// @param _reward Address of the reward to get the information for + /// @return periodFinish End timestamp of reward distribution + /// @return duration Distribution length of time in seconds + /// @return lastUpdateTime Latest timestamp of an update + /// @return rate Distribution speed in wei per second + function rewardInfo(address _reward) external view returns ( + uint256 periodFinish, + uint256 duration, + uint256 lastUpdateTime, + uint256 rate + ) { + RewardInfo storage info = _getRewardInfo(_reward); + periodFinish = info.periodFinish; + duration = info.duration; + lastUpdateTime = info.lastUpdateTime; + rate = info.rate; } + /* ------------------------------- ERC20 OVERRIDE FUNCTIONS ------------------------------- */ + + /// @notice Update rewards for both source and recipient and then transfer receipt tokens to + /// the recipient address + /// @dev Overrides the ERC20 implementation to add the reward update + /// @param _to Recipient address of the token transfer + /// @param _value Amount to transfer + /// @return success Transfer was successful or not + function transfer(address _to, uint256 _value) public override returns (bool success) { + _update(msg.sender); + _update(_to); + return super.transfer(_to, _value); + } + + /// @notice Update rewards for both source and recipient and then transfer receipt tokens from + /// the source address to the recipient address + /// @dev Overrides the ERC20 implementation to add the reward update + /// @param _from Source address of the token transfer + /// @param _to Recipient address of the token transfer + /// @param _value Amount to transfer + /// @return success Transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) public override returns (bool) { + _update(_from); + _update(_to); + return super.transferFrom(_from, _to, _value); + } + + /* ----------------------------------- OWNER FUNCTIONS ------------------------------------ */ + + /// @notice Manager function to start a reward distribution + /// @dev Must approve this contract to spend the reward amount before calling this function. + /// New rewards will be assigned a id using their address and the block timestamp. + /// @param _reward Address of the reward + /// @param _amount Amount of reward + /// @param _duration Duration of the reward distribution in seconds function notifyRewardAmount( address _reward, uint256 _amount, uint256 _duration - ) external onlyOwner updateReward(address(0)) { + ) external onlyManager update(address(0)) { if (_reward == address(stakedToken)) revert StakedTokenIsNotAReward(); - if (_duration < 1 days) revert ShortDuration(); - if (_reward != rewardTokens[rewardTokenIndex[_reward]]) { - rewardTokenIndex[_reward] = rewardTokens.length; - rewardTokens.push(_reward); - if (rewardTokens.length > rewardMax) revert TooManyRewards(); + if (_duration < 1 days) revert ShortDuration(_duration); + + if (!_rewardExists(_reward)) { + _id[_reward] = keccak256(abi.encodePacked(_reward, block.timestamp)); + uint256 rewardLength = rewards.length; + if (rewards.length + 1 > rewardMax) revert TooManyRewards(); + index[_reward] = rewardLength; + rewards.push(_reward); + emit AddReward(_reward); } + IERC20Upgradeable(_reward).safeTransferFrom(msg.sender, address(this), _amount); + + RewardInfo storage rewardData = _getRewardInfo(_reward); uint256 leftover; - if (block.timestamp < rewardInfo[_reward].periodFinish) { - uint256 remaining = rewardInfo[_reward].periodFinish - block.timestamp; - leftover = remaining * rewardInfo[_reward].rewardRate; - } - if (_amount + leftover > IERC20Upgradeable(_reward).balanceOf(address(this))) revert OverNotify(); - rewardInfo[_reward].rewardRate = (_amount + leftover) / _duration; - rewardInfo[_reward].lastUpdateTime = block.timestamp; - rewardInfo[_reward].periodFinish = block.timestamp + _duration; - rewardInfo[_reward].duration = _duration; - emit RewardAdded(_reward, _amount); - } - - function removeReward(address _reward) external onlyOwner updateReward(address(0)) { - if (_reward != rewardTokens[rewardTokenIndex[_reward]]) revert RewardNotFound(); - if (block.timestamp < rewardInfo[_reward].periodFinish) { - uint256 remaining = rewardInfo[_reward].periodFinish - block.timestamp; - uint256 leftover = remaining * rewardInfo[_reward].rewardRate; - if (leftover > 0) IERC20Upgradeable(_reward).safeTransfer(owner(), leftover); - rewardInfo[_reward].periodFinish = block.timestamp; + + if (block.timestamp < rewardData.periodFinish) { + uint256 remaining = rewardData.periodFinish - block.timestamp; + leftover = remaining * rewardData.rate; } - address endToken = rewardTokens[rewardTokens.length - 1]; - rewardTokenIndex[endToken] = rewardTokenIndex[_reward]; - rewardTokens[rewardTokenIndex[_reward]] = endToken; - rewardTokens.pop(); + + rewardData.rate = (_amount + leftover) / _duration; + rewardData.lastUpdateTime = block.timestamp; + rewardData.periodFinish = block.timestamp + _duration; + rewardData.duration = _duration; + + emit NotifyReward(_reward, _amount, _duration); } - function inCaseTokensGetStuck(address _token) external onlyOwner { + /// @notice Owner function to remove a reward from this contract + /// @dev All unclaimed earnings are ignored. Re-adding the reward will have a new set of + /// reward information so any unclaimed earnings cannot be recovered + /// @param _reward Address of the reward to be removed + /// @param _recipient Address of the recipient that the removed reward was sent to + function removeReward(address _reward, address _recipient) external onlyOwner { + if (!_rewardExists(_reward)) revert RewardNotFound(_reward); + + uint256 replacedIndex = index[_reward]; + address endToken = rewards[rewards.length - 1]; + rewards[replacedIndex] = endToken; + index[endToken] = replacedIndex; + rewards.pop(); + + uint256 rewardBal = IERC20Upgradeable(_reward).balanceOf(address(this)); + IERC20Upgradeable(_reward).safeTransfer(_recipient, rewardBal); + + emit RemoveReward(_reward, _recipient); + } + + /// @notice Owner function to remove unsupported tokens sent to this contract + /// @param _token Address of the token to be removed + /// @param _recipient Address of the recipient that the removed token was sent to + function rescueTokens(address _token, address _recipient) external onlyOwner { if (_token == address(stakedToken)) revert WithdrawingStakedToken(); - if (_token == rewardTokens[rewardTokenIndex[_token]]) revert WithdrawingRewardToken(); + if (_rewardExists(_token)) revert WithdrawingRewardToken(_token); uint256 amount = IERC20Upgradeable(_token).balanceOf(address(this)); - IERC20Upgradeable(_token).safeTransfer(owner(), amount); + IERC20Upgradeable(_token).safeTransfer(_recipient, amount); + emit RescueTokens(_token, _recipient); } - function _safeRewardTransfer(address _reward, address _recipient, uint256 _amount) internal { - uint256 rewardBal = IERC20Upgradeable(_reward).balanceOf(address(this)); - if (_amount > rewardBal) { - _amount = rewardBal; + /// @notice Owner function to add addresses to the whitelist + /// @param _manager Address able to call manager functions + /// @param _whitelisted Whether to add or remove from whitelist + function setWhitelist(address _manager, bool _whitelisted) external onlyOwner { + whitelisted[_manager] = _whitelisted; + emit SetWhitelist(_manager, _whitelisted); + } + + /* ---------------------------------- INTERNAL FUNCTIONS ---------------------------------- */ + + /// @dev Update the rewards and earnings for a user + /// @param _user Address to update the earnings for + function _update(address _user) private { + uint256 rewardLength = rewards.length; + for (uint i; i < rewardLength;) { + address reward = rewards[i]; + RewardInfo storage rewardData = _getRewardInfo(reward); + rewardData.rewardPerTokenStored = _rewardPerToken(reward); + rewardData.lastUpdateTime = _lastTimeRewardApplicable(rewardData.periodFinish); + if (_user != address(0)) { + rewardData.earned[_user] = _earned(_user, reward); + rewardData.userRewardPerTokenPaid[_user] = rewardData.rewardPerTokenStored; + } + unchecked { ++i; } + } + } + + /// @dev Stake BIFI tokens and mint the caller receipt tokens + /// @param _amount Amount of BIFI to stake + function _stake(uint256 _amount) private { + _mint(msg.sender, _amount); + stakedToken.safeTransferFrom(msg.sender, address(this), _amount); + emit Staked(msg.sender, _amount); + } + + /// @dev Withdraw BIFI tokens and burn an equal number of receipt tokens from the caller + /// @param _amount Amount of BIFI to withdraw + function _withdraw(uint256 _amount) private { + _burn(msg.sender, _amount); + stakedToken.safeTransfer(msg.sender, _amount); + emit Withdrawn(msg.sender, _amount); + } + + /// @dev Claim all the caller's earned rewards + function _getReward() private { + uint256 rewardLength = rewards.length; + for (uint i; i < rewardLength;) { + address reward = rewards[i]; + uint256 rewardEarned = _earned(msg.sender, reward); + if (rewardEarned > 0) { + _getRewardInfo(reward).earned[msg.sender] = 0; + _rewardTransfer(reward, msg.sender, rewardEarned); + emit RewardPaid(msg.sender, reward, rewardEarned); + } + unchecked { ++i; } } - if (_amount > 0) { - IERC20Upgradeable(_reward).safeTransfer(_recipient, _amount); + } + + /// @dev Return either the period finish or the current timestamp, whichever is earliest + /// @param _periodFinish End timestamp of the reward distribution + /// @return timestamp Earliest timestamp out of the period finish or block timestamp + function _lastTimeRewardApplicable(uint256 _periodFinish) private view returns (uint256 timestamp) { + timestamp = block.timestamp > _periodFinish ? _periodFinish : block.timestamp; + } + + /// @dev Calculate the reward amount per BIFI token + /// @param _reward Address of the reward + /// @return rewardPerToken Reward amount per BIFI token + function _rewardPerToken(address _reward) private view returns (uint256 rewardPerToken) { + RewardInfo storage rewardData = _getRewardInfo(_reward); + if (totalSupply() == 0) { + rewardPerToken = rewardData.rewardPerTokenStored; + } else { + rewardPerToken = rewardData.rewardPerTokenStored + ( + (_lastTimeRewardApplicable(rewardData.periodFinish) - rewardData.lastUpdateTime) + * rewardData.rate + * 1e18 + / totalSupply() + ); } } + + /// @dev Calculate the reward amount earned by the user + /// @param _user Address of the user + /// @param _reward Address of the reward + /// @return earnedAmount Amount of reward earned by the user + function _earned(address _user, address _reward) private view returns (uint256 earnedAmount) { + RewardInfo storage rewardData = _getRewardInfo(_reward); + earnedAmount = rewardData.earned[_user] + ( + balanceOf(_user) * + (_rewardPerToken(_reward) - rewardData.userRewardPerTokenPaid[_user]) + / 1e18 + ); + } + + /// @dev Return the most current reward information for a reward + /// @param _reward Address of the reward + /// @return info Reward information for the reward + function _getRewardInfo(address _reward) private view returns(RewardInfo storage info) { + info = _rewardInfo[_id[_reward]]; + } + + /// @dev Check if a reward exists in the reward array already + /// @param _reward Address of the reward + /// @return exists Returns true if token is in the array + function _rewardExists(address _reward) private view returns (bool exists) { + exists = _reward == rewards[index[_reward]]; + } + + /// @dev Transfer at most the balance of the reward on this contract to avoid errors + /// @param _reward Address of the reward + /// @param _recipient Address of the recipient of the reward + /// @param _amount Amount of the reward to be sent to the recipient + function _rewardTransfer(address _reward, address _recipient, uint256 _amount) private { + uint256 rewardBal = IERC20Upgradeable(_reward).balanceOf(address(this)); + if (_amount > rewardBal) _amount = rewardBal; + if (_amount > 0) IERC20Upgradeable(_reward).safeTransfer(_recipient, _amount); + } } diff --git a/contracts/BIFI/infra/BeefySwapper.sol b/contracts/BIFI/infra/BeefySwapper.sol index 0d24c8df..7fe1037a 100644 --- a/contracts/BIFI/infra/BeefySwapper.sol +++ b/contracts/BIFI/infra/BeefySwapper.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.19; import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + import { IBeefyOracle } from "../interfaces/oracle/IBeefyOracle.sol"; import { BytesLib } from "../utils/BytesLib.sol"; /// @title Beefy Swapper /// @author Beefy, @kexley -/// @notice Centralized swapper for strategies +/// @notice Centralized swapper contract BeefySwapper is OwnableUpgradeable { using SafeERC20Upgradeable for IERC20MetadataUpgradeable; using BytesLib for bytes; @@ -19,11 +20,21 @@ contract BeefySwapper is OwnableUpgradeable { /// @param token Address of token that failed the price update error PriceFailed(address token); + /// @dev No swap data has been set by the owner + /// @param fromToken Token to swap from + /// @param toToken Token to swap to + error NoSwapData(address fromToken, address toToken); + /// @dev Swap call failed /// @param router Target address of the failed swap call /// @param data Payload of the failed call error SwapFailed(address router, bytes data); + /// @dev Not enough output was returned from the swap + /// @param amountOut Amount returned by the swap + /// @param minAmountOut Minimum amount required from the swap + error SlippageExceeded(uint256 amountOut, uint256 minAmountOut); + /// @dev Stored data for a swap /// @param router Target address that will handle the swap /// @param data Payload of a template swap between the two tokens @@ -66,15 +77,15 @@ contract BeefySwapper is OwnableUpgradeable { /// swap the _toToken token is sent directly to the caller /// @param _fromToken Token to swap from /// @param _toToken Token to swap to - /// @param _amount Amount of _fromToken to use in the swap - /// @return outputAmount Amount of _toToken returned to the caller + /// @param _amountIn Amount of _fromToken to use in the swap + /// @return amountOut Amount of _toToken returned to the caller function swap( address _fromToken, address _toToken, - uint256 _amount - ) external returns (uint256 outputAmount) { - uint256 minAmountOut = _getMinAmount(_fromToken, _toToken, _amount); - outputAmount = _swap(_fromToken, _toToken, _amount, minAmountOut); + uint256 _amountIn + ) external returns (uint256 amountOut) { + uint256 minAmountOut = _getAmountOut(_fromToken, _toToken, _amountIn); + amountOut = _swap(_fromToken, _toToken, _amountIn, minAmountOut); } /// @notice Swap between two tokens with slippage provided by the caller @@ -82,77 +93,92 @@ contract BeefySwapper is OwnableUpgradeable { /// swap the _toToken token is sent directly to the caller /// @param _fromToken Token to swap from /// @param _toToken Token to swap to - /// @param _amount Amount of _fromToken to use in the swap + /// @param _amountIn Amount of _fromToken to use in the swap /// @param _minAmountOut Minimum amount of _toToken that is acceptable to be returned to caller - /// @return outputAmount Amount of _toToken returned to the caller + /// @return amountOut Amount of _toToken returned to the caller function swap( address _fromToken, address _toToken, - uint256 _amount, + uint256 _amountIn, uint256 _minAmountOut - ) external returns (uint256 outputAmount) { - outputAmount = _swap(_fromToken, _toToken, _amount, _minAmountOut); + ) external returns (uint256 amountOut) { + amountOut = _swap(_fromToken, _toToken, _amountIn, _minAmountOut); + } + + /// @notice Get the amount out from a simulated swap with slippage and non-fresh prices + /// @param _fromToken Token to swap from + /// @param _toToken Token to swap to + /// @param _amountIn Amount of _fromToken to use in the swap + /// @return amountOut Amount of _toTokens returned from the swap + function getAmountOut( + address _fromToken, + address _toToken, + uint256 _amountIn + ) external view returns (uint256 amountOut) { + (uint256 fromPrice, uint256 toPrice) = _getPrice(_fromToken, _toToken); + uint8 decimals0 = IERC20MetadataUpgradeable(_fromToken).decimals(); + uint8 decimals1 = IERC20MetadataUpgradeable(_toToken).decimals(); + + amountOut = _calculateAmountOut(_amountIn, fromPrice, toPrice, decimals0, decimals1); } /// @dev Use the oracle to get prices for both _fromToken and _toToken and calculate the /// estimated output reduced by the slippage /// @param _fromToken Token to swap from /// @param _toToken Token to swap to - /// @param _amount Amount of _fromToken to use in the swap - /// @return minAmountOut Minimum amount of _toToken that is acceptable to be returned to caller - function _getMinAmount( + /// @param _amountIn Amount of _fromToken to use in the swap + /// @return amountOut Amount of _toToken returned by the swap + function _getAmountOut( address _fromToken, address _toToken, - uint256 _amount - ) private returns (uint256 minAmountOut) { - address[] memory tokens = new address[](2); - (tokens[0], tokens[1]) = (_fromToken, _toToken); - (uint256[] memory prices, bool[] memory successes) = oracle.getFreshPrice(tokens); - for (uint i; i < successes.length;) { - if (!successes[i]) revert PriceFailed(tokens[i]); - unchecked { i++; } - } + uint256 _amountIn + ) private returns (uint256 amountOut) { + (uint256 fromPrice, uint256 toPrice) = _getFreshPrice(_fromToken, _toToken); + uint8 decimals0 = IERC20MetadataUpgradeable(_fromToken).decimals(); + uint8 decimals1 = IERC20MetadataUpgradeable(_toToken).decimals(); + uint256 slippedAmountIn = _amountIn * slippage / 1 ether; - minAmountOut = (prices[0] * _amount * 10 ** IERC20MetadataUpgradeable(_toToken).decimals() * slippage) / - (prices[1] * 10 ** IERC20MetadataUpgradeable(_fromToken).decimals() * 1 ether); + amountOut = _calculateAmountOut(slippedAmountIn, fromPrice, toPrice, decimals0, decimals1); } /// @dev _fromToken is pulled into this contract from the caller, swap is executed according to /// the stored data, resulting _toTokens are sent to the caller /// @param _fromToken Token to swap from /// @param _toToken Token to swap to - /// @param _amount Amount of _fromToken to use in the swap + /// @param _amountIn Amount of _fromToken to use in the swap /// @param _minAmountOut Minimum amount of _toToken that is acceptable to be returned to caller - /// @return outputAmount Amount of _toToken returned to the caller + /// @return amountOut Amount of _toToken returned to the caller function _swap( address _fromToken, address _toToken, - uint256 _amount, + uint256 _amountIn, uint256 _minAmountOut - ) private returns (uint256 outputAmount) { - IERC20MetadataUpgradeable(_fromToken).safeTransferFrom(msg.sender, address(this), _amount); - _executeSwap(_fromToken, _toToken, _amount, _minAmountOut); - outputAmount = IERC20MetadataUpgradeable(_toToken).balanceOf(address(this)); - IERC20MetadataUpgradeable(_toToken).safeTransfer(msg.sender, outputAmount); + ) private returns (uint256 amountOut) { + IERC20MetadataUpgradeable(_fromToken).safeTransferFrom(msg.sender, address(this), _amountIn); + _executeSwap(_fromToken, _toToken, _amountIn, _minAmountOut); + amountOut = IERC20MetadataUpgradeable(_toToken).balanceOf(address(this)); + if (amountOut < _minAmountOut) revert SlippageExceeded(amountOut, _minAmountOut); + IERC20MetadataUpgradeable(_toToken).safeTransfer(msg.sender, amountOut); } /// @dev Fetch the stored swap info for the route between the two tokens, insert the encoded /// balance and minimum output to the payload and call the stored router with the data /// @param _fromToken Token to swap from /// @param _toToken Token to swap to - /// @param _amount Amount of _fromToken to use in the swap + /// @param _amountIn Amount of _fromToken to use in the swap /// @param _minAmountOut Minimum amount of _toToken that is acceptable to be returned to caller function _executeSwap( address _fromToken, address _toToken, - uint256 _amount, + uint256 _amountIn, uint256 _minAmountOut ) private { SwapInfo memory swapData = swapInfo[_fromToken][_toToken]; address router = swapData.router; + if (router == address(0)) revert NoSwapData(_fromToken, _toToken); bytes memory data = swapData.data; - data = _insertData(data, swapData.amountIndex, abi.encode(_amount)); + data = _insertData(data, swapData.amountIndex, abi.encode(_amountIn)); bytes memory minAmountData = swapData.minAmountSign >= 0 ? abi.encode(_minAmountOut) @@ -184,6 +210,62 @@ contract BeefySwapper is OwnableUpgradeable { ); } + /// @dev Fetch non-fresh prices from the oracle + /// @param _fromToken Token to swap from + /// @param _toToken Token to swap to + /// @return fromPrice Price of token to swap from + /// @return toPrice Price of token to swap to + function _getPrice( + address _fromToken, + address _toToken + ) private view returns (uint256 fromPrice, uint256 toPrice) { + address[] memory tokens = new address[](2); + (tokens[0], tokens[1]) = (_fromToken, _toToken); + + uint256[] memory prices = oracle.getPrice(tokens); + (fromPrice, toPrice) = (prices[0], prices[1]); + } + + /// @dev Fetch fresh prices from the oracle + /// @param _fromToken Token to swap from + /// @param _toToken Token to swap to + /// @return fromPrice Price of token to swap from + /// @return toPrice Price of token to swap to + function _getFreshPrice( + address _fromToken, + address _toToken + ) private returns (uint256 fromPrice, uint256 toPrice) { + address[] memory tokens = new address[](2); + (tokens[0], tokens[1]) = (_fromToken, _toToken); + uint256[] memory prices = new uint256[](2); + bool[] memory successes = new bool[](2); + + (prices, successes) = oracle.getFreshPrice(tokens); + for (uint i; i < successes.length;) { + if (!successes[i]) revert PriceFailed(tokens[i]); + unchecked { ++i; } + } + (fromPrice, toPrice) = (prices[0], prices[1]); + } + + /// @dev Calculate the amount out given the prices and the decimals of the tokens involved + /// @param _amountIn Amount of _fromToken to use in the swap + /// @param _price0 Price of the _fromToken + /// @param _price1 Price of the _toToken + /// @param _decimals0 Decimals of the _fromToken + /// @param _decimals1 Decimals of the _toToken + function _calculateAmountOut( + uint256 _amountIn, + uint256 _price0, + uint256 _price1, + uint8 _decimals0, + uint8 _decimals1 + ) private pure returns (uint256 amountOut) { + amountOut = _amountIn * (_price0 * 10 ** _decimals1) / (_price1 * 10 ** _decimals0); + } + + /* ----------------------------------- OWNER FUNCTIONS ----------------------------------- */ + /// @notice Owner function to set the stored swap info for the route between two tokens /// @dev No validation checks /// @param _fromToken Token to swap from diff --git a/contracts/BIFI/interfaces/beefy/IBeefyRewardPool.sol b/contracts/BIFI/interfaces/beefy/IBeefyRewardPool.sol new file mode 100644 index 00000000..ff751116 --- /dev/null +++ b/contracts/BIFI/interfaces/beefy/IBeefyRewardPool.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +interface IBeefyRewardPool { + function notifyRewardAmount(address reward, uint256 amount, uint256 duration) external; + function removeReward(address reward, address recipient) external; + function rescueTokens(address token, address recipient) external; + function setWhitelist(address manager, bool whitelisted) external; + function transferOwnership(address owner) external; +} diff --git a/contracts/BIFI/interfaces/beefy/IBeefySwapper.sol b/contracts/BIFI/interfaces/beefy/IBeefySwapper.sol new file mode 100644 index 00000000..8ee254de --- /dev/null +++ b/contracts/BIFI/interfaces/beefy/IBeefySwapper.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +interface IBeefySwapper { + function swap( + address fromToken, + address toToken, + uint256 amountIn + ) external returns (uint256 amountOut); + + function swap( + address fromToken, + address toToken, + uint256 amountIn, + uint256 minAmountOut + ) external returns (uint256 amountOut); + + function getAmountOut( + address _fromToken, + address _toToken, + uint256 _amountIn + ) external view returns (uint256 amountOut); +} diff --git a/contracts/BIFI/interfaces/common/IWrappedNative.sol b/contracts/BIFI/interfaces/common/IWrappedNative.sol index 33a66ce7..a286d33e 100644 --- a/contracts/BIFI/interfaces/common/IWrappedNative.sol +++ b/contracts/BIFI/interfaces/common/IWrappedNative.sol @@ -4,6 +4,5 @@ pragma solidity >=0.6.0 <0.9.0; interface IWrappedNative { function deposit() external payable; - function withdraw(uint256 wad) external; }