From 81929e2fe70a63e525a7ac38c807e148b2406391 Mon Sep 17 00:00:00 2001 From: ryanycw Date: Fri, 27 Oct 2023 03:05:23 +0800 Subject: [PATCH] build: import controller --- src/controller/Controller.sol | 322 ++++++++++++++++++ src/controller/RewardForwarder.sol | 80 +++++ src/controller/inheritance/Controllable.sol | 22 ++ .../inheritance/ControllableInit.sol | 30 ++ src/controller/inheritance/Governable.sol | 27 ++ src/controller/inheritance/GovernableInit.sol | 49 +++ src/controller/inheritance/IUpgradeSource.sol | 7 + .../inheritance/OwnableWhitelist.sol | 17 + src/controller/inheritance/Storage.sol | 34 ++ src/controller/interface/IController.sol | 127 +++++++ .../interface/IProfitSharingReceiver.sol | 8 + src/controller/interface/IRewardForwarder.sol | 51 +++ src/controller/interface/IStrategy.sol | 36 ++ .../interface/IUniversalLiquidator.sol | 17 + src/controller/interface/IVault.sol | 53 +++ .../ReentrancyGuardUpgradeable.sol | 49 +++ 16 files changed, 929 insertions(+) create mode 100644 src/controller/Controller.sol create mode 100644 src/controller/RewardForwarder.sol create mode 100644 src/controller/inheritance/Controllable.sol create mode 100644 src/controller/inheritance/ControllableInit.sol create mode 100644 src/controller/inheritance/Governable.sol create mode 100644 src/controller/inheritance/GovernableInit.sol create mode 100644 src/controller/inheritance/IUpgradeSource.sol create mode 100644 src/controller/inheritance/OwnableWhitelist.sol create mode 100644 src/controller/inheritance/Storage.sol create mode 100644 src/controller/interface/IController.sol create mode 100644 src/controller/interface/IProfitSharingReceiver.sol create mode 100644 src/controller/interface/IRewardForwarder.sol create mode 100644 src/controller/interface/IStrategy.sol create mode 100644 src/controller/interface/IUniversalLiquidator.sol create mode 100644 src/controller/interface/IVault.sol create mode 100644 src/controller/upgradability/ReentrancyGuardUpgradeable.sol diff --git a/src/controller/Controller.sol b/src/controller/Controller.sol new file mode 100644 index 0000000..57028c9 --- /dev/null +++ b/src/controller/Controller.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./inheritance/Governable.sol"; + +import "./interface/IController.sol"; +import "./interface/IStrategy.sol"; +import "./interface/IVault.sol"; + +import "./RewardForwarder.sol"; + +contract Controller is Governable { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // ========================= Fields ========================= + + // external parties + address public targetToken; + address public protocolFeeReceiver; + address public profitSharingReceiver; + address public rewardForwarder; + address public universalLiquidator; + address public dolomiteYieldFarmingRouter; + + uint256 public nextImplementationDelay; + + /// 15% of fees captured go to iFARM stakers + uint256 public profitSharingNumerator = 700; + uint256 public nextProfitSharingNumerator = 0; + uint256 public nextProfitSharingNumeratorTimestamp = 0; + + /// 5% of fees captured go to strategists + uint256 public strategistFeeNumerator = 0; + uint256 public nextStrategistFeeNumerator = 0; + uint256 public nextStrategistFeeNumeratorTimestamp = 0; + + /// 5% of fees captured go to the devs of the platform + uint256 public platformFeeNumerator = 300; + uint256 public nextPlatformFeeNumerator = 0; + uint256 public nextPlatformFeeNumeratorTimestamp = 0; + + /// used for queuing a new delay + uint256 public tempNextImplementationDelay = 0; + uint256 public tempNextImplementationDelayTimestamp = 0; + + uint256 public constant MAX_TOTAL_FEE = 3000; + uint256 public constant FEE_DENOMINATOR = 10000; + + /// @notice This mapping allows certain contracts to stake on a user's behalf + mapping(address => bool) public addressWhitelist; + mapping(bytes32 => bool) public codeWhitelist; + + // All eligible hardWorkers that we have + mapping(address => bool) public hardWorkers; + + // ========================= Events ========================= + + event QueueProfitSharingChange(uint256 profitSharingNumerator, uint256 validAtTimestamp); + event ConfirmProfitSharingChange(uint256 profitSharingNumerator); + + event QueueStrategistFeeChange(uint256 strategistFeeNumerator, uint256 validAtTimestamp); + event ConfirmStrategistFeeChange(uint256 strategistFeeNumerator); + + event QueuePlatformFeeChange(uint256 platformFeeNumerator, uint256 validAtTimestamp); + event ConfirmPlatformFeeChange(uint256 platformFeeNumerator); + + event QueueNextImplementationDelay(uint256 implementationDelay, uint256 validAtTimestamp); + event ConfirmNextImplementationDelay(uint256 implementationDelay); + + event AddedAddressToWhitelist(address indexed _address); + event RemovedAddressFromWhitelist(address indexed _address); + + event AddedCodeToWhitelist(address indexed _address); + event RemovedCodeFromWhitelist(address indexed _address); + + event SharePriceChangeLog( + address indexed vault, address indexed strategy, uint256 oldSharePrice, uint256 newSharePrice, uint256 timestamp + ); + + // ========================= Modifiers ========================= + + modifier onlyHardWorkerOrGovernance() { + require(hardWorkers[msg.sender] || (msg.sender == governance()), "only hard worker can call this"); + _; + } + + constructor( + address _storage, + address _targetToken, + address _protocolFeeReceiver, + address _profitSharingReceiver, + address _rewardForwarder, + address _universalLiquidator, + uint256 _nextImplementationDelay + ) Governable(_storage) { + require(_targetToken != address(0), "_targetToken should not be empty"); + require(_protocolFeeReceiver != address(0), "_protocolFeeReceiver should not be empty"); + require(_profitSharingReceiver != address(0), "_profitSharingReceiver should not be empty"); + require(_rewardForwarder != address(0), "_rewardForwarder should not be empty"); + require(_nextImplementationDelay > 0, "_nextImplementationDelay should be gt 0"); + + targetToken = _targetToken; + protocolFeeReceiver = _protocolFeeReceiver; + profitSharingReceiver = _profitSharingReceiver; + rewardForwarder = _rewardForwarder; + universalLiquidator = _universalLiquidator; + nextImplementationDelay = _nextImplementationDelay; + } + + // [Grey list] + // An EOA can safely interact with the system no matter what. + // If you're using Metamask, you're using an EOA. + // Only smart contracts may be affected by this grey list. + // + // This contract will not be able to ban any EOA from the system + // even if an EOA is being added to the greyList, he/she will still be able + // to interact with the whole system as if nothing happened. + // Only smart contracts will be affected by being added to the greyList. + function greyList(address _addr) public view returns (bool) { + return !addressWhitelist[_addr] && !codeWhitelist[getContractHash(_addr)]; + } + + // Only smart contracts will be affected by the whitelist. + function addToWhitelist(address _target) public onlyGovernance { + addressWhitelist[_target] = true; + emit AddedAddressToWhitelist(_target); + } + + function addMultipleToWhitelist(address[] memory _targets) public onlyGovernance { + for (uint256 i = 0; i < _targets.length; i++) { + addressWhitelist[_targets[i]] = true; + } + } + + function removeFromWhitelist(address _target) public onlyGovernance { + addressWhitelist[_target] = false; + emit RemovedAddressFromWhitelist(_target); + } + + function removeMultipleFromWhitelist(address[] memory _targets) public onlyGovernance { + for (uint256 i = 0; i < _targets.length; i++) { + addressWhitelist[_targets[i]] = false; + } + } + + function getContractHash(address a) public view returns (bytes32 hash) { + assembly { + hash := extcodehash(a) + } + } + + function addCodeToWhitelist(address _target) public onlyGovernance { + codeWhitelist[getContractHash(_target)] = true; + emit AddedCodeToWhitelist(_target); + } + + function removeCodeFromWhitelist(address _target) public onlyGovernance { + codeWhitelist[getContractHash(_target)] = false; + emit RemovedCodeFromWhitelist(_target); + } + + function setRewardForwarder(address _rewardForwarder) public onlyGovernance { + require(_rewardForwarder != address(0), "new reward forwarder should not be empty"); + rewardForwarder = _rewardForwarder; + } + + function setTargetToken(address _targetToken) public onlyGovernance { + require(_targetToken != address(0), "new target token should not be empty"); + targetToken = _targetToken; + } + + function setProfitSharingReceiver(address _profitSharingReceiver) public onlyGovernance { + require(_profitSharingReceiver != address(0), "new profit sharing receiver should not be empty"); + profitSharingReceiver = _profitSharingReceiver; + } + + function setProtocolFeeReceiver(address _protocolFeeReceiver) public onlyGovernance { + require(_protocolFeeReceiver != address(0), "new protocol fee receiver should not be empty"); + protocolFeeReceiver = _protocolFeeReceiver; + } + + function setUniversalLiquidator(address _universalLiquidator) public onlyGovernance { + require(_universalLiquidator != address(0), "new universal liquidator should not be empty"); + universalLiquidator = _universalLiquidator; + } + + function setDolomiteYieldFarmingRouter(address _dolomiteYieldFarmingRouter) public onlyGovernance { + require(_dolomiteYieldFarmingRouter != address(0), "new reward forwarder should not be empty"); + dolomiteYieldFarmingRouter = _dolomiteYieldFarmingRouter; + } + + function getPricePerFullShare(address _vault) public view returns (uint256) { + return IVault(_vault).getPricePerFullShare(); + } + + function doHardWork(address _vault) external onlyHardWorkerOrGovernance { + uint256 oldSharePrice = IVault(_vault).getPricePerFullShare(); + IVault(_vault).doHardWork(); + emit SharePriceChangeLog( + _vault, IVault(_vault).strategy(), oldSharePrice, IVault(_vault).getPricePerFullShare(), block.timestamp + ); + } + + function addHardWorker(address _worker) public onlyGovernance { + require(_worker != address(0), "_worker must be defined"); + hardWorkers[_worker] = true; + } + + function removeHardWorker(address _worker) public onlyGovernance { + require(_worker != address(0), "_worker must be defined"); + hardWorkers[_worker] = false; + } + + // transfers token in the controller contract to the governance + function salvage(address _token, uint256 _amount) external onlyGovernance { + IERC20(_token).safeTransfer(governance(), _amount); + } + + function salvageStrategy(address _strategy, address _token, uint256 _amount) external onlyGovernance { + // the strategy is responsible for maintaining the list of + // salvageable tokens, to make sure that governance cannot come + // in and take away the coins + IStrategy(_strategy).salvageToken(governance(), _token, _amount); + } + + function feeDenominator() public pure returns (uint256) { + // keep the interface for this function as a `view` for now, in case it changes in the future + return FEE_DENOMINATOR; + } + + function setProfitSharingNumerator(uint256 _profitSharingNumerator) public onlyGovernance { + require(_profitSharingNumerator + strategistFeeNumerator + platformFeeNumerator <= MAX_TOTAL_FEE, "total fee too high"); + + nextProfitSharingNumerator = _profitSharingNumerator; + nextProfitSharingNumeratorTimestamp = block.timestamp + nextImplementationDelay; + emit QueueProfitSharingChange(nextProfitSharingNumerator, nextProfitSharingNumeratorTimestamp); + } + + function confirmSetProfitSharingNumerator() public onlyGovernance { + require( + nextProfitSharingNumerator != 0 && nextProfitSharingNumeratorTimestamp != 0 + && block.timestamp >= nextProfitSharingNumeratorTimestamp, + "invalid timestamp or no new profit sharing numerator confirmed" + ); + require(nextProfitSharingNumerator + strategistFeeNumerator + platformFeeNumerator <= MAX_TOTAL_FEE, "total fee too high"); + + profitSharingNumerator = nextProfitSharingNumerator; + nextProfitSharingNumerator = 0; + nextProfitSharingNumeratorTimestamp = 0; + emit ConfirmProfitSharingChange(profitSharingNumerator); + } + + function setStrategistFeeNumerator(uint256 _strategistFeeNumerator) public onlyGovernance { + require(_strategistFeeNumerator + platformFeeNumerator + profitSharingNumerator <= MAX_TOTAL_FEE, "total fee too high"); + + nextStrategistFeeNumerator = _strategistFeeNumerator; + nextStrategistFeeNumeratorTimestamp = block.timestamp + nextImplementationDelay; + emit QueueStrategistFeeChange(nextStrategistFeeNumerator, nextStrategistFeeNumeratorTimestamp); + } + + function confirmSetStrategistFeeNumerator() public onlyGovernance { + require( + nextStrategistFeeNumerator != 0 && nextStrategistFeeNumeratorTimestamp != 0 + && block.timestamp >= nextStrategistFeeNumeratorTimestamp, + "invalid timestamp or no new strategist fee numerator confirmed" + ); + require(nextStrategistFeeNumerator + platformFeeNumerator + profitSharingNumerator <= MAX_TOTAL_FEE, "total fee too high"); + + strategistFeeNumerator = nextStrategistFeeNumerator; + nextStrategistFeeNumerator = 0; + nextStrategistFeeNumeratorTimestamp = 0; + emit ConfirmStrategistFeeChange(strategistFeeNumerator); + } + + function setPlatformFeeNumerator(uint256 _platformFeeNumerator) public onlyGovernance { + require(_platformFeeNumerator + strategistFeeNumerator + profitSharingNumerator <= MAX_TOTAL_FEE, "total fee too high"); + + nextPlatformFeeNumerator = _platformFeeNumerator; + nextPlatformFeeNumeratorTimestamp = block.timestamp + nextImplementationDelay; + emit QueuePlatformFeeChange(nextPlatformFeeNumerator, nextPlatformFeeNumeratorTimestamp); + } + + function confirmSetPlatformFeeNumerator() public onlyGovernance { + require( + nextPlatformFeeNumerator != 0 && nextPlatformFeeNumeratorTimestamp != 0 + && block.timestamp >= nextPlatformFeeNumeratorTimestamp, + "invalid timestamp or no new platform fee numerator confirmed" + ); + require(nextPlatformFeeNumerator + strategistFeeNumerator + profitSharingNumerator <= MAX_TOTAL_FEE, "total fee too high"); + + platformFeeNumerator = nextPlatformFeeNumerator; + nextPlatformFeeNumerator = 0; + nextPlatformFeeNumeratorTimestamp = 0; + emit ConfirmPlatformFeeChange(platformFeeNumerator); + } + + function setNextImplementationDelay(uint256 _nextImplementationDelay) public onlyGovernance { + require(_nextImplementationDelay > 0, "invalid _nextImplementationDelay"); + + tempNextImplementationDelay = _nextImplementationDelay; + tempNextImplementationDelayTimestamp = block.timestamp + nextImplementationDelay; + emit QueueNextImplementationDelay(tempNextImplementationDelay, tempNextImplementationDelayTimestamp); + } + + function confirmNextImplementationDelay() public onlyGovernance { + require( + tempNextImplementationDelayTimestamp != 0 && block.timestamp >= tempNextImplementationDelayTimestamp, + "invalid timestamp or no new implementation delay confirmed" + ); + nextImplementationDelay = tempNextImplementationDelay; + tempNextImplementationDelay = 0; + tempNextImplementationDelayTimestamp = 0; + emit ConfirmNextImplementationDelay(nextImplementationDelay); + } +} diff --git a/src/controller/RewardForwarder.sol b/src/controller/RewardForwarder.sol new file mode 100644 index 0000000..1cb13de --- /dev/null +++ b/src/controller/RewardForwarder.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./inheritance/Governable.sol"; +import "./interface/IController.sol"; +import "./interface/IRewardForwarder.sol"; +import "./interface/IProfitSharingReceiver.sol"; +import "./interface/IStrategy.sol"; +import "./interface/IUniversalLiquidator.sol"; +import "./inheritance/Controllable.sol"; + +/** + * @dev This contract receives rewards from strategies and is responsible for routing the reward's liquidation into + * specific buyback tokens and profit tokens for the DAO. + */ +contract RewardForwarder is Controllable { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + address public constant iFARM = address(0x9dCA587dc65AC0a043828B0acd946d71eb8D46c1); + + constructor(address _storage) Controllable(_storage) {} + + function notifyFee(address _token, uint256 _profitSharingFee, uint256 _strategistFee, uint256 _platformFee) external { + _notifyFee(_token, _profitSharingFee, _strategistFee, _platformFee); + } + + function _notifyFee(address _token, uint256 _profitSharingFee, uint256 _strategistFee, uint256 _platformFee) internal { + address _controller = controller(); + address liquidator = IController(_controller).universalLiquidator(); + + uint256 totalTransferAmount = _profitSharingFee.add(_strategistFee).add(_platformFee); + require(totalTransferAmount > 0, "totalTransferAmount should not be 0"); + IERC20(_token).safeTransferFrom(msg.sender, address(this), totalTransferAmount); + + address _targetToken = IController(_controller).targetToken(); + + if (_token != _targetToken) { + IERC20(_token).safeApprove(liquidator, 0); + IERC20(_token).safeApprove(liquidator, _platformFee); + + uint256 amountOutMin = 1; + + if (_platformFee > 0) { + IUniversalLiquidator(liquidator).swap( + _token, _targetToken, _platformFee, amountOutMin, IController(_controller).protocolFeeReceiver() + ); + } + } else { + IERC20(_targetToken).safeTransfer(IController(_controller).protocolFeeReceiver(), _platformFee); + } + + if (_token != iFARM) { + IERC20(_token).safeApprove(liquidator, 0); + IERC20(_token).safeApprove(liquidator, _profitSharingFee.add(_strategistFee)); + + uint256 amountOutMin = 1; + + if (_profitSharingFee > 0) { + IUniversalLiquidator(liquidator).swap( + _token, iFARM, _profitSharingFee, amountOutMin, IController(_controller).profitSharingReceiver() + ); + } + if (_strategistFee > 0) { + IUniversalLiquidator(liquidator).swap( + _token, iFARM, _strategistFee, amountOutMin, IStrategy(msg.sender).strategist() + ); + } + } else { + if (_strategistFee > 0) { + IERC20(iFARM).safeTransfer(IStrategy(msg.sender).strategist(), _strategistFee); + } + IERC20(iFARM).safeTransfer(IController(_controller).profitSharingReceiver(), _profitSharingFee); + } + } +} diff --git a/src/controller/inheritance/Controllable.sol b/src/controller/inheritance/Controllable.sol new file mode 100644 index 0000000..5e30552 --- /dev/null +++ b/src/controller/inheritance/Controllable.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "./Governable.sol"; + +contract Controllable is Governable { + constructor(address _storage) Governable(_storage) {} + + modifier onlyController() { + require(store.isController(msg.sender), "Not a controller"); + _; + } + + modifier onlyControllerOrGovernance() { + require((store.isController(msg.sender) || store.isGovernance(msg.sender)), "The caller must be controller or governance"); + _; + } + + function controller() public view returns (address) { + return store.controller(); + } +} diff --git a/src/controller/inheritance/ControllableInit.sol b/src/controller/inheritance/ControllableInit.sol new file mode 100644 index 0000000..5f620ea --- /dev/null +++ b/src/controller/inheritance/ControllableInit.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "./GovernableInit.sol"; + +// A clone of Governable supporting the Initializable interface and pattern +contract ControllableInit is GovernableInit { + constructor() {} + + function initialize(address _storage) public override initializer { + GovernableInit.initialize(_storage); + } + + modifier onlyController() { + require(Storage(_storage()).isController(msg.sender), "Not a controller"); + _; + } + + modifier onlyControllerOrGovernance() { + require( + (Storage(_storage()).isController(msg.sender) || Storage(_storage()).isGovernance(msg.sender)), + "The caller must be controller or governance" + ); + _; + } + + function controller() public view returns (address) { + return Storage(_storage()).controller(); + } +} diff --git a/src/controller/inheritance/Governable.sol b/src/controller/inheritance/Governable.sol new file mode 100644 index 0000000..58c2e0d --- /dev/null +++ b/src/controller/inheritance/Governable.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "./Storage.sol"; + +contract Governable { + Storage public store; + + constructor(address _store) { + require(_store != address(0), "new storage shouldn't be empty"); + store = Storage(_store); + } + + modifier onlyGovernance() { + require(store.isGovernance(msg.sender), "Not governance"); + _; + } + + function setStorage(address _store) public onlyGovernance { + require(_store != address(0), "new storage shouldn't be empty"); + store = Storage(_store); + } + + function governance() public view returns (address) { + return store.governance(); + } +} diff --git a/src/controller/inheritance/GovernableInit.sol b/src/controller/inheritance/GovernableInit.sol new file mode 100644 index 0000000..aafa58c --- /dev/null +++ b/src/controller/inheritance/GovernableInit.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../upgradability/ReentrancyGuardUpgradeable.sol"; +import "./Storage.sol"; + +// A clone of Governable supporting the Initializable interface and pattern +contract GovernableInit is ReentrancyGuardUpgradeable { + bytes32 internal constant _STORAGE_SLOT = 0xa7ec62784904ff31cbcc32d09932a58e7f1e4476e1d041995b37c917990b16dc; + + modifier onlyGovernance() { + require(Storage(_storage()).isGovernance(msg.sender), "Not governance"); + _; + } + + constructor() { + assert(_STORAGE_SLOT == bytes32(uint256(keccak256("eip1967.governableInit.storage")) - 1)); + } + + function initialize(address _store) public virtual initializer { + _setStorage(_store); + ReentrancyGuardUpgradeable.initialize(); + } + + function _setStorage(address newStorage) private { + bytes32 slot = _STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newStorage) + } + } + + function setStorage(address _store) public onlyGovernance { + require(_store != address(0), "new storage shouldn't be empty"); + _setStorage(_store); + } + + function _storage() internal view returns (address str) { + bytes32 slot = _STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + function governance() public view returns (address) { + return Storage(_storage()).governance(); + } +} diff --git a/src/controller/inheritance/IUpgradeSource.sol b/src/controller/inheritance/IUpgradeSource.sol new file mode 100644 index 0000000..1de32e9 --- /dev/null +++ b/src/controller/inheritance/IUpgradeSource.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IUpgradeSource { + function shouldUpgrade() external view returns (bool, address); + function finalizeUpgrade() external; +} diff --git a/src/controller/inheritance/OwnableWhitelist.sol b/src/controller/inheritance/OwnableWhitelist.sol new file mode 100644 index 0000000..1b32bf7 --- /dev/null +++ b/src/controller/inheritance/OwnableWhitelist.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract OwnableWhitelist is Ownable { + mapping(address => bool) public whitelist; + + modifier onlyWhitelisted() { + require(whitelist[msg.sender] || msg.sender == owner(), "not allowed"); + _; + } + + function setWhitelist(address target, bool isWhitelisted) public onlyOwner { + whitelist[target] = isWhitelisted; + } +} diff --git a/src/controller/inheritance/Storage.sol b/src/controller/inheritance/Storage.sol new file mode 100644 index 0000000..0755f63 --- /dev/null +++ b/src/controller/inheritance/Storage.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract Storage { + address public governance; + address public controller; + + constructor() { + governance = msg.sender; + } + + modifier onlyGovernance() { + require(isGovernance(msg.sender), "Not governance"); + _; + } + + function setGovernance(address _governance) public onlyGovernance { + require(_governance != address(0), "new governance shouldn't be empty"); + governance = _governance; + } + + function setController(address _controller) public onlyGovernance { + require(_controller != address(0), "new controller shouldn't be empty"); + controller = _controller; + } + + function isGovernance(address account) public view returns (bool) { + return account == governance; + } + + function isController(address account) public view returns (bool) { + return account == controller; + } +} diff --git a/src/controller/interface/IController.sol b/src/controller/interface/IController.sol new file mode 100644 index 0000000..e6f8a14 --- /dev/null +++ b/src/controller/interface/IController.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IController { + // ========================= Events ========================= + + event QueueProfitSharingChange(uint256 profitSharingNumerator, uint256 validAtTimestamp); + event ConfirmProfitSharingChange(uint256 profitSharingNumerator); + + event QueueStrategistFeeChange(uint256 strategistFeeNumerator, uint256 validAtTimestamp); + event ConfirmStrategistFeeChange(uint256 strategistFeeNumerator); + + event QueuePlatformFeeChange(uint256 platformFeeNumerator, uint256 validAtTimestamp); + event ConfirmPlatformFeeChange(uint256 platformFeeNumerator); + + event QueueNextImplementationDelay(uint256 implementationDelay, uint256 validAtTimestamp); + event ConfirmNextImplementationDelay(uint256 implementationDelay); + + event AddedStakingContract(address indexed stakingContract); + event RemovedStakingContract(address indexed stakingContract); + + event SharePriceChangeLog( + address indexed vault, address indexed strategy, uint256 oldSharePrice, uint256 newSharePrice, uint256 timestamp + ); + + // ==================== Functions ==================== + + /** + * An EOA can safely interact with the system no matter what. If you're using Metamask, you're using an EOA. Only + * smart contracts may be affected by this grey list. This contract will not be able to ban any EOA from the system + * even if an EOA is being added to the greyList, he/she will still be able to interact with the whole system as if + * nothing happened. Only smart contracts will be affected by being added to the greyList. This grey list is only + * used in VaultV3.sol, see the code there for reference + */ + function greyList(address _target) external view returns (bool); + + function addressWhiteList(address _target) external view returns (bool); + + function codeWhiteList(address _target) external view returns (bool); + + function addToWhitelist(address _target) external; + + function addCodeToWhitelist(address _target) external; + + function store() external view returns (address); + + function governance() external view returns (address); + + function doHardWork(address _vault) external; + + function addHardWorker(address _worker) external; + + function removeHardWorker(address _worker) external; + + function salvage(address _token, uint256 amount) external; + + function salvageStrategy(address _strategy, address _token, uint256 amount) external; + + /** + * @return The targeted profit token to convert all-non-compounding rewards to. Defaults to WETH. + */ + function targetToken() external view returns (address); + + function setTargetToken(address _targetToken) external; + + function profitSharingReceiver() external view returns (address); + + function setProfitSharingReceiver(address _profitSharingReceiver) external; + + function protocolFeeReceiver() external view returns (address); + + function setProtocolFeeReceiver(address _protocolFeeReceiver) external; + + function rewardForwarder() external view returns (address); + + function setRewardForwarder(address _rewardForwarder) external; + + function universalLiquidator() external view returns (address); + + function setUniversalLiquidator(address _universalLiquidator) external; + + function dolomiteYieldFarmingRouter() external view returns (address); + + function setDolomiteYieldFarmingRouter(address _value) external; + + function nextImplementationDelay() external view returns (uint256); + + function profitSharingNumerator() external view returns (uint256); + + function strategistFeeNumerator() external view returns (uint256); + + function platformFeeNumerator() external view returns (uint256); + + function feeDenominator() external view returns (uint256); + + function setProfitSharingNumerator(uint256 _profitSharingNumerator) external; + + function confirmSetProfitSharingNumerator() external; + + function setStrategistFeeNumerator(uint256 _strategistFeeNumerator) external; + + function confirmSetStrategistFeeNumerator() external; + + function setPlatformFeeNumerator(uint256 _platformFeeNumerator) external; + + function confirmSetPlatformFeeNumerator() external; + + function setNextImplementationDelay(uint256 _nextImplementationDelay) external; + + function confirmNextImplementationDelay() external; + + function nextProfitSharingNumerator() external view returns (uint256); + + function nextProfitSharingNumeratorTimestamp() external view returns (uint256); + + function nextStrategistFeeNumerator() external view returns (uint256); + + function nextStrategistFeeNumeratorTimestamp() external view returns (uint256); + + function nextPlatformFeeNumerator() external view returns (uint256); + + function nextPlatformFeeNumeratorTimestamp() external view returns (uint256); + + function tempNextImplementationDelay() external view returns (uint256); + + function tempNextImplementationDelayTimestamp() external view returns (uint256); +} diff --git a/src/controller/interface/IProfitSharingReceiver.sol b/src/controller/interface/IProfitSharingReceiver.sol new file mode 100644 index 0000000..f8cdfd8 --- /dev/null +++ b/src/controller/interface/IProfitSharingReceiver.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IProfitSharingReceiver { + function governance() external view returns (address); + + function withdrawTokens(address[] calldata _tokens) external; +} diff --git a/src/controller/interface/IRewardForwarder.sol b/src/controller/interface/IRewardForwarder.sol new file mode 100644 index 0000000..3dfc76e --- /dev/null +++ b/src/controller/interface/IRewardForwarder.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/** + * @dev A routing contract that is responsible for taking the harvested gains and routing them into FARM and additional + * buyback tokens for the corresponding strategy + */ +interface IRewardForwarder { + function store() external view returns (address); + + function governance() external view returns (address); + + /** + * @dev This function sends converted `_buybackTokens` to `msg.sender`. The returned amounts will match the + * `amounts` return value. The fee amounts are converted to the profit sharing token and sent to the proper + * addresses (profit sharing, strategist, and governance (platform)). + * + * @param _token the token that will be compounded or sold into the profit sharing token for the Harvest + * collective (users that stake iFARM) + * @param _profitSharingFee the amount of `_token` that will be sold into the profit sharing token + * @param _strategistFee the amount of `_token` that will be sold into the profit sharing token for the + * strategist + * @param _platformFee the amount of `_token` that will be sold into the profit sharing token for the Harvest + * treasury + * @param _buybackTokens the output tokens that `_buyBackAmounts` should be swapped to (outputToken) + * @param _buybackAmounts the amounts of `_token` that will be bought into more `_buybackTokens` token + * @return amounts The amounts that were purchased of _buybackTokens + */ + function notifyFeeAndBuybackAmounts( + address _token, + uint256 _profitSharingFee, + uint256 _strategistFee, + uint256 _platformFee, + address[] calldata _buybackTokens, + uint256[] calldata _buybackAmounts + ) external returns (uint256[] memory amounts); + + /** + * @dev This function converts the fee amounts to the profit sharing token and sends them to the proper addresses + * (profit sharing, strategist, and governance (platform)). + * + * @param _token the token that will be compounded or sold into the profit sharing token for the Harvest + * collective (users that stake iFARM) + * @param _profitSharingFee the amount of `_token` that will be sold into the profit sharing token + * @param _strategistFee the amount of `_token` that will be sold into the profit sharing token for the + * strategist + * @param _platformFee the amount of `_token` that will be sold into the profit sharing token for the Harvest + * treasury + */ + function notifyFee(address _token, uint256 _profitSharingFee, uint256 _strategistFee, uint256 _platformFee) external; +} diff --git a/src/controller/interface/IStrategy.sol b/src/controller/interface/IStrategy.sol new file mode 100644 index 0000000..8c70548 --- /dev/null +++ b/src/controller/interface/IStrategy.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IStrategy { + /// @notice declared as public so child contract can call it + function isUnsalvageableToken(address token) external view returns (bool); + + function salvageToken(address recipient, address token, uint256 amount) external; + + function governance() external view returns (address); + + function controller() external view returns (address); + + function underlying() external view returns (address); + + function vault() external view returns (address); + + function withdrawAllToVault() external; + + function withdrawToVault(uint256 _amount) external; + + function investedUnderlyingBalance() external view returns (uint256); + + function doHardWork() external; + + function depositArbCheck() external view returns (bool); + + function strategist() external view returns (address); + + /** + * @return The value of any accumulated rewards that are under control by the strategy. Each index corresponds with + * the tokens in `rewardTokens`. This function is not a `view`, because some protocols, like Curve, need + * writeable functions to get the # of claimable reward tokens + */ + function getRewardPoolValues() external returns (uint256[] memory); +} diff --git a/src/controller/interface/IUniversalLiquidator.sol b/src/controller/interface/IUniversalLiquidator.sol new file mode 100644 index 0000000..130e876 --- /dev/null +++ b/src/controller/interface/IUniversalLiquidator.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IUniversalLiquidator { + event Swap( + address indexed sellToken, + address indexed buyToken, + address indexed receiver, + address initiator, + uint256 sellAmount, + uint256 minBuyAmount + ); + + function swap(address _sellToken, address _buyToken, uint256 _sellAmount, uint256 _minBuyAmount, address _receiver) + external + returns (uint256); +} diff --git a/src/controller/interface/IVault.sol b/src/controller/interface/IVault.sol new file mode 100644 index 0000000..51e4231 --- /dev/null +++ b/src/controller/interface/IVault.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +interface IVault { + function initializeVault(address _storage, address _underlying, uint256 _toInvestNumerator, uint256 _toInvestDenominator) + external; + + function balanceOf(address _holder) external view returns (uint256); + + function underlyingBalanceInVault() external view returns (uint256); + + function underlyingBalanceWithInvestment() external view returns (uint256); + + function governance() external view returns (address); + + function controller() external view returns (address); + + function underlying() external view returns (address); + + function underlyingUnit() external view returns (uint256); + + function strategy() external view returns (address); + + function setStrategy(address _strategy) external; + + function announceStrategyUpdate(address _strategy) external; + + function setVaultFractionToInvest(uint256 _numerator, uint256 _denominator) external; + + function deposit(uint256 _amount) external; + function deposit(uint256 _amount, address _receiver) external; + + function depositFor(uint256 _amount, address _holder) external; + + function withdrawAll() external; + + function withdraw(uint256 _numberOfShares) external; + + function getPricePerFullShare() external view returns (uint256); + + function underlyingBalanceWithInvestmentForHolder(address _holder) external view returns (uint256); + + /** + * The total amount available to be deposited from this vault into the strategy, while adhering to the + * `vaultFractionToInvestNumerator` and `vaultFractionToInvestDenominator` rules + */ + function availableToInvestOut() external view returns (uint256); + + /** + * This should be callable only by the controller (by the hard worker) or by governance + */ + function doHardWork() external; +} diff --git a/src/controller/upgradability/ReentrancyGuardUpgradeable.sol b/src/controller/upgradability/ReentrancyGuardUpgradeable.sol new file mode 100644 index 0000000..b36f4a4 --- /dev/null +++ b/src/controller/upgradability/ReentrancyGuardUpgradeable.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * Same old `ReentrancyGuard`, but can be used by upgradable contracts + */ +contract ReentrancyGuardUpgradeable is Initializable { + bytes32 internal constant _NOT_ENTERED_SLOT = 0x62ae7bf2df4e95c187ea09c8c47c3fc3d9abc36298f5b5b6c5e2e7b4b291fe25; + + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_getNotEntered(_NOT_ENTERED_SLOT), "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _setNotEntered(_NOT_ENTERED_SLOT, false); + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _setNotEntered(_NOT_ENTERED_SLOT, true); + } + + constructor() { + assert(_NOT_ENTERED_SLOT == bytes32(uint256(keccak256("eip1967.reentrancyGuard.notEntered")) - 1)); + } + + function initialize() public initializer { + _setNotEntered(_NOT_ENTERED_SLOT, true); + } + + function _getNotEntered(bytes32 slot) private view returns (bool) { + uint256 str; + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + return str == 1; + } + + function _setNotEntered(bytes32 slot, bool _value) private { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _value) + } + } +}