From ecc2c59efa93e25d4a3c06ab6f555071cdef61ef Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Mon, 28 Mar 2022 10:32:58 -0600 Subject: [PATCH 1/8] Onsen Allocator update --- contracts/allocators/OnsenAllocatorV2.sol | 268 ++++++++++++++++++ .../allocators/interfaces/IMasterChef.sol | 37 +++ contracts/allocators/interfaces/ISushiBar.sol | 8 + 3 files changed, 313 insertions(+) create mode 100644 contracts/allocators/OnsenAllocatorV2.sol create mode 100644 contracts/allocators/interfaces/IMasterChef.sol create mode 100644 contracts/allocators/interfaces/ISushiBar.sol diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol new file mode 100644 index 000000000..76168fb4b --- /dev/null +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.10; +import "../libraries/Address.sol"; + +/// ========== TYPES ========== +import "../types/BaseAllocator.sol"; + +/// ========== INTERFACES ========== +import "./interfaces/IMasterChef.sol"; +import "./interfaces/ISushiBar.sol"; + +struct PoolData { + bool poolActive; + uint256 id; +} + +/** + * @notice Contract deploys liquidity from treasury into the Onsen program, + * earning $SUSHI that can be staked and/or deposited into the treasury. + */ +contract OnsenAllocatorV2 is BaseAllocator { + using SafeERC20 for IERC20; + + /// ========== STATE VARIABLES ========== + + address public sushi; /// $SUSHI token + address public xSushi; /// $xSUSHI token + address public masterChef; /// Onsen contract + + address public treasury; /// Olympus Treasury + + mapping(address => PoolData) internal _lpToOnsenId; + + /// ========== CONSTRUCTOR ========== + + constructor( + address _chef, + address _sushi, + address _xSushi, + address _treasury, + AllocatorInitData memory data + ) BaseAllocator(data) { + require(_chef != address(0)); + masterChef = _chef; + require(_sushi != address(0)); + sushi = _sushi; + require(_xSushi != address(0)); + xSushi = _xSushi; + require(_treasury != address(0)); + treasury = _treasury; + + /** @dev approve for safety, sushi is being staked and xsushi is being instantly sent to treasury and that is fine + * but to be absolutely safe this one approve won't hurt + */ + IERC20(sushi).approve(address(extender), type(uint256).max); + IERC20(xSushi).approve(address(extender), type(uint256).max); + } + + /** + * @notice Find out which LP tokens to be deposited into onsen, then deposit it into onsen pool and stake the sushi tokens that are returned. + * 1- Based on the LP token to be deposited, find out the Onsen Pool Id + * 2- Deposit LP token into onsen, if deposit is succesfull we should get in return sushi tokens + * 2- Stake the Sushi rewards tokens returned from deposit LP tokens and as rewards form onsen LP stake, we should get in return xSushi tokens + * 3- Keep the xSushi on the allocator contract for easily deallocation + * 4- Calculate gains/loss based on the LP balance on the Onsen Pool. + */ + function _update(uint256 id) internal override returns (uint128 gain, uint128 loss) { + uint256 index = tokenIds[id]; + IERC20 LPtoken = _tokens[index]; + uint256 balance = LPtoken.balanceOf(address(this)); + + /// Deposit LP token into onsen, if deposit succesfull this address should have in return sushi tokens + if (balance > 0) { + /// Find out if there is a Onsen Pool for the LPToken + (bool onsenLPFound, uint256 onsenPoolId) = _findPoolByLP(address(LPtoken)); + + if (onsenLPFound) { + /// Approve and deposit balance into onsen Pool + LPtoken.approve(masterChef, balance); + IMasterChef(masterChef).deposit(onsenPoolId, balance, address(this)); + + /// Stake the sushi tokens + _stakeSushi(); + + ///Calculate gains/loss + /// Retrieve current balance for pool and address + UserInfo memory currentUserInfo = IMasterChef(masterChef).userInfo(onsenPoolId, address(this)); + + uint256 last = extender.getAllocatorAllocated(id) + extender.getAllocatorPerformance(id).gain; + + if (currentUserInfo.amount >= last) { + gain = uint128(currentUserInfo.amount - last); + } else { + loss = uint128(last - currentUserInfo.amount); + } + } else { + /// If no Onsen pool was found for the LP token then return LP token to the treasury. + LPtoken.safeTransfer(treasury, balance); + } + } + } + + function deallocate(uint256[] memory amounts) public override onlyGuardian { + for (uint256 i; i < amounts.length; i++) { + uint256 amount = amounts[i]; + if (amount > 0) { + /// Get the LPToken + IERC20 token = _tokens[i]; + + /// Get the onsen pool Id + (bool onsenLPFound, uint256 onsenPoolId) = _findPoolByLP(address(token)); + + if (onsenLPFound) { + /// Withdraw LP Token from Onsen Pool + IMasterChef(masterChef).withdrawAndHarvest(onsenPoolId, amount, address(this)); + } + } + } + } + + function _deactivate(bool panic) internal override { + /// Get amounts by LP to unstake and then deallocate (unstake) each token. + _deallocateAll(); + _unStakeSushi(); + + /// If Panic transfer LP tokens to the treasury + if (panic) { + for (uint256 i; i < _tokens.length; i++) { + IERC20 token = _tokens[i]; + token.safeTransfer(treasury, token.balanceOf(address(this))); + } + + /// And transfer Sushi tokens (rewards) to treasury + IERC20(sushi).safeTransfer(treasury, IERC20(sushi).balanceOf(address(this))); + } + } + + function _prepareMigration() internal override { + /// Get amounts by LP to unstake and then deallocate (unstake) each token. + _deallocateAll(); + _unStakeSushi(); + } + + function amountAllocated(uint256 id) public view override returns (uint256) { + /// Find the LP token which amount allocated is requested + uint256 index = tokenIds[id]; + IERC20 LPtoken = _tokens[index]; + PoolData memory pd = _lpToOnsenId[address(LPtoken)]; + + if (pd.poolActive) { + UserInfo memory currentUserInfo = IMasterChef(masterChef).userInfo(pd.id, address(this)); + return currentUserInfo.amount; + } else { + return 0; + } + } + + function rewardTokens() public view override returns (IERC20[] memory) { + IERC20[] memory rewards = new IERC20[](2); + rewards[0] = IERC20(sushi); + rewards[1] = IERC20(xSushi); + return rewards; + } + + function utilityTokens() public view override returns (IERC20[] memory) { + IERC20[] memory utility = new IERC20[](1); + utility[0] = IERC20(sushi); + return utility; + } + + function name() external pure override returns (string memory) { + return "OnsenAllocator"; + } + + function setTreasury(address treasuryAddress) external onlyGuardian { + treasury = treasuryAddress; + } + + function setSushi(address sushiAddress) external onlyGuardian { + sushi = sushiAddress; + } + + function setXSushi(address xSushiAddress) external onlyGuardian { + xSushi = xSushiAddress; + } + + function setMasterChefAddress(address masterChefAddress) external onlyGuardian { + masterChef = masterChefAddress; + } + + /// ========== INTERNAL FUNCTIONS ========== + + /** + * @notice stake sushi rewards + */ + function _stakeSushi() internal { + uint256 balance = IERC20(sushi).balanceOf(address(this)); + if (balance > 0) { + IERC20(sushi).approve(xSushi, balance); + ISushiBar(xSushi).enter(balance); // stake sushi + } + } + + /** + * @notice unStake sushi rewards + */ + function _unStakeSushi() internal { + uint256 balance = IERC20(xSushi).balanceOf(address(this)); + if (balance > 0) { + ISushiBar(xSushi).leave(balance); // unstake $xSUSHI + } + } + + /** + * @notice pending $SUSHI rewards + * @return uint + */ + function _pendingSushi(uint256 pid) internal view returns (uint256) { + return IMasterChef(masterChef).pendingSushi(pid, address(this)); + } + + /** + * @notice Find out pool Id from Onsen based on the LP token + * according to Onsen documentation there shouldn't be more than pool by LPToken, so we are going to use the first ocurrence of the LPToken in the Onsen pools + * @return [bool, uint256], first value shows if value was found, second value the id for the pool + */ + function _findPoolByLP(address LPToken) internal view returns (bool, uint256) { + /// Check if the id is already stored in the local variable + PoolData memory pd = _lpToOnsenId[LPToken]; + + /// If the pool is active return the id else search for the pool id + if (pd.poolActive) { + return (pd.poolActive, pd.id); + } else { + /// Call the poolLength function from sushi masterchef v2 and compare against the LP token passed as parameter + uint256 poolsLength = IMasterChef(masterChef).poolLength(); + + for (uint256 i; i < poolsLength; i++) { + /// If found, return the the pool Id from Onsen for the current LPToken. + if (LPToken == (address)(IMasterChef(masterChef).lpToken(i))) { + pd = _lpToOnsenId[LPToken]; + pd.poolActive = true; + pd.id = i; + return (true, i); + } + } + + /// If the LPToken was not found return 0 and indicate that the token was not found + return (false, 0); + } + } + + function _deallocateAll() internal { + uint256[] memory amounts = new uint256[](_tokens.length); + + // interactions + for (uint256 i; i < _tokens.length; i++) { + (bool onsenLPFound, uint256 onsenPoolId) = _findPoolByLP(address(_tokens[i])); + if (onsenLPFound) { + /// Retrieve current balance for pool and address + UserInfo memory currentUserInfo = IMasterChef(masterChef).userInfo(onsenPoolId, address(this)); + amounts[i] = currentUserInfo.amount; + } + } + + deallocate(amounts); + } +} diff --git a/contracts/allocators/interfaces/IMasterChef.sol b/contracts/allocators/interfaces/IMasterChef.sol new file mode 100644 index 000000000..51ee09d3c --- /dev/null +++ b/contracts/allocators/interfaces/IMasterChef.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.7.5; + +struct UserInfo { + uint256 amount; // How many LP tokens the user has provided. + uint256 rewardDebt; +} + +interface IMasterChef { + function pendingSushi(uint256 _pid, address _user) external view returns (uint256); + + function deposit( + uint256 _pid, + uint256 _amount, + address _to + ) external; + + function withdraw( + uint256 _pid, + uint256 _amount, + address _to + ) external; + + function withdrawAndHarvest( + uint256 _pid, + uint256 _amount, + address _to + ) external; + + function emergencyWithdraw(uint256 _pid) external; + + function userInfo(uint256 _pid, address _user) external view returns (UserInfo memory); + + function poolLength() external view returns (uint256); + + function lpToken(uint256) external view returns (address); +} diff --git a/contracts/allocators/interfaces/ISushiBar.sol b/contracts/allocators/interfaces/ISushiBar.sol new file mode 100644 index 000000000..bae594665 --- /dev/null +++ b/contracts/allocators/interfaces/ISushiBar.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.7.5; + +interface ISushiBar { + function enter(uint256 _amount) external; + + function leave(uint256 _share) external; +} From 8e5449e76c00140832954af32640939780c537e4 Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Mon, 28 Mar 2022 23:07:17 -0600 Subject: [PATCH 2/8] Remove unused function --- contracts/allocators/OnsenAllocatorV2.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index 76168fb4b..374eaf84a 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -211,14 +211,6 @@ contract OnsenAllocatorV2 is BaseAllocator { } } - /** - * @notice pending $SUSHI rewards - * @return uint - */ - function _pendingSushi(uint256 pid) internal view returns (uint256) { - return IMasterChef(masterChef).pendingSushi(pid, address(this)); - } - /** * @notice Find out pool Id from Onsen based on the LP token * according to Onsen documentation there shouldn't be more than pool by LPToken, so we are going to use the first ocurrence of the LPToken in the Onsen pools From e989a8910b6a1c6bc6d08ea96d883cd405ad681d Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Tue, 29 Mar 2022 01:18:47 -0600 Subject: [PATCH 3/8] Set/Get Onsen Pool Ids based on LP token address --- contracts/allocators/OnsenAllocatorV2.sol | 32 ++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index 374eaf84a..269105cb1 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -17,6 +17,7 @@ struct PoolData { /** * @notice Contract deploys liquidity from treasury into the Onsen program, * earning $SUSHI that can be staked and/or deposited into the treasury. + * Set the Onsen Pool Id for the LP tokens that will be processed ahead of time by calling setLPTokenOnsenPoolId */ contract OnsenAllocatorV2 is BaseAllocator { using SafeERC20 for IERC20; @@ -188,6 +189,34 @@ contract OnsenAllocatorV2 is BaseAllocator { masterChef = masterChefAddress; } + /** + * @notice Set Onsen Pool Id for LP token + * @param lpToken address of the LP token + * @param onsenPoolId Pool id from onsen sushi + */ + function setLPTokenOnsenPoolId(address lpToken, uint256 onsenPoolId) external onlyGuardian { + PoolData memory pd = _lpToOnsenId[lpToken]; + pd.poolActive = true; + pd.id = onsenPoolId; + } + + /** + * @notice Get the stored Onsen Pool id based on the LP token address + * @param lpToken address of the LP token + * @return first value will show if the value is stored , second value show the Onsen Pool Id stored locally. + */ + function getLPTokenOnsenPoolId(address lpToken) external view returns (bool, uint256) { + /// Check if the id is already stored in the local variable + PoolData memory pd = _lpToOnsenId[lpToken]; + + /// If the pool is active return the id else search for the pool id + if (pd.poolActive) { + return (pd.poolActive, pd.id); + } else { + return (false, 0); + } + } + /// ========== INTERNAL FUNCTIONS ========== /** @@ -213,7 +242,8 @@ contract OnsenAllocatorV2 is BaseAllocator { /** * @notice Find out pool Id from Onsen based on the LP token - * according to Onsen documentation there shouldn't be more than pool by LPToken, so we are going to use the first ocurrence of the LPToken in the Onsen pools + * according to Onsen documentation there shouldn't be more than pool by LPToken, so we are going + * to use the first ocurrence of the LPToken in the Onsen pools * @return [bool, uint256], first value shows if value was found, second value the id for the pool */ function _findPoolByLP(address LPToken) internal view returns (bool, uint256) { From a416445ec9da43268abf012b8aaa7f42506c72fa Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Tue, 29 Mar 2022 01:28:54 -0600 Subject: [PATCH 4/8] Update function not to be View type. --- contracts/allocators/OnsenAllocatorV2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index 269105cb1..d9b4e6b3d 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -246,7 +246,7 @@ contract OnsenAllocatorV2 is BaseAllocator { * to use the first ocurrence of the LPToken in the Onsen pools * @return [bool, uint256], first value shows if value was found, second value the id for the pool */ - function _findPoolByLP(address LPToken) internal view returns (bool, uint256) { + function _findPoolByLP(address LPToken) internal returns (bool, uint256) { /// Check if the id is already stored in the local variable PoolData memory pd = _lpToOnsenId[LPToken]; From 8c55efdefb9c5e088e49dedcb64bf489218f7f89 Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Tue, 29 Mar 2022 01:58:10 -0600 Subject: [PATCH 5/8] Allow update to just stake any sushi rewards --- contracts/allocators/OnsenAllocatorV2.sol | 39 ++++++++++--------- .../allocators/interfaces/IMasterChef.sol | 2 + 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index d9b4e6b3d..4e79b252f 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -70,34 +70,37 @@ contract OnsenAllocatorV2 is BaseAllocator { IERC20 LPtoken = _tokens[index]; uint256 balance = LPtoken.balanceOf(address(this)); + /// Find out if there is a Onsen Pool for the LPToken + (bool onsenLPFound, uint256 onsenPoolId) = _findPoolByLP(address(LPtoken)); + /// Deposit LP token into onsen, if deposit succesfull this address should have in return sushi tokens if (balance > 0) { - /// Find out if there is a Onsen Pool for the LPToken - (bool onsenLPFound, uint256 onsenPoolId) = _findPoolByLP(address(LPtoken)); - if (onsenLPFound) { /// Approve and deposit balance into onsen Pool LPtoken.approve(masterChef, balance); IMasterChef(masterChef).deposit(onsenPoolId, balance, address(this)); - - /// Stake the sushi tokens - _stakeSushi(); - - ///Calculate gains/loss - /// Retrieve current balance for pool and address - UserInfo memory currentUserInfo = IMasterChef(masterChef).userInfo(onsenPoolId, address(this)); - - uint256 last = extender.getAllocatorAllocated(id) + extender.getAllocatorPerformance(id).gain; - - if (currentUserInfo.amount >= last) { - gain = uint128(currentUserInfo.amount - last); - } else { - loss = uint128(last - currentUserInfo.amount); - } } else { /// If no Onsen pool was found for the LP token then return LP token to the treasury. LPtoken.safeTransfer(treasury, balance); } + } else { + /// If LP token balance is 0, then harvest any pending Sushi rewards for it to be stake. + IMasterChef(masterChef).harvest(onsenPoolId, address(this)); + } + + /// Stake the sushi tokens + _stakeSushi(); + + ///Calculate gains/loss + /// Retrieve current balance for pool and address + UserInfo memory currentUserInfo = IMasterChef(masterChef).userInfo(onsenPoolId, address(this)); + + uint256 last = extender.getAllocatorAllocated(id) + extender.getAllocatorPerformance(id).gain; + + if (currentUserInfo.amount >= last) { + gain = uint128(currentUserInfo.amount - last); + } else { + loss = uint128(last - currentUserInfo.amount); } } diff --git a/contracts/allocators/interfaces/IMasterChef.sol b/contracts/allocators/interfaces/IMasterChef.sol index 51ee09d3c..61980f2c1 100644 --- a/contracts/allocators/interfaces/IMasterChef.sol +++ b/contracts/allocators/interfaces/IMasterChef.sol @@ -34,4 +34,6 @@ interface IMasterChef { function poolLength() external view returns (uint256); function lpToken(uint256) external view returns (address); + + function harvest(uint256 pid, address to) external; } From 666a184b9ca6077ca65f67b13bf538ac25a1a42b Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Tue, 29 Mar 2022 12:10:46 -0600 Subject: [PATCH 6/8] Avoid double loop for deallocate all LP tokens --- contracts/allocators/OnsenAllocatorV2.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index 4e79b252f..ad71ed6a8 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -276,18 +276,16 @@ contract OnsenAllocatorV2 is BaseAllocator { } function _deallocateAll() internal { - uint256[] memory amounts = new uint256[](_tokens.length); - - // interactions + /// Find the Onsen Pools Id for each token and withdraw and harvest each pool. for (uint256 i; i < _tokens.length; i++) { (bool onsenLPFound, uint256 onsenPoolId) = _findPoolByLP(address(_tokens[i])); if (onsenLPFound) { /// Retrieve current balance for pool and address UserInfo memory currentUserInfo = IMasterChef(masterChef).userInfo(onsenPoolId, address(this)); - amounts[i] = currentUserInfo.amount; + if (currentUserInfo.amount > 0) { + IMasterChef(masterChef).withdrawAndHarvest(onsenPoolId, currentUserInfo.amount, address(this)); + } } } - - deallocate(amounts); } } From 0d1cc7ecf3471c6f8f8978e40bdaed3c689153c5 Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Tue, 29 Mar 2022 12:16:22 -0600 Subject: [PATCH 7/8] Update function name to follow camel case format --- contracts/allocators/OnsenAllocatorV2.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index ad71ed6a8..4aa675126 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -125,7 +125,7 @@ contract OnsenAllocatorV2 is BaseAllocator { function _deactivate(bool panic) internal override { /// Get amounts by LP to unstake and then deallocate (unstake) each token. _deallocateAll(); - _unStakeSushi(); + _unstakeSushi(); /// If Panic transfer LP tokens to the treasury if (panic) { @@ -142,7 +142,7 @@ contract OnsenAllocatorV2 is BaseAllocator { function _prepareMigration() internal override { /// Get amounts by LP to unstake and then deallocate (unstake) each token. _deallocateAll(); - _unStakeSushi(); + _unstakeSushi(); } function amountAllocated(uint256 id) public view override returns (uint256) { @@ -236,7 +236,7 @@ contract OnsenAllocatorV2 is BaseAllocator { /** * @notice unStake sushi rewards */ - function _unStakeSushi() internal { + function _unstakeSushi() internal { uint256 balance = IERC20(xSushi).balanceOf(address(this)); if (balance > 0) { ISushiBar(xSushi).leave(balance); // unstake $xSUSHI From d0cd7c6ec802b35f39c8c1587b1722628a480094 Mon Sep 17 00:00:00 2001 From: Carlos Sandi Date: Tue, 29 Mar 2022 12:25:16 -0600 Subject: [PATCH 8/8] Update _deactivate function --- contracts/allocators/OnsenAllocatorV2.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/allocators/OnsenAllocatorV2.sol b/contracts/allocators/OnsenAllocatorV2.sol index 4aa675126..44c6519f5 100644 --- a/contracts/allocators/OnsenAllocatorV2.sol +++ b/contracts/allocators/OnsenAllocatorV2.sol @@ -123,12 +123,12 @@ contract OnsenAllocatorV2 is BaseAllocator { } function _deactivate(bool panic) internal override { - /// Get amounts by LP to unstake and then deallocate (unstake) each token. - _deallocateAll(); - _unstakeSushi(); - /// If Panic transfer LP tokens to the treasury if (panic) { + /// Get amounts by LP to unstake and then deallocate (unstake) each token. + _deallocateAll(); + _unstakeSushi(); + for (uint256 i; i < _tokens.length; i++) { IERC20 token = _tokens[i]; token.safeTransfer(treasury, token.balanceOf(address(this)));