From 298cf48f0369401bf21a43d796f9b2e27591297b Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 28 Mar 2023 14:17:07 +0200 Subject: [PATCH 01/18] blocksec improvements --- src/utils/MultiRewardStaking.sol | 35 ++-- src/vault/FeeRecipientProxy.sol | 16 ++ src/vault/TemplateRegistry.sol | 9 +- src/vault/Vault.sol | 28 +-- src/vault/VaultRouter.sol | 22 +- src/vault/adapter/abstracts/AdapterBase.sol | 67 ------ src/vault/adapter/beefy/BeefyAdapter.sol | 32 ++- src/vault/adapter/beefy/IBeefy.sol | 2 + src/vault/adapter/convex/ConvexAdapter.sol | 12 +- src/vault/adapter/sushi/IMasterChef.sol | 36 ++++ src/vault/adapter/sushi/MasterChefAdapter.sol | 124 +++++++++++ src/vault/adapter/yearn/YearnAdapter.sol | 24 +-- test/MultiRewardStaking.t.sol | 18 ++ test/utils/mocks/MockStrategyClaimer.sol | 35 ++++ test/vault/FeeRecipientProxy.t.sol | 172 ++++++++++++++++ test/vault/Vault.t.sol | 2 +- test/vault/VaultController.t.sol | 33 +++ .../integration/aaveV2/AaveV2Adapter.t.sol | 9 +- .../integration/aaveV3/AaveV3Adapter.t.sol | 10 +- .../abstract/AbstractAdapterTest.sol | 21 +- .../integration/beefy/BeefyAdapter.t.sol | 194 ++++++++++++++++-- .../beefy/BeefyTestConfigStorage.sol | 13 +- .../integration/convex/ConvexAdapter.t.sol | 34 ++- .../integration/sushi/MasterChefAdapter.t.sol | 115 +++++++++++ .../sushi/MasterChefTestConfigStorage.sol | 24 +++ .../yearn/YearnTestConfigStorage.sol | 3 + 26 files changed, 903 insertions(+), 187 deletions(-) create mode 100644 src/vault/adapter/sushi/IMasterChef.sol create mode 100644 src/vault/adapter/sushi/MasterChefAdapter.sol create mode 100644 test/utils/mocks/MockStrategyClaimer.sol create mode 100644 test/vault/FeeRecipientProxy.t.sol create mode 100644 test/vault/integration/sushi/MasterChefAdapter.t.sol create mode 100644 test/vault/integration/sushi/MasterChefTestConfigStorage.sol diff --git a/src/utils/MultiRewardStaking.sol b/src/utils/MultiRewardStaking.sol index 8c2696f..1a3b7a9 100644 --- a/src/utils/MultiRewardStaking.sol +++ b/src/utils/MultiRewardStaking.sol @@ -38,11 +38,7 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { * @param _escrow An optional escrow contract which can be used to lock rewards on claim. * @param _owner Owner of the contract. Controls management functions. */ - function initialize( - IERC20 _stakingToken, - IMultiRewardEscrow _escrow, - address _owner - ) external initializer { + function initialize(IERC20 _stakingToken, IMultiRewardEscrow _escrow, address _owner) external initializer { __ERC4626_init(IERC20Metadata(address(_stakingToken))); __Owned_init(_owner); @@ -134,11 +130,7 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { } /// @notice Internal transfer function used by `transfer()` and `transferFrom()`. Accrues rewards for `from` and `to`. - function _transfer( - address from, - address to, - uint256 amount - ) internal override accrueRewards(from, to) { + function _transfer(address from, address to, uint256 amount) internal override accrueRewards(from, to) { if (from == address(0) || to == address(0)) revert ZeroAddressTransfer(from, to); uint256 fromBalance = balanceOf(from); @@ -188,12 +180,7 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { } /// @notice Locks a percentage of a reward in an escrow contract. Pays out the rest to the user. - function _lockToken( - address user, - IERC20 rewardToken, - uint256 rewardAmount, - EscrowInfo memory escrowInfo - ) internal { + function _lockToken(address user, IERC20 rewardToken, uint256 rewardAmount, EscrowInfo memory escrowInfo) internal { uint256 escrowed = rewardAmount.mulDiv(uint256(escrowInfo.escrowPercentage), 1e18, Math.Rounding.Down); uint256 payout = rewardAmount - escrowed; @@ -258,7 +245,7 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { if (rewards.lastUpdatedTimestamp > 0) revert RewardTokenAlreadyExist(rewardToken); if (amount > 0) { - if (rewardsPerSecond == 0) revert ZeroRewardsSpeed(); + if (rewardsPerSecond == 0 && totalSupply() == 0) revert InvalidConfig(); rewardToken.safeTransferFrom(msg.sender, address(this), amount); } @@ -275,7 +262,7 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { rewardToken.safeApprove(address(escrow), type(uint256).max); } - uint64 ONE = (10**IERC20Metadata(address(rewardToken)).decimals()).safeCastTo64(); + uint64 ONE = (10 ** IERC20Metadata(address(rewardToken)).decimals()).safeCastTo64(); uint32 rewardsEndTimestamp = rewardsPerSecond == 0 ? block.timestamp.safeCastTo32() : _calcRewardsEnd(0, rewardsPerSecond, amount); @@ -328,6 +315,8 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { // Cache RewardInfo RewardInfo memory rewards = rewardInfos[rewardToken]; + if (rewards.rewardsPerSecond == 0 && totalSupply() == 0) revert InvalidConfig(); + // Make sure that the reward exists if (rewards.lastUpdatedTimestamp == 0) revert RewardTokenDoesntExist(rewardToken); @@ -402,9 +391,11 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { /// @notice Accrue global rewards for a rewardToken function _accrueRewards(IERC20 _rewardToken, uint256 accrued) internal { uint256 supplyTokens = totalSupply(); - uint224 deltaIndex; + uint224 deltaIndex; // DeltaIndex is the amount of rewardsToken paid out per stakeToken if (supplyTokens != 0) - deltaIndex = accrued.mulDiv(uint256(10**decimals()), supplyTokens, Math.Rounding.Down).safeCastTo224(); + deltaIndex = accrued.mulDiv(uint256(10 ** decimals()), supplyTokens, Math.Rounding.Down).safeCastTo224(); + // rewardDecimals * stakeDecimals / stakeDecimals = rewardDecimals + // 1e18 * 1e6 / 10e6 = 0.1e18 | 1e6 * 1e18 / 10e18 = 0.1e6 rewardInfos[_rewardToken].index += deltaIndex; rewardInfos[_rewardToken].lastUpdatedTimestamp = block.timestamp.safeCastTo32(); @@ -425,7 +416,9 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { uint256 deltaIndex = rewards.index - oldIndex; // Accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed - uint256 supplierDelta = balanceOf(_user).mulDiv(deltaIndex, uint256(rewards.ONE), Math.Rounding.Down); + uint256 supplierDelta = balanceOf(_user).mulDiv(deltaIndex, uint256(10 ** decimals()), Math.Rounding.Down); + // stakeDecimals * rewardDecimals / stakeDecimals = rewardDecimals + // 1e18 * 1e6 / 10e18 = 0.1e18 | 1e6 * 1e18 / 10e18 = 0.1e6 userIndex[_user][_rewardToken] = rewards.index; diff --git a/src/vault/FeeRecipientProxy.sol b/src/vault/FeeRecipientProxy.sol index 7d3b001..587b6a3 100644 --- a/src/vault/FeeRecipientProxy.sol +++ b/src/vault/FeeRecipientProxy.sol @@ -11,26 +11,42 @@ contract FeeRecipientProxy is Owned { uint256 public approvals; + event TokenApproved(uint8 len); + event TokenApprovalVoided(uint8 len); + + error TokenAlreadyApproved(IERC20 token); + error TokenApprovalAlreadyVoided(IERC20 token); + function approveToken(IERC20[] calldata tokens) external onlyOwner { uint8 len = uint8(tokens.length); for (uint8 i = 0; i < len; i++) { + if (tokens[i].allowance(address(this), owner) > 0) revert TokenAlreadyApproved(tokens[i]); + tokens[i].approve(owner, type(uint256).max); approvals++; } + + emit TokenApproved(len); } function voidTokenApproval(IERC20[] calldata tokens) external onlyOwner { uint8 len = uint8(tokens.length); for (uint8 i = 0; i < len; i++) { + if (tokens[i].allowance(address(this), owner) == 0) revert TokenApprovalAlreadyVoided(tokens[i]); + tokens[i].approve(owner, 0); approvals--; } + + emit TokenApprovalVoided(len); } function acceptOwnership() external override { require(msg.sender == nominatedOwner, "You must be nominated before you can accept ownership"); require(approvals == 0, "Must void all approvals first"); + emit OwnerChanged(owner, nominatedOwner); + owner = nominatedOwner; nominatedOwner = address(0); } diff --git a/src/vault/TemplateRegistry.sol b/src/vault/TemplateRegistry.sol index 5738e00..7420501 100644 --- a/src/vault/TemplateRegistry.sol +++ b/src/vault/TemplateRegistry.sol @@ -59,16 +59,12 @@ contract TemplateRegistry is Owned { } /** - * @notice Adds a new template to the registry. Caller must be owner. (`DeploymentController`) + * @notice Adds a new template to the registry. * @param templateCategory TemplateCategory of the new template. * @param templateId Unique TemplateId of the new template. * @param template Contains the implementation address and necessary informations to clone the implementation. */ - function addTemplate( - bytes32 templateCategory, - bytes32 templateId, - Template memory template - ) external onlyOwner { + function addTemplate(bytes32 templateCategory, bytes32 templateId, Template memory template) external onlyOwner { if (!templateCategoryExists[templateCategory]) revert KeyNotFound(templateCategory); if (templateExists[templateId]) revert TemplateExists(templateId); @@ -100,6 +96,7 @@ contract TemplateRegistry is Owned { * @dev Only the DAO can endorse templates via `VaultController`. */ function toggleTemplateEndorsement(bytes32 templateCategory, bytes32 templateId) external onlyOwner { + if (!templateCategoryExists[templateCategory]) revert KeyNotFound(templateCategory); if (!templateExists[templateId]) revert KeyNotFound(templateId); bool oldEndorsement = templates[templateCategory][templateId].endorsed; diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index 92a416b..7d3d72c 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -128,13 +128,10 @@ contract Vault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgrad * @param receiver Receiver of issued vault shares. * @return shares Quantity of vault shares issued to `receiver`. */ - function deposit(uint256 assets, address receiver) - public - override - nonReentrant - whenNotPaused - returns (uint256 shares) - { + function deposit( + uint256 assets, + address receiver + ) public override nonReentrant whenNotPaused returns (uint256 shares) { if (receiver == address(0)) revert InvalidReceiver(); if (assets > maxDeposit(receiver)) revert MaxError(assets); @@ -176,7 +173,7 @@ contract Vault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgrad uint256 feeShares = shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Down); assets = _convertToAssets(shares + feeShares, Math.Rounding.Up); - + if (assets > maxMint(receiver)) revert MaxError(assets); if (feeShares > 0) _mint(feeRecipient, feeShares); @@ -323,18 +320,15 @@ contract Vault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgrad assets -= assets.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); } - function _convertToShares(uint256 assets, Math.Rounding rounding) - internal - view - virtual - override - returns (uint256 shares) - { - return assets.mulDiv(totalSupply() + 10**decimalOffset, totalAssets() + 1, rounding); + function _convertToShares( + uint256 assets, + Math.Rounding rounding + ) internal view virtual override returns (uint256 shares) { + return assets.mulDiv(totalSupply() + 10 ** decimalOffset, totalAssets() + 1, rounding); } function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) { - return shares.mulDiv(totalAssets() + 1, totalSupply() + 10**decimalOffset, rounding); + return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** decimalOffset, rounding); } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/VaultRouter.sol b/src/vault/VaultRouter.sol index ee8ca46..85bc276 100644 --- a/src/vault/VaultRouter.sol +++ b/src/vault/VaultRouter.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.15; -import { IERC4626Upgradeable as IERC4626 } from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; +import { IERC4626Upgradeable as IERC4626, IERC20Upgradeable as IERC20 } from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; +import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { IVaultRegistry, VaultMetadata } from "../interfaces/vault/IVaultRegistry.sol"; /** @@ -13,6 +14,8 @@ import { IVaultRegistry, VaultMetadata } from "../interfaces/vault/IVaultRegistr * */ contract VaultRouter { + using SafeERC20 for IERC20; + IVaultRegistry public vaultRegistry; constructor(IVaultRegistry _vaultRegistry) { @@ -21,25 +24,20 @@ contract VaultRouter { error NoStaking(); - function depositAndStake( - IERC4626 vault, - uint256 assetAmount, - address receiver - ) external { + function depositAndStake(IERC4626 vault, uint256 assetAmount, address receiver) external { VaultMetadata memory metadata = vaultRegistry.getVault(address(vault)); if (metadata.staking == address(0)) revert NoStaking(); + IERC20 asset = IERC20(vault.asset()); + asset.safeTransferFrom(msg.sender, address(this), assetAmount); + asset.approve(address(vault), assetAmount); + uint256 shares = vault.deposit(assetAmount, address(this)); vault.approve(metadata.staking, shares); IERC4626(metadata.staking).deposit(shares, receiver); } - function redeemAndWithdraw( - IERC4626 vault, - uint256 burnAmount, - address receiver, - address owner - ) external { + function redeemAndWithdraw(IERC4626 vault, uint256 burnAmount, address receiver, address owner) external { VaultMetadata memory metadata = vaultRegistry.getVault(address(vault)); if (metadata.staking == address(0)) revert NoStaking(); diff --git a/src/vault/adapter/abstracts/AdapterBase.sol b/src/vault/adapter/abstracts/AdapterBase.sol index 7ab232a..81f4ecf 100644 --- a/src/vault/adapter/abstracts/AdapterBase.sol +++ b/src/vault/adapter/abstracts/AdapterBase.sol @@ -96,34 +96,6 @@ abstract contract AdapterBase is error MaxError(uint256 amount); error ZeroAmount(); - /** - * @notice Deposits assets into the underlying protocol and mints vault shares to `receiver`. - * @param assets Amount of assets to deposit. - * @param receiver Receiver of the shares. - */ - function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { - if (assets > maxDeposit(receiver)) revert MaxError(assets); - - uint256 shares = _convertToShares(assets, Math.Rounding.Down); - _deposit(_msgSender(), receiver, assets, shares); - - return shares; - } - - /** - * @notice Mints vault shares to `receiver` and deposits assets into the underlying protocol. - * @param shares Amount of shares to mint. - * @param receiver Receiver of the shares. - */ - function mint(uint256 shares, address receiver) public virtual override returns (uint256) { - if (shares > maxMint(receiver)) revert MaxError(shares); - - uint256 assets = _convertToAssets(shares, Math.Rounding.Up); - _deposit(_msgSender(), receiver, assets, shares); - - return assets; - } - /** * @notice Deposit `assets` into the underlying protocol and mints vault shares to `receiver`. * @dev Executes harvest if `harvestCooldown` is passed since last invocation. @@ -144,45 +116,6 @@ abstract contract AdapterBase is emit Deposit(caller, receiver, assets, shares); } - /** - * @notice Withdraws `assets` from the underlying protocol and burns vault shares from `owner`. - * @param assets Amount of assets to withdraw. - * @param receiver Receiver of the assets. - * @param owner Owner of the shares. - */ - function withdraw( - uint256 assets, - address receiver, - address owner - ) public virtual override returns (uint256) { - if (assets > maxWithdraw(owner)) revert MaxError(assets); - - uint256 shares = _convertToShares(assets, Math.Rounding.Up); - - _withdraw(_msgSender(), receiver, owner, assets, shares); - - return shares; - } - - /** - * @notice Burns vault shares from `owner` and withdraws `assets` from the underlying protocol. - * @param shares Amount of shares to burn. - * @param receiver Receiver of the assets. - * @param owner Owner of the shares. - */ - function redeem( - uint256 shares, - address receiver, - address owner - ) public virtual override returns (uint256) { - if (shares > maxRedeem(owner)) revert MaxError(shares); - - uint256 assets = _convertToAssets(shares, Math.Rounding.Down); - _withdraw(_msgSender(), receiver, owner, assets, shares); - - return assets; - } - /** * @notice Withdraws `assets` from the underlying protocol and burns vault shares from `owner`. * @dev Executes harvest if `harvestCooldown` is passed since last invocation. diff --git a/src/vault/adapter/beefy/BeefyAdapter.sol b/src/vault/adapter/beefy/BeefyAdapter.sol index 66958a8..ed9a48c 100644 --- a/src/vault/adapter/beefy/BeefyAdapter.sol +++ b/src/vault/adapter/beefy/BeefyAdapter.sol @@ -43,16 +43,13 @@ contract BeefyAdapter is AdapterBase, WithRewards { * @dev `_beefyBooster` - An optional beefy booster. * @dev This function is called by the factory contract when deploying a new vault. */ - function initialize( - bytes memory adapterInitData, - address registry, - bytes memory beefyInitData - ) external initializer { + function initialize(bytes memory adapterInitData, address registry, bytes memory beefyInitData) external initializer { (address _beefyVault, address _beefyBooster) = abi.decode(beefyInitData, (address, address)); __AdapterBase_init(adapterInitData); if (!IPermissionRegistry(registry).endorsed(_beefyVault)) revert NotEndorsed(_beefyVault); - if (!IPermissionRegistry(registry).endorsed(_beefyBooster)) revert NotEndorsed(_beefyBooster); + if (_beefyBooster != address(0) && !IPermissionRegistry(registry).endorsed(_beefyBooster)) + revert NotEndorsed(_beefyBooster); if (IBeefyVault(_beefyVault).want() != asset()) revert InvalidBeefyVault(_beefyVault); if (_beefyBooster != address(0) && IBeefyBooster(_beefyBooster).stakedToken() != _beefyVault) revert InvalidBeefyBooster(_beefyBooster); @@ -108,8 +105,15 @@ contract BeefyAdapter is AdapterBase, WithRewards { /// @notice `previewWithdraw` that takes beefy withdrawal fees into account function previewWithdraw(uint256 assets) public view override returns (uint256) { IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); - uint256 beefyFee = strat.withdrawalFee(); - if (beefyFee > 0) assets -= assets.mulDiv(beefyFee, BPS_DENOMINATOR, Math.Rounding.Up); + + uint256 beefyFee; + try strat.withdrawalFee() returns (uint256 _beefyFee) { + beefyFee = _beefyFee; + } catch { + beefyFee = strat.withdrawFee(); + } + + if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR, BPS_DENOMINATOR - beefyFee, Math.Rounding.Up); return _convertToShares(assets, Math.Rounding.Up); } @@ -119,8 +123,15 @@ contract BeefyAdapter is AdapterBase, WithRewards { uint256 assets = _convertToAssets(shares, Math.Rounding.Down); IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); - uint256 beefyFee = strat.withdrawalFee(); - if (beefyFee > 0) assets -= assets.mulDiv(beefyFee, BPS_DENOMINATOR, Math.Rounding.Up); + + uint256 beefyFee; + try strat.withdrawalFee() returns (uint256 _beefyFee) { + beefyFee = _beefyFee; + } catch { + beefyFee = strat.withdrawFee(); + } + + if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR - beefyFee, BPS_DENOMINATOR, Math.Rounding.Up); return assets; } @@ -138,6 +149,7 @@ contract BeefyAdapter is AdapterBase, WithRewards { /// @notice Withdraw from the beefy vault and optionally from the booster given its configured function _protocolWithdraw(uint256, uint256 shares) internal virtual override { uint256 beefyShares = convertToUnderlyingShares(0, shares); + if (address(beefyBooster) != address(0)) beefyBooster.withdraw(beefyShares); beefyVault.withdraw(beefyShares); } diff --git a/src/vault/adapter/beefy/IBeefy.sol b/src/vault/adapter/beefy/IBeefy.sol index 4f14bdc..d72a398 100644 --- a/src/vault/adapter/beefy/IBeefy.sol +++ b/src/vault/adapter/beefy/IBeefy.sol @@ -53,5 +53,7 @@ interface IBeefyBalanceCheck { } interface IBeefyStrat { + function withdrawFee() external view returns (uint256); + function withdrawalFee() external view returns (uint256); } diff --git a/src/vault/adapter/convex/ConvexAdapter.sol b/src/vault/adapter/convex/ConvexAdapter.sol index ed0ea8b..344f565 100644 --- a/src/vault/adapter/convex/ConvexAdapter.sol +++ b/src/vault/adapter/convex/ConvexAdapter.sol @@ -46,11 +46,7 @@ contract ConvexAdapter is AdapterBase, WithRewards { * @dev `_pid` - The poolId for lpToken. * @dev This function is called by the factory contract when deploying a new vault. */ - function initialize( - bytes memory adapterInitData, - address registry, - bytes memory convexInitData - ) public initializer { + function initialize(bytes memory adapterInitData, address registry, bytes memory convexInitData) public initializer { __AdapterBase_init(adapterInitData); uint256 _pid = abi.decode(convexInitData, (uint256)); @@ -92,9 +88,11 @@ contract ConvexAdapter is AdapterBase, WithRewards { function rewardTokens() external view override returns (address[] memory) { uint256 len = convexRewards.extraRewardsLength(); - address[] memory tokens = new address[](len); + address[] memory tokens = new address[](len + 1); + tokens[0] = 0xD533a949740bb3306d119CC777fa900bA034cd52; // CRV + for (uint256 i; i < len; i++) { - tokens[i] = convexRewards.extraRewards(i).rewardToken(); + tokens[i + 1] = convexRewards.extraRewards(i).rewardToken(); } return tokens; } diff --git a/src/vault/adapter/sushi/IMasterChef.sol b/src/vault/adapter/sushi/IMasterChef.sol new file mode 100644 index 0000000..41fb1df --- /dev/null +++ b/src/vault/adapter/sushi/IMasterChef.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +interface IMasterChef { + struct PoolInfo { + address lpToken; + uint256 allocPoint; + uint256 lastRewardBlock; + uint256 accSushiPerShare; + } + + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + function poolInfo(uint256 pid) external view returns (IMasterChef.PoolInfo memory); + + function userInfo(uint256 pid, address adapterAddress) external view returns (IMasterChef.UserInfo memory); + + function totalAllocPoint() external view returns (uint256); + + function deposit(uint256 _pid, uint256 _amount) external; + + function withdraw(uint256 _pid, uint256 _amount) external; + + function enterStaking(uint256 _amount) external; + + function leaveStaking(uint256 _amount) external; + + function emergencyWithdraw(uint256 _pid) external; + + function pendingSushi(uint256 _pid, address _user) external view returns (uint256); +} diff --git a/src/vault/adapter/sushi/MasterChefAdapter.sol b/src/vault/adapter/sushi/MasterChefAdapter.sol new file mode 100644 index 0000000..89413df --- /dev/null +++ b/src/vault/adapter/sushi/MasterChefAdapter.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../abstracts/AdapterBase.sol"; +import { WithRewards, IWithRewards } from "../abstracts/WithRewards.sol"; +import { IMasterChef } from "./IMasterChef.sol"; + +/** + * @title MasterChef Adapter + * @notice ERC4626 wrapper for MasterChef Vaults. + * + * An ERC4626 compliant Wrapper for https://github.com/sushiswap/sushiswap/blob/archieve/canary/contracts/MasterChefV2.sol. + * Allows wrapping MasterChef Vaults. + */ +contract MasterChefAdapter is AdapterBase, WithRewards { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + // @notice The MasterChef contract + IMasterChef public masterChef; + + // @notice The address of the reward token + address public rewardsToken; + + // @notice The pool ID + uint256 public pid; + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + error InvalidAsset(); + + /** + * @notice Initialize a new MasterChef Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @dev `_pid` - The poolId for lpToken. + * @dev `_rewardsToken` - The token rewarded by the MasterChef contract (Sushi, Cake...) + * @dev This function is called by the factory contract when deploying a new vault. + */ + + function initialize( + bytes memory adapterInitData, + address registry, + bytes memory masterchefInitData + ) external initializer { + __AdapterBase_init(adapterInitData); + + (uint256 _pid, address _rewardsToken) = abi.decode(masterchefInitData, (uint256, address)); + + masterChef = IMasterChef(registry); + IMasterChef.PoolInfo memory pool = masterChef.poolInfo(_pid); + + if (pool.lpToken != asset()) revert InvalidAsset(); + + pid = _pid; + rewardsToken = _rewardsToken; + + _name = string.concat("Popcorn MasterChef", IERC20Metadata(asset()).name(), " Adapter"); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + IERC20(pool.lpToken).approve(address(masterChef), type(uint256).max); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculates the total amount of underlying tokens the Vault holds. + /// @return The total amount of underlying tokens the Vault holds. + + function _totalAssets() internal view override returns (uint256) { + IMasterChef.UserInfo memory user = masterChef.userInfo(pid, address(this)); + return user.amount; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit(uint256 amount, uint256) internal override { + masterChef.deposit(pid, amount); + } + + function _protocolWithdraw(uint256 amount, uint256) internal override { + masterChef.withdraw(pid, amount); + } + + /*////////////////////////////////////////////////////////////// + STRATEGY LOGIC + //////////////////////////////////////////////////////////////*/ + /// @notice Claim rewards from the masterChef + function claim() public override onlyStrategy { + masterChef.deposit(pid, 0); + } + + /// @notice The token rewarded + function rewardTokens() external view override returns (address[] memory) { + address[] memory _rewardTokens = new address[](1); + _rewardTokens[0] = rewardsToken; + return _rewardTokens; + } + + /*////////////////////////////////////////////////////////////// + EIP-165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { + return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; + } +} diff --git a/src/vault/adapter/yearn/YearnAdapter.sol b/src/vault/adapter/yearn/YearnAdapter.sol index ee74a91..48c1335 100644 --- a/src/vault/adapter/yearn/YearnAdapter.sol +++ b/src/vault/adapter/yearn/YearnAdapter.sol @@ -22,7 +22,7 @@ contract YearnAdapter is AdapterBase { string internal _symbol; VaultAPI public yVault; - uint256 constant DEGRADATION_COEFFICIENT = 10**18; + uint256 constant DEGRADATION_COEFFICIENT = 10 ** 18; /** * @notice Initialize a new Yearn Adapter. @@ -31,11 +31,7 @@ contract YearnAdapter is AdapterBase { * @dev This function is called by the factory contract when deploying a new vault. * @dev The yearn registry will be used given the `asset` from `adapterInitData` to find the latest yVault. */ - function initialize( - bytes memory adapterInitData, - address externalRegistry, - bytes memory - ) external initializer { + function initialize(bytes memory adapterInitData, address externalRegistry, bytes memory) external initializer { (address _asset, , , , , ) = abi.decode(adapterInitData, (address, address, address, uint256, bytes4[8], bytes)); __AdapterBase_init(adapterInitData); @@ -102,20 +98,20 @@ contract YearnAdapter is AdapterBase { return supply == 0 ? shares : shares.mulDiv(yVault.balanceOf(address(this)), supply, Math.Rounding.Up); } - function previewDeposit(uint256 assets) public view virtual override returns (uint256) { - return paused() ? 0 : _convertToShares(assets - 10, Math.Rounding.Down); + function previewDeposit(uint256 assets) public view override returns (uint256) { + return paused() ? 0 : _convertToShares(assets - 0, Math.Rounding.Down); } - function previewMint(uint256 shares) public view virtual override returns (uint256) { - return paused() ? 0 : _convertToAssets(shares + 10, Math.Rounding.Up); + function previewMint(uint256 shares) public view override returns (uint256) { + return paused() ? 0 : _convertToAssets(shares + 0, Math.Rounding.Up); } - function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { - return _convertToShares(assets + 10, Math.Rounding.Up); + function previewWithdraw(uint256 assets) public view override returns (uint256) { + return _convertToShares(assets + 0, Math.Rounding.Up); } - function previewRedeem(uint256 shares) public view virtual override returns (uint256) { - return _convertToAssets(shares - 10, Math.Rounding.Down); + function previewRedeem(uint256 shares) public view override returns (uint256) { + return _convertToAssets(shares - 0, Math.Rounding.Down); } /*////////////////////////////////////////////////////////////// diff --git a/test/MultiRewardStaking.t.sol b/test/MultiRewardStaking.t.sol index 9f3f133..03a4295 100644 --- a/test/MultiRewardStaking.t.sol +++ b/test/MultiRewardStaking.t.sol @@ -718,6 +718,10 @@ contract MultiRewardStakingTest is Test { staking.addRewardToken(iRewardToken1, 0.1 ether, 10 ether, true, 1e19, 100, 0); } + function testFail__addRewardToken_0_rewardsSpeed_amount_larger_0_and_0_shares() public { + staking.addRewardToken(iRewardToken1, 0, 10, false, 0, 0, 0); + } + /*////////////////////////////////////////////////////////////// CHANGE REWARDS SPEED LOGIC //////////////////////////////////////////////////////////////*/ @@ -850,6 +854,11 @@ contract MultiRewardStakingTest is Test { rewardToken1.mint(address(this), 10 ether); rewardToken1.approve(address(staking), 10 ether); + stakingToken.mint(address(this), 1 ether); + stakingToken.approve(address(staking), 1 ether); + + staking.deposit(1 ether); + (, , uint32 oldRewardsEndTimestamp, , ) = staking.rewardInfos(iRewardToken1); vm.expectEmit(false, false, false, true, address(staking)); @@ -898,6 +907,15 @@ contract MultiRewardStakingTest is Test { staking.fundReward(IERC20(address(0)), 10 ether); } + function testFail__fundReward_0_rewardsSpeed_zero_shares() public { + _addRewardTokenWithZeroRewardsSpeed(rewardToken1); + + rewardToken1.mint(address(this), 10 ether); + rewardToken1.approve(address(staking), 10 ether); + + staking.fundReward(iRewardToken1, 10 ether); + } + /*////////////////////////////////////////////////////////////// CLAIM LOGIC //////////////////////////////////////////////////////////////*/ diff --git a/test/utils/mocks/MockStrategyClaimer.sol b/test/utils/mocks/MockStrategyClaimer.sol new file mode 100644 index 0000000..2cc84c2 --- /dev/null +++ b/test/utils/mocks/MockStrategyClaimer.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.8.15; + +import { IWithRewards } from "../../../src/interfaces/vault/IWithRewards.sol"; +import { IERC20Upgradeable as IERC20 } from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; + +contract MockStrategyClaimer { + event SelectorsVerified(); + event AdapterVerified(); + event StrategySetup(); + event StrategyExecuted(); + event Claimed(uint256 amount); + + function verifyAdapterSelectorCompatibility(bytes4[8] memory) public { + emit SelectorsVerified(); + } + + function verifyAdapterCompatibility(bytes memory) public { + emit AdapterVerified(); + } + + function setUp(bytes memory) public { + emit StrategySetup(); + } + + function harvest() public { + IWithRewards(address(this)).claim(); + address[] memory rewardTokens = IWithRewards(address(this)).rewardTokens(); + + for (uint256 i; i < rewardTokens.length; i++) { + emit Claimed(IERC20(rewardTokens[i]).balanceOf(address(this))); + } + + emit StrategyExecuted(); + } +} diff --git a/test/vault/FeeRecipientProxy.t.sol b/test/vault/FeeRecipientProxy.t.sol new file mode 100644 index 0000000..161b2ee --- /dev/null +++ b/test/vault/FeeRecipientProxy.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { FeeRecipientProxy } from "../../src/vault/FeeRecipientProxy.sol"; +import { MockERC20 } from "../utils/mocks/MockERC20.sol"; +import { IERC20Upgradeable as IERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +contract FeeRecipientProxyTest is Test { + FeeRecipientProxy feeRecipient; + MockERC20 asset1; + MockERC20 asset2; + MockERC20 asset3; + + address owner = address(0x4444); + + IERC20[] tokens; + + event TokenApproved(uint8 len); + event TokenApprovalVoided(uint8 len); + + function setUp() public { + asset1 = new MockERC20("Mock Token", "TKN", 18); + asset2 = new MockERC20("Mock Token", "TKN", 6); + asset3 = new MockERC20("Mock Token", "TKN", 2); + + feeRecipient = new FeeRecipientProxy(owner); + } + + // 2. Approve gifted token + // 3. Owner pulls approved token + // 4. Owner cant pull non-approved token + // 5. void approval + // 6. Approval change only works when all is voided + + /*////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////*/ + + function giftToken(MockERC20 asset, uint256 amount) internal { + asset.mint(address(feeRecipient), amount); + } + + function approveToken(IERC20[] memory _tokens) internal { + vm.prank(owner); + feeRecipient.approveToken(_tokens); + } + + /*////////////////////////////////////////////////////////////// + APPROVE TOKEN + //////////////////////////////////////////////////////////////*/ + + function test__approveToken() public { + tokens.push(IERC20(address(asset1))); + + vm.prank(owner); + vm.expectEmit(false, false, false, true, address(feeRecipient)); + emit TokenApproved(uint8(1)); + + feeRecipient.approveToken(tokens); + + assertEq(asset1.allowance(address(feeRecipient), owner), type(uint256).max); + assertEq(feeRecipient.approvals(), 1); + + tokens[0] = IERC20(address(asset2)); + tokens.push(IERC20(address(asset3))); + + vm.prank(owner); + vm.expectEmit(false, false, false, true, address(feeRecipient)); + emit TokenApproved(uint8(2)); + + feeRecipient.approveToken(tokens); + + assertEq(asset1.allowance(address(feeRecipient), owner), type(uint256).max); + assertEq(asset2.allowance(address(feeRecipient), owner), type(uint256).max); + assertEq(asset3.allowance(address(feeRecipient), owner), type(uint256).max); + assertEq(feeRecipient.approvals(), 3); + + // Transfer approved token to owner + giftToken(asset1, 1e18); + + vm.prank(owner); + asset1.transferFrom(address(feeRecipient), owner, 1e18); + + assertEq(asset1.balanceOf(address(feeRecipient)), 0); + assertEq(asset1.balanceOf(owner), 1e18); + } + + function testReverts__approveToken_token_already_approved() public { + tokens.push(IERC20(address(asset1))); + + vm.startPrank(owner); + feeRecipient.approveToken(tokens); + + vm.expectRevert(abi.encodeWithSelector(FeeRecipientProxy.TokenAlreadyApproved.selector, address(asset1))); + feeRecipient.approveToken(tokens); + } + + function testFail__approveToken_nonOwner() public { + tokens.push(IERC20(address(asset1))); + + feeRecipient.approveToken(tokens); + } + + /*////////////////////////////////////////////////////////////// + VOID TOKEN APPROVAL + //////////////////////////////////////////////////////////////*/ + + function test__voidTokenApproval() public { + tokens.push(IERC20(address(asset1))); + tokens.push(IERC20(address(asset2))); + approveToken(tokens); + + vm.prank(owner); + vm.expectEmit(false, false, false, true, address(feeRecipient)); + emit TokenApprovalVoided(uint8(2)); + + feeRecipient.voidTokenApproval(tokens); + + assertEq(asset1.allowance(address(feeRecipient), owner), 0); + assertEq(asset2.allowance(address(feeRecipient), owner), 0); + assertEq(feeRecipient.approvals(), 0); + } + + function testRevert__voidTokenApproval_already_voided() public { + tokens.push(IERC20(address(asset1))); + + vm.startPrank(owner); + vm.expectRevert(abi.encodeWithSelector(FeeRecipientProxy.TokenApprovalAlreadyVoided.selector, address(asset1))); + feeRecipient.voidTokenApproval(tokens); + } + + function testFail__voidTokenApproval_nonOwner() public { + tokens.push(IERC20(address(asset1))); + + feeRecipient.voidTokenApproval(tokens); + } + + /*////////////////////////////////////////////////////////////// + ACCEPT OWNERSHIP + //////////////////////////////////////////////////////////////*/ + + function test__acceptOwnership() public { + vm.prank(owner); + feeRecipient.nominateNewOwner(address(this)); + + feeRecipient.acceptOwnership(); + + assertEq(feeRecipient.owner(), address(this)); + assertEq(feeRecipient.nominatedOwner(), address(0)); + } + + function testFail__acceptOwnership_approvalCount_greater_0() public { + tokens.push(IERC20(address(asset1))); + approveToken(tokens); + + vm.prank(owner); + feeRecipient.nominateNewOwner(address(this)); + + feeRecipient.acceptOwnership(); + } + + function testFail__acceptOwnership_nonOwner() public { + vm.prank(owner); + feeRecipient.nominateNewOwner(address(this)); + + vm.prank(owner); + feeRecipient.acceptOwnership(); + } +} diff --git a/test/vault/Vault.t.sol b/test/vault/Vault.t.sol index 4f16938..23f0510 100644 --- a/test/vault/Vault.t.sol +++ b/test/vault/Vault.t.sol @@ -553,7 +553,7 @@ contract VaultTest is Test { assertEq(vault.balanceOf(feeRecipient), expectedFeeInShares, "bal"); // There should be a new High Water Mark - assertApproxEqRel(vault.highWaterMark(), totalAssets / 1e9, 10, "hwm"); + assertApproxEqRel(vault.highWaterMark(), totalAssets / 1e9, 30, "hwm"); } function test_performanceFee2() public { diff --git a/test/vault/VaultController.t.sol b/test/vault/VaultController.t.sol index b867894..f1a4abe 100644 --- a/test/vault/VaultController.t.sol +++ b/test/vault/VaultController.t.sol @@ -615,6 +615,39 @@ contract VaultControllerTest is Test { ); } + function testFail__deployVault_without_adapter_nor_adapterData() public { + addTemplate("Adapter", templateId, adapterImpl, true, true); + addTemplate("Strategy", "MockStrategy", strategyImpl, false, true); + addTemplate("Vault", "V1", vaultImpl, true, true); + controller.setPerformanceFee(uint256(1000)); + controller.setHarvestCooldown(1 days); + + controller.deployVault( + VaultInitParams({ + asset: iAsset, + adapter: IERC4626(address(0)), + fees: VaultFees({ deposit: 100, withdrawal: 200, management: 300, performance: 400 }), + feeRecipient: feeRecipient, + depositLimit: type(uint256).max, + owner: address(this) + }), + DeploymentArgs({ id: "", data: "" }), + DeploymentArgs({ id: "", data: "" }), + false, + "", + VaultMetadata({ + vault: address(0), + staking: address(0), + creator: address(this), + metadataCID: metadataCid, + swapTokenAddresses: swapTokenAddresses, + swapAddress: address(0x5555), + exchange: uint256(1) + }), + 0 + ); + } + /*////////////////////////////////////////////////////////////// ADAPTER DEPLOYMENT //////////////////////////////////////////////////////////////*/ diff --git a/test/vault/integration/aaveV2/AaveV2Adapter.t.sol b/test/vault/integration/aaveV2/AaveV2Adapter.t.sol index a07bf6e..e24b8db 100644 --- a/test/vault/integration/aaveV2/AaveV2Adapter.t.sol +++ b/test/vault/integration/aaveV2/AaveV2Adapter.t.sol @@ -5,9 +5,10 @@ pragma solidity ^0.8.15; import { Test } from "forge-std/Test.sol"; -import { AaveV2Adapter, SafeERC20, IERC20, IERC20Metadata, Math, ILendingPool, IAaveMining, IAToken, IProtocolDataProvider, DataTypes } from "../../../../src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol"; +import { AaveV2Adapter, SafeERC20, IERC20, IERC20Metadata, Math, ILendingPool, IAaveMining, IAToken, IProtocolDataProvider, DataTypes, IStrategy, IWithRewards } from "../../../../src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol"; import { AaveV2TestConfigStorage, AaveV2TestConfig } from "./AaveV2TestConfigStorage.sol"; import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../abstract/AbstractAdapterTest.sol"; +import { MockStrategyClaimer } from "../../../utils/mocks/MockStrategyClaimer.sol"; contract AaveV2AdapterTest is AbstractAdapterTest { using Math for uint256; @@ -95,4 +96,10 @@ contract AaveV2AdapterTest is AbstractAdapterTest { assertEq(asset.allowance(address(adapter), address(lendingPool)), type(uint256).max, "allowance"); } + + /*////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////*/ + + // Cant test claim for Aave since they diabled it. Geist a fork of Aave uses a slightly different interface on the Mining contract. } diff --git a/test/vault/integration/aaveV3/AaveV3Adapter.t.sol b/test/vault/integration/aaveV3/AaveV3Adapter.t.sol index 5ebf7e6..ce4a3fb 100644 --- a/test/vault/integration/aaveV3/AaveV3Adapter.t.sol +++ b/test/vault/integration/aaveV3/AaveV3Adapter.t.sol @@ -46,8 +46,8 @@ contract AaveV3AdapterTest is AbstractAdapterTest { vm.label(address(this), "test"); adapter.initialize(abi.encode(asset, address(this), strategy, 0, sigs, ""), externalRegistry, ""); - - defaultAmount = 10**IERC20Metadata(address(asset)).decimals(); + + defaultAmount = 10 ** IERC20Metadata(address(asset)).decimals(); minFuzz = defaultAmount * 10; raise = defaultAmount * 100_000_000; maxAssets = defaultAmount * 100; @@ -107,4 +107,10 @@ contract AaveV3AdapterTest is AbstractAdapterTest { uint128 supplyRate = data.currentLiquidityRate; return uint256(supplyRate / 1e9); } + + /*////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////*/ + + // Cant test claim for Aave since they dont use it yet. } diff --git a/test/vault/integration/abstract/AbstractAdapterTest.sol b/test/vault/integration/abstract/AbstractAdapterTest.sol index 1030629..7719187 100644 --- a/test/vault/integration/abstract/AbstractAdapterTest.sol +++ b/test/vault/integration/abstract/AbstractAdapterTest.sol @@ -65,7 +65,7 @@ contract AbstractAdapterTest is PropertyTest { _asset_ = address(asset_); _delta_ = delta_; - defaultAmount = 10**IERC20Metadata(address(asset_)).decimals() * 1e9; + defaultAmount = 10 ** IERC20Metadata(address(asset_)).decimals() * 1e9; raise = defaultAmount; maxAssets = defaultAmount * 1000; @@ -250,7 +250,7 @@ contract AbstractAdapterTest is PropertyTest { function test__previewWithdraw(uint8 fuzzAmount) public virtual { uint256 amount = bound(uint256(fuzzAmount), minFuzz, maxAssets); - uint256 reqAssets = (adapter.previewMint(adapter.previewWithdraw(amount)) * 10) / 9; + uint256 reqAssets = adapter.previewMint(adapter.previewWithdraw(amount)) * 10; _mintFor(reqAssets, bob); vm.prank(bob); adapter.deposit(reqAssets, bob); @@ -261,7 +261,7 @@ contract AbstractAdapterTest is PropertyTest { function test__previewRedeem(uint8 fuzzAmount) public virtual { uint256 amount = bound(uint256(fuzzAmount), minFuzz, maxShares); - uint256 reqAssets = (adapter.previewMint(amount) * 10) / 9; + uint256 reqAssets = adapter.previewMint(amount) * 10; _mintFor(reqAssets, bob); vm.prank(bob); adapter.deposit(reqAssets, bob); @@ -311,7 +311,7 @@ contract AbstractAdapterTest is PropertyTest { for (uint8 i; i < len; i++) { if (i > 0) overrideSetup(testConfigStorage.getTestConfig(i)); - uint256 reqAssets = (adapter.previewMint(adapter.previewWithdraw(amount)) * 10) / 8; + uint256 reqAssets = adapter.previewMint(adapter.previewWithdraw(amount)) * 10; _mintFor(reqAssets, bob); vm.prank(bob); adapter.deposit(reqAssets, bob); @@ -335,7 +335,7 @@ contract AbstractAdapterTest is PropertyTest { for (uint8 i; i < len; i++) { if (i > 0) overrideSetup(testConfigStorage.getTestConfig(i)); - uint256 reqAssets = (adapter.previewMint(amount) * 10) / 9; + uint256 reqAssets = adapter.previewMint(amount) * 10; _mintFor(reqAssets, bob); vm.prank(bob); adapter.deposit(reqAssets, bob); @@ -425,10 +425,10 @@ contract AbstractAdapterTest is PropertyTest { vm.startPrank(bob); // Deposit and mint are paused (maxDeposit/maxMint are set to 0 on pause) - vm.expectRevert(abi.encodeWithSelector(MaxError.selector, defaultAmount)); + vm.expectRevert(); adapter.deposit(defaultAmount, bob); - vm.expectRevert(abi.encodeWithSelector(MaxError.selector, defaultAmount)); + vm.expectRevert(); adapter.mint(defaultAmount, bob); // Withdraw and Redeem dont revert @@ -494,8 +494,11 @@ contract AbstractAdapterTest is PropertyTest { increasePricePerShare(raise); uint256 gain = ((adapter.convertToAssets(1e18) - adapter.highWaterMark()) * adapter.totalSupply()) / 1e18; + emit log_named_uint("gain", gain); uint256 fee = (gain * performanceFee) / 1e18; + emit log_named_uint("fee", fee); uint256 expectedFee = adapter.convertToShares(fee); + emit log_named_uint("expectedFee", expectedFee); vm.expectEmit(false, false, false, true, address(adapter)); emit Harvested(); @@ -558,13 +561,13 @@ contract AbstractAdapterTest is PropertyTest { //////////////////////////////////////////////////////////////*/ // OPTIONAL - function testClaim() public virtual {} + function test__claim() public virtual {} /*////////////////////////////////////////////////////////////// PERMIT //////////////////////////////////////////////////////////////*/ - function testPermit() public { + function test__permit() public { uint256 privateKey = 0xBEEF; address owner = vm.addr(privateKey); diff --git a/test/vault/integration/beefy/BeefyAdapter.t.sol b/test/vault/integration/beefy/BeefyAdapter.t.sol index 148e156..8165018 100644 --- a/test/vault/integration/beefy/BeefyAdapter.t.sol +++ b/test/vault/integration/beefy/BeefyAdapter.t.sol @@ -5,11 +5,12 @@ pragma solidity ^0.8.15; import { Test } from "forge-std/Test.sol"; -import { BeefyAdapter, SafeERC20, IERC20, IERC20Metadata, IBeefyVault, IBeefyBooster, IBeefyBalanceCheck } from "../../../../src/vault/adapter/beefy/BeefyAdapter.sol"; +import { BeefyAdapter, SafeERC20, IERC20, IERC20Metadata, IBeefyVault, IBeefyBooster, IBeefyStrat, IBeefyBalanceCheck, IWithRewards, IStrategy } from "../../../../src/vault/adapter/beefy/BeefyAdapter.sol"; import { BeefyTestConfigStorage, BeefyTestConfig } from "./BeefyTestConfigStorage.sol"; import { AbstractAdapterTest, ITestConfigStorage, IAdapter, Math } from "../abstract/AbstractAdapterTest.sol"; import { IPermissionRegistry, Permission } from "../../../../src/interfaces/vault/IPermissionRegistry.sol"; import { PermissionRegistry } from "../../../../src/vault/PermissionRegistry.sol"; +import { MockStrategyClaimer } from "../../../utils/mocks/MockStrategyClaimer.sol"; contract BeefyAdapterTest is AbstractAdapterTest { using Math for uint256; @@ -20,9 +21,6 @@ contract BeefyAdapterTest is AbstractAdapterTest { IPermissionRegistry permissionRegistry; function setUp() public { - uint256 forkId = vm.createSelectFork(vm.rpcUrl("polygon")); - vm.selectFork(forkId); - testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); _setUpTest(testConfigStorage.getTestConfig(0)); @@ -33,7 +31,14 @@ contract BeefyAdapterTest is AbstractAdapterTest { } function _setUpTest(bytes memory testConfig) internal { - (address _beefyVault, address _beefyBooster) = abi.decode(testConfig, (address, address)); + (address _beefyVault, address _beefyBooster, string memory _network) = abi.decode( + testConfig, + (address, address, string) + ); + + uint256 forkId = vm.createSelectFork(vm.rpcUrl(_network)); + vm.selectFork(forkId); + beefyVault = IBeefyVault(_beefyVault); beefyBooster = IBeefyBooster(_beefyBooster); beefyBalanceCheck = IBeefyBalanceCheck(_beefyBooster == address(0) ? _beefyVault : _beefyBooster); @@ -46,7 +51,14 @@ contract BeefyAdapterTest is AbstractAdapterTest { setPermission(_beefyBooster, true, false); } - setUpBaseTest(IERC20(IBeefyVault(beefyVault).want()), address(new BeefyAdapter()), address(permissionRegistry), 10, "Beefy ", true); + setUpBaseTest( + IERC20(IBeefyVault(beefyVault).want()), + address(new BeefyAdapter()), + address(permissionRegistry), + 10, + "Beefy ", + true + ); vm.label(_beefyVault, "beefyVault"); vm.label(_beefyBooster, "beefyBooster"); @@ -90,11 +102,7 @@ contract BeefyAdapterTest is AbstractAdapterTest { ); } - function setPermission( - address target, - bool endorsed, - bool rejected - ) public { + function setPermission(address target, bool endorsed, bool rejected) public { address[] memory targets = new address[](1); Permission[] memory permissions = new Permission[](1); targets[0] = target; @@ -106,6 +114,46 @@ contract BeefyAdapterTest is AbstractAdapterTest { INITIALIZATION //////////////////////////////////////////////////////////////*/ + function test__initialization() public override { + testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); + + (address _beefyVault, address _beefyBooster, string memory _network) = abi.decode( + testConfigStorage.getTestConfig(0), + (address, address, string) + ); + + createAdapter(); + uint256 callTime = block.timestamp; + + if (address(strategy) != address(0)) { + vm.expectEmit(false, false, false, true, address(strategy)); + emit SelectorsVerified(); + vm.expectEmit(false, false, false, true, address(strategy)); + emit AdapterVerified(); + vm.expectEmit(false, false, false, true, address(strategy)); + emit StrategySetup(); + } + vm.expectEmit(false, false, false, true, address(adapter)); + emit Initialized(uint8(1)); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + abi.encode(_beefyVault, _beefyBooster) + ); + + assertEq(adapter.owner(), address(this), "owner"); + assertEq(adapter.strategy(), address(strategy), "strategy"); + assertEq(adapter.harvestCooldown(), 0, "harvestCooldown"); + assertEq(adapter.strategyConfig(), "", "strategyConfig"); + assertEq( + IERC20Metadata(address(adapter)).decimals(), + IERC20Metadata(address(asset)).decimals() + adapter.decimalOffset(), + "decimals" + ); + + verify_adapterInit(); + } + function verify_adapterInit() public override { assertEq(adapter.asset(), beefyVault.want(), "asset"); assertEq( @@ -165,6 +213,98 @@ contract BeefyAdapterTest is AbstractAdapterTest { ); } + /*////////////////////////////////////////////////////////////// + DEPOSIT/MINT/WITHDRAW/REDEEM + //////////////////////////////////////////////////////////////*/ + + function test__deposit(uint8 fuzzAmount) public override { + testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); + + uint256 amount = bound(uint256(fuzzAmount), minFuzz, maxAssets); + uint8 len = uint8(testConfigStorage.getTestConfigLength()); + for (uint8 i; i < len; i++) { + if (i > 0) overrideSetup(testConfigStorage.getTestConfig(i)); + + _mintFor(amount, bob); + prop_deposit(bob, bob, amount, testId); + + increasePricePerShare(raise); + + _mintFor(amount, bob); + prop_deposit(bob, alice, amount, testId); + } + } + + function test__mint(uint8 fuzzAmount) public override { + testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); + + uint256 amount = bound(uint256(fuzzAmount), minFuzz, maxShares); + uint8 len = uint8(testConfigStorage.getTestConfigLength()); + for (uint8 i; i < len; i++) { + if (i > 0) overrideSetup(testConfigStorage.getTestConfig(i)); + + _mintFor(adapter.previewMint(amount), bob); + prop_mint(bob, bob, amount, testId); + + increasePricePerShare(raise); + + _mintFor(adapter.previewMint(amount), bob); + prop_mint(bob, alice, amount, testId); + } + } + + function test__withdraw(uint8 fuzzAmount) public override { + testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); + + uint256 amount = bound(uint256(fuzzAmount), minFuzz, maxAssets); + uint8 len = uint8(testConfigStorage.getTestConfigLength()); + for (uint8 i; i < len; i++) { + if (i > 0) overrideSetup(testConfigStorage.getTestConfig(i)); + + uint256 reqAssets = (adapter.previewMint(adapter.previewWithdraw(amount)) * 10) / 8; + _mintFor(reqAssets, bob); + vm.prank(bob); + adapter.deposit(reqAssets, bob); + prop_withdraw(bob, bob, amount, testId); + + _mintFor(reqAssets, bob); + vm.prank(bob); + adapter.deposit(reqAssets, bob); + + increasePricePerShare(raise); + + vm.prank(bob); + adapter.approve(alice, type(uint256).max); + prop_withdraw(alice, bob, amount, testId); + } + } + + function test__redeem(uint8 fuzzAmount) public override { + testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); + + uint256 amount = bound(uint256(fuzzAmount), minFuzz, maxShares); + uint8 len = uint8(testConfigStorage.getTestConfigLength()); + for (uint8 i; i < len; i++) { + if (i > 0) overrideSetup(testConfigStorage.getTestConfig(i)); + + uint256 reqAssets = (adapter.previewMint(amount) * 10) / 9; + _mintFor(reqAssets, bob); + vm.prank(bob); + adapter.deposit(reqAssets, bob); + prop_redeem(bob, bob, amount, testId); + + _mintFor(reqAssets, bob); + vm.prank(bob); + adapter.deposit(reqAssets, bob); + + increasePricePerShare(raise); + + vm.prank(bob); + adapter.approve(alice, type(uint256).max); + prop_redeem(alice, bob, amount, testId); + } + } + /*////////////////////////////////////////////////////////////// ROUNDTRIP TESTS //////////////////////////////////////////////////////////////*/ @@ -216,11 +356,41 @@ contract BeefyAdapterTest is AbstractAdapterTest { assertApproxEqAbs(oldTotalAssets, adapter.totalAssets(), 50, "totalAssets"); assertApproxEqAbs(oldTotalSupply, adapter.totalSupply(), 50, "totalSupply"); assertApproxEqAbs(asset.balanceOf(address(adapter)), 0, 50, "asset balance"); - assertApproxEqAbs(iouBalance(), oldIouBalance, 50, "iou balance"); + assertApproxEqRel(iouBalance(), oldIouBalance, 1, "iou balance"); // Deposit and mint dont revert vm.startPrank(bob); adapter.deposit(defaultAmount, bob); adapter.mint(defaultAmount, bob); } + + /*////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////*/ + + function test__claim() public override { + testConfigStorage = ITestConfigStorage(address(new BeefyTestConfigStorage())); + strategy = IStrategy(address(new MockStrategyClaimer())); + createAdapter(); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfigStorage.getTestConfig(0) + ); + + _mintFor(1000e18, bob); + + vm.prank(bob); + adapter.deposit(1000e18, bob); + + vm.warp(block.timestamp + 10 days); + + vm.prank(bob); + adapter.withdraw(1, bob, bob); + + address[] memory rewardTokens = IWithRewards(address(adapter)).rewardTokens(); + assertEq(rewardTokens[0], beefyBooster.rewardToken()); + + assertGt(IERC20(rewardTokens[0]).balanceOf(address(adapter)), 0); + } } diff --git a/test/vault/integration/beefy/BeefyTestConfigStorage.sol b/test/vault/integration/beefy/BeefyTestConfigStorage.sol index 583d028..de995fe 100644 --- a/test/vault/integration/beefy/BeefyTestConfigStorage.sol +++ b/test/vault/integration/beefy/BeefyTestConfigStorage.sol @@ -8,25 +8,24 @@ import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; struct BeefyTestConfig { address beefyVault; address beefyBooster; + string network; } contract BeefyTestConfigStorage is ITestConfigStorage { BeefyTestConfig[] internal testConfigs; constructor() { - // Polygon - stMATIC-MATIC vault + // Polygon - wstEth-ETH vault testConfigs.push( - BeefyTestConfig(0xF79BF908d0e6d8E7054375CD80dD33424B1980bf, 0x69C28193185CFcd42D62690Db3767915872bC5EA) + BeefyTestConfig(0x1d81c50d5aB5f095894c41B41BA49B9873033399, 0x4Cc44C30f4d3789AE8d8e9C8dE409D11c79C5CE3, "polygon") ); - // Polygon - MAI-FRAX sLP vault - //testConfigs.push(BeefyTestConfig(0xbC94bDb5393CBABF9B319E892abC95B93B5949A8, address(0))); - - //testConfigs.push(BeefyTestConfig(0xc10C75247f503cc7B7496D72a6F3C443adDB7110, address(0))); + // Ethereum - stEth-ETH vault + testConfigs.push(BeefyTestConfig(0xa7739fd3d12ac7F16D8329AF3Ee407e19De10D8D, address(0), "mainnet")); } function getTestConfig(uint256 i) public view returns (bytes memory) { - return abi.encode(testConfigs[i].beefyVault, testConfigs[i].beefyBooster); + return abi.encode(testConfigs[i].beefyVault, testConfigs[i].beefyBooster, testConfigs[i].network); } function getTestConfigLength() public view returns (uint256) { diff --git a/test/vault/integration/convex/ConvexAdapter.t.sol b/test/vault/integration/convex/ConvexAdapter.t.sol index 4a8ea26..e44bfe4 100644 --- a/test/vault/integration/convex/ConvexAdapter.t.sol +++ b/test/vault/integration/convex/ConvexAdapter.t.sol @@ -3,9 +3,10 @@ pragma solidity ^0.8.15; import { Test } from "forge-std/Test.sol"; -import { ConvexAdapter, SafeERC20, IERC20, IERC20Metadata, Math, IConvexBooster, IConvexRewards } from "../../../../src/vault/adapter/convex/ConvexAdapter.sol"; +import { ConvexAdapter, SafeERC20, IERC20, IERC20Metadata, Math, IConvexBooster, IConvexRewards, IWithRewards, IStrategy } from "../../../../src/vault/adapter/convex/ConvexAdapter.sol"; import { ConvexTestConfigStorage, ConvexTestConfig } from "./ConvexTestConfigStorage.sol"; import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../abstract/AbstractAdapterTest.sol"; +import { MockStrategyClaimer } from "../../../utils/mocks/MockStrategyClaimer.sol"; contract ConvexAdapterTest is AbstractAdapterTest { using Math for uint256; @@ -88,4 +89,35 @@ contract ConvexAdapterTest is AbstractAdapterTest { assertEq(asset.allowance(address(adapter), address(convexBooster)), type(uint256).max, "allowance"); } + + /*////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////*/ + + function test__claim() public override { + strategy = IStrategy(address(new MockStrategyClaimer())); + createAdapter(); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfigStorage.getTestConfig(0) + ); + + _mintFor(1000e18, bob); + + vm.prank(bob); + adapter.deposit(1000e18, bob); + + vm.warp(block.timestamp + 30 days); + + vm.prank(bob); + adapter.withdraw(1, bob, bob); + + address[] memory rewardTokens = IWithRewards(address(adapter)).rewardTokens(); + assertEq(rewardTokens[0], 0xD533a949740bb3306d119CC777fa900bA034cd52); // CRV + assertEq(rewardTokens[1], 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); // CVX + + assertGt(IERC20(rewardTokens[0]).balanceOf(address(adapter)), 0); + assertGt(IERC20(rewardTokens[1]).balanceOf(address(adapter)), 0); + } } diff --git a/test/vault/integration/sushi/MasterChefAdapter.t.sol b/test/vault/integration/sushi/MasterChefAdapter.t.sol new file mode 100644 index 0000000..d94a9c4 --- /dev/null +++ b/test/vault/integration/sushi/MasterChefAdapter.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; + +import { MasterChefAdapter, SafeERC20, IERC20, IERC20Metadata, Math, IMasterChef, IStrategy, IAdapter, IWithRewards } from "../../../../src/vault/adapter/sushi/MasterChefAdapter.sol"; +import { MasterChefTestConfigStorage, MasterChefTestConfig } from "./MasterChefTestConfigStorage.sol"; +import { AbstractAdapterTest, ITestConfigStorage } from "../abstract/AbstractAdapterTest.sol"; +import { MockStrategyClaimer } from "../../../utils/mocks/MockStrategyClaimer.sol"; + +contract MasterChefAdapterTest is AbstractAdapterTest { + using Math for uint256; + + IMasterChef public masterChef = IMasterChef(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd); + + address public rewardsToken; + uint256 pid; + + function setUp() public { + uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); + vm.selectFork(forkId); + + testConfigStorage = ITestConfigStorage(address(new MasterChefTestConfigStorage())); + + _setUpTest(testConfigStorage.getTestConfig(0)); + } + + function overrideSetup(bytes memory testConfig) public override { + _setUpTest(testConfig); + } + + function _setUpTest(bytes memory testConfig) internal { + (uint256 _pid, address _rewardsToken) = abi.decode(testConfig, (uint256, address)); + + pid = _pid; + rewardsToken = _rewardsToken; + IMasterChef.PoolInfo memory info = masterChef.poolInfo(_pid); + + setUpBaseTest(IERC20(info.lpToken), address(new MasterChefAdapter()), address(masterChef), 10, "MasterChef", true); + + vm.label(address(masterChef), "masterChef"); + vm.label(address(asset), "asset"); + vm.label(address(this), "test"); + + adapter.initialize(abi.encode(asset, address(this), strategy, 0, sigs, ""), externalRegistry, testConfig); + } + + /*////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////*/ + + // Verify that totalAssets returns the expected amount + function verify_totalAssets() public override { + deal(address(asset), bob, defaultAmount); + vm.startPrank(bob); + asset.approve(address(adapter), defaultAmount); + adapter.deposit(defaultAmount, bob); + vm.stopPrank(); + + assertEq( + adapter.totalAssets(), + adapter.convertToAssets(adapter.totalSupply()), + string.concat("totalSupply converted != totalAssets", baseTestId) + ); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function verify_adapterInit() public override { + assertEq(adapter.asset(), address(asset), "asset"); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + + assertEq(asset.allowance(address(adapter), address(masterChef)), type(uint256).max, "allowance"); + } + + /*////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////*/ + + function test__claim() public override { + strategy = IStrategy(address(new MockStrategyClaimer())); + createAdapter(); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfigStorage.getTestConfig(0) + ); + + _mintFor(1000e18, bob); + + vm.prank(bob); + adapter.deposit(1000e18, bob); + + vm.roll(block.number + 30); + + vm.prank(bob); + adapter.withdraw(0, bob, bob); + + address[] memory rewardTokens = IWithRewards(address(adapter)).rewardTokens(); + assertEq(rewardTokens[0], rewardsToken); + + assertGt(IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2).balanceOf(address(adapter)), 0); + } +} diff --git a/test/vault/integration/sushi/MasterChefTestConfigStorage.sol b/test/vault/integration/sushi/MasterChefTestConfigStorage.sol new file mode 100644 index 0000000..20f159e --- /dev/null +++ b/test/vault/integration/sushi/MasterChefTestConfigStorage.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.8.15; + +import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; + +struct MasterChefTestConfig { + uint256 pid; + address rewardsToken; +} + +contract MasterChefTestConfigStorage is ITestConfigStorage { + MasterChefTestConfig[] internal testConfigs; + + constructor() { + testConfigs.push(MasterChefTestConfig(2, 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2)); + } + + function getTestConfig(uint256 i) public view returns (bytes memory) { + return abi.encode(testConfigs[i].pid, testConfigs[i].rewardsToken); + } + + function getTestConfigLength() public view returns (uint256) { + return testConfigs.length; + } +} diff --git a/test/vault/integration/yearn/YearnTestConfigStorage.sol b/test/vault/integration/yearn/YearnTestConfigStorage.sol index edba8f6..3c29e97 100644 --- a/test/vault/integration/yearn/YearnTestConfigStorage.sol +++ b/test/vault/integration/yearn/YearnTestConfigStorage.sol @@ -18,6 +18,9 @@ contract YearnTestConfigStorage is ITestConfigStorage { // WETH // testConfigs.push(YearnTestConfig(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); + + // DAI + testConfigs.push(YearnTestConfig(0x6B175474E89094C44Da98b954EedeAC495271d0F)); } function getTestConfig(uint256 i) public view returns (bytes memory) { From 5b67d71f1126fad79406a16a8b8cb4c337419c91 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Fri, 31 Mar 2023 15:46:54 +0200 Subject: [PATCH 02/18] fixed issue 4 and 12 --- src/vault/Vault.sol | 1178 ++++++++++--------- src/vault/adapter/abstracts/AdapterBase.sol | 838 +++++++------ 2 files changed, 1101 insertions(+), 915 deletions(-) diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index 7d3d72c..7cfdb9f 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.15; -import { ERC4626Upgradeable, IERC20MetadataUpgradeable as IERC20Metadata, ERC20Upgradeable as ERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import { ReentrancyGuardUpgradeable } from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/security/PausableUpgradeable.sol"; -import { MathUpgradeable as Math } from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; -import { OwnedUpgradeable } from "../utils/OwnedUpgradeable.sol"; -import { VaultFees, IERC4626, IERC20 } from "../interfaces/vault/IVault.sol"; +import {ERC4626Upgradeable, IERC20MetadataUpgradeable as IERC20Metadata, ERC20Upgradeable as ERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/security/PausableUpgradeable.sol"; +import {MathUpgradeable as Math} from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import {OwnedUpgradeable} from "../utils/OwnedUpgradeable.sol"; +import {VaultFees, IERC4626, IERC20} from "../interfaces/vault/IVault.sol"; /** * @title Vault @@ -21,658 +21,784 @@ import { VaultFees, IERC4626, IERC20 } from "../interfaces/vault/IVault.sol"; * It allows for multiple type of fees which are taken by issuing new vault shares. * Adapter and fees can be changed by the owner after a ragequit time. */ -contract Vault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, OwnedUpgradeable { - using SafeERC20 for IERC20; - using Math for uint256; +contract Vault is + ERC4626Upgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + OwnedUpgradeable +{ + using SafeERC20 for IERC20; + using Math for uint256; - uint256 internal constant SECONDS_PER_YEAR = 365.25 days; + uint256 internal constant SECONDS_PER_YEAR = 365.25 days; - uint8 internal _decimals; - uint8 public constant decimalOffset = 9; + uint8 internal _decimals; + uint8 public constant decimalOffset = 9; - string internal _name; - string internal _symbol; + string internal _name; + string internal _symbol; - bytes32 public contractName; + bytes32 public contractName; - event VaultInitialized(bytes32 contractName, address indexed asset); + event VaultInitialized(bytes32 contractName, address indexed asset); - error InvalidAsset(); - error InvalidAdapter(); + error InvalidAsset(); + error InvalidAdapter(); - constructor() { - _disableInitializers(); - } - - /** - * @notice Initialize a new Vault. - * @param asset_ Underlying Asset which users will deposit. - * @param adapter_ Adapter which will be used to interact with the wrapped protocol. - * @param fees_ Desired fees in 1e18. (1e18 = 100%, 1e14 = 1 BPS) - * @param feeRecipient_ Recipient of all vault fees. (Must not be zero address) - * @param depositLimit_ Maximum amount of assets which can be deposited. - * @param owner Owner of the contract. Controls management functions. - * @dev This function is called by the factory contract when deploying a new vault. - * @dev Usually the adapter should already be pre configured. Otherwise a new one can only be added after a ragequit time. - */ - function initialize( - IERC20 asset_, - IERC4626 adapter_, - VaultFees calldata fees_, - address feeRecipient_, - uint256 depositLimit_, - address owner - ) external initializer { - __ERC4626_init(IERC20Metadata(address(asset_))); - __Owned_init(owner); - - if (address(asset_) == address(0)) revert InvalidAsset(); - if (address(asset_) != adapter_.asset()) revert InvalidAdapter(); - - adapter = adapter_; - - asset_.approve(address(adapter_), type(uint256).max); + constructor() { + _disableInitializers(); + } - _decimals = IERC20Metadata(address(asset_)).decimals() + decimalOffset; // Asset decimals + decimal offset to combat inflation attacks + /** + * @notice Initialize a new Vault. + * @param asset_ Underlying Asset which users will deposit. + * @param adapter_ Adapter which will be used to interact with the wrapped protocol. + * @param fees_ Desired fees in 1e18. (1e18 = 100%, 1e14 = 1 BPS) + * @param feeRecipient_ Recipient of all vault fees. (Must not be zero address) + * @param depositLimit_ Maximum amount of assets which can be deposited. + * @param owner Owner of the contract. Controls management functions. + * @dev This function is called by the factory contract when deploying a new vault. + * @dev Usually the adapter should already be pre configured. Otherwise a new one can only be added after a ragequit time. + */ + function initialize( + IERC20 asset_, + IERC4626 adapter_, + VaultFees calldata fees_, + address feeRecipient_, + uint256 depositLimit_, + address owner + ) external initializer { + __ERC4626_init(IERC20Metadata(address(asset_))); + __Owned_init(owner); + + if (address(asset_) == address(0)) revert InvalidAsset(); + if (address(asset_) != adapter_.asset()) revert InvalidAdapter(); + + adapter = adapter_; + + asset_.approve(address(adapter_), type(uint256).max); + + _decimals = IERC20Metadata(address(asset_)).decimals() + decimalOffset; // Asset decimals + decimal offset to combat inflation attacks + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + + if ( + fees_.deposit >= 1e18 || + fees_.withdrawal >= 1e18 || + fees_.management >= 1e18 || + fees_.performance >= 1e18 + ) revert InvalidVaultFees(); + fees = fees_; + + if (feeRecipient_ == address(0)) revert InvalidFeeRecipient(); + feeRecipient = feeRecipient_; + + contractName = keccak256( + abi.encodePacked("Popcorn", name(), block.timestamp, "Vault") + ); + + feesUpdatedAt = block.timestamp; + highWaterMark = 1e9; + quitPeriod = 3 days; + depositLimit = depositLimit_; + + emit VaultInitialized(contractName, address(asset_)); + + _name = string.concat( + "Popcorn ", + IERC20Metadata(address(asset_)).name(), + " Vault" + ); + _symbol = string.concat( + "pop-", + IERC20Metadata(address(asset_)).symbol() + ); + } - INITIAL_CHAIN_ID = block.chainid; - INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } - if (fees_.deposit >= 1e18 || fees_.withdrawal >= 1e18 || fees_.management >= 1e18 || fees_.performance >= 1e18) - revert InvalidVaultFees(); - fees = fees_; + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } - if (feeRecipient_ == address(0)) revert InvalidFeeRecipient(); - feeRecipient = feeRecipient_; + function decimals() + public + view + override(IERC20Metadata, ERC20) + returns (uint8) + { + return _decimals; + } - contractName = keccak256(abi.encodePacked("Popcorn", name(), block.timestamp, "Vault")); + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LOGIC + //////////////////////////////////////////////////////////////*/ - feesUpdatedAt = block.timestamp; - highWaterMark = 1e9; - quitPeriod = 3 days; - depositLimit = depositLimit_; + error ZeroAmount(); + error InvalidReceiver(); + error MaxError(uint256 amount); - emit VaultInitialized(contractName, address(asset_)); + function deposit(uint256 assets) public returns (uint256) { + return deposit(assets, msg.sender); + } - _name = string.concat("Popcorn ", IERC20Metadata(address(asset_)).name(), " Vault"); - _symbol = string.concat("pop-", IERC20Metadata(address(asset_)).symbol()); - } + /** + * @notice Deposit exactly `assets` amount of tokens, issuing vault shares to `receiver`. + * @param assets Quantity of tokens to deposit. + * @param receiver Receiver of issued vault shares. + * @return shares Quantity of vault shares issued to `receiver`. + */ + function deposit( + uint256 assets, + address receiver + ) public override nonReentrant whenNotPaused returns (uint256 shares) { + if (receiver == address(0)) revert InvalidReceiver(); + if (assets > maxDeposit(receiver)) revert MaxError(assets); - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } + uint256 feeShares = _convertToShares( + assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), + Math.Rounding.Down + ); - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } + shares = _convertToShares(assets, Math.Rounding.Down) - feeShares; + if (shares == 0) revert ZeroAmount(); - function decimals() public view override(IERC20Metadata, ERC20) returns (uint8) { - return _decimals; - } + if (feeShares > 0) _mint(feeRecipient, feeShares); - /*////////////////////////////////////////////////////////////// - DEPOSIT/WITHDRAWAL LOGIC - //////////////////////////////////////////////////////////////*/ + _mint(receiver, shares); - error ZeroAmount(); - error InvalidReceiver(); - error MaxError(uint256 amount); + IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); - function deposit(uint256 assets) public returns (uint256) { - return deposit(assets, msg.sender); - } + adapter.deposit(assets, address(this)); - /** - * @notice Deposit exactly `assets` amount of tokens, issuing vault shares to `receiver`. - * @param assets Quantity of tokens to deposit. - * @param receiver Receiver of issued vault shares. - * @return shares Quantity of vault shares issued to `receiver`. - */ - function deposit( - uint256 assets, - address receiver - ) public override nonReentrant whenNotPaused returns (uint256 shares) { - if (receiver == address(0)) revert InvalidReceiver(); - if (assets > maxDeposit(receiver)) revert MaxError(assets); + emit Deposit(msg.sender, receiver, assets, shares); + } - uint256 feeShares = _convertToShares( - assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), - Math.Rounding.Down - ); + function mint(uint256 shares) external returns (uint256) { + return mint(shares, msg.sender); + } - shares = _convertToShares(assets, Math.Rounding.Down) - feeShares; - if (shares == 0) revert ZeroAmount(); + /** + * @notice Mint exactly `shares` vault shares to `receiver`, taking the necessary amount of `asset` from the caller. + * @param shares Quantity of shares to mint. + * @param receiver Receiver of issued vault shares. + * @return assets Quantity of assets deposited by caller. + */ + function mint( + uint256 shares, + address receiver + ) public override nonReentrant whenNotPaused returns (uint256 assets) { + if (receiver == address(0)) revert InvalidReceiver(); + if (shares == 0) revert ZeroAmount(); - if (feeShares > 0) _mint(feeRecipient, feeShares); + uint256 depositFee = uint256(fees.deposit); - _mint(receiver, shares); + uint256 feeShares = shares.mulDiv( + depositFee, + 1e18 - depositFee, + Math.Rounding.Down + ); - IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); + assets = _convertToAssets(shares + feeShares, Math.Rounding.Up); - adapter.deposit(assets, address(this)); + if (assets > maxMint(receiver)) revert MaxError(assets); - emit Deposit(msg.sender, receiver, assets, shares); - } + if (feeShares > 0) _mint(feeRecipient, feeShares); - function mint(uint256 shares) external returns (uint256) { - return mint(shares, msg.sender); - } + _mint(receiver, shares); - /** - * @notice Mint exactly `shares` vault shares to `receiver`, taking the necessary amount of `asset` from the caller. - * @param shares Quantity of shares to mint. - * @param receiver Receiver of issued vault shares. - * @return assets Quantity of assets deposited by caller. - */ - function mint(uint256 shares, address receiver) public override nonReentrant whenNotPaused returns (uint256 assets) { - if (receiver == address(0)) revert InvalidReceiver(); - if (shares == 0) revert ZeroAmount(); + IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); - uint256 depositFee = uint256(fees.deposit); + adapter.deposit(assets, address(this)); - uint256 feeShares = shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Down); + emit Deposit(msg.sender, receiver, assets, shares); + } - assets = _convertToAssets(shares + feeShares, Math.Rounding.Up); + function withdraw(uint256 assets) public returns (uint256) { + return withdraw(assets, msg.sender, msg.sender); + } - if (assets > maxMint(receiver)) revert MaxError(assets); + /** + * @notice Burn shares from `owner` in exchange for `assets` amount of underlying token. + * @param assets Quantity of underlying `asset` token to withdraw. + * @param receiver Receiver of underlying token. + * @param owner Owner of burned vault shares. + * @return shares Quantity of vault shares burned in exchange for `assets`. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override nonReentrant returns (uint256 shares) { + if (receiver == address(0)) revert InvalidReceiver(); + if (assets > maxWithdraw(owner)) revert MaxError(assets); - if (feeShares > 0) _mint(feeRecipient, feeShares); + shares = _convertToShares(assets, Math.Rounding.Up); + if (shares == 0) revert ZeroAmount(); - _mint(receiver, shares); + uint256 withdrawalFee = uint256(fees.withdrawal); - IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); + uint256 feeShares = shares.mulDiv( + withdrawalFee, + 1e18 - withdrawalFee, + Math.Rounding.Down + ); - adapter.deposit(assets, address(this)); + shares += feeShares; - emit Deposit(msg.sender, receiver, assets, shares); - } + if (msg.sender != owner) + _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); - function withdraw(uint256 assets) public returns (uint256) { - return withdraw(assets, msg.sender, msg.sender); - } + _burn(owner, shares); - /** - * @notice Burn shares from `owner` in exchange for `assets` amount of underlying token. - * @param assets Quantity of underlying `asset` token to withdraw. - * @param receiver Receiver of underlying token. - * @param owner Owner of burned vault shares. - * @return shares Quantity of vault shares burned in exchange for `assets`. - */ - function withdraw( - uint256 assets, - address receiver, - address owner - ) public override nonReentrant returns (uint256 shares) { - if (receiver == address(0)) revert InvalidReceiver(); - if (assets > maxWithdraw(owner)) revert MaxError(assets); + if (feeShares > 0) _mint(feeRecipient, feeShares); - shares = _convertToShares(assets, Math.Rounding.Up); - if (shares == 0) revert ZeroAmount(); + adapter.withdraw(assets, receiver, address(this)); - uint256 withdrawalFee = uint256(fees.withdrawal); + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } - uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); + function redeem(uint256 shares) external returns (uint256) { + return redeem(shares, msg.sender, msg.sender); + } - shares += feeShares; + /** + * @notice Burn exactly `shares` vault shares from `owner` and send underlying `asset` tokens to `receiver`. + * @param shares Quantity of vault shares to exchange for underlying tokens. + * @param receiver Receiver of underlying tokens. + * @param owner Owner of burned vault shares. + * @return assets Quantity of `asset` sent to `receiver`. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) public override nonReentrant returns (uint256 assets) { + if (receiver == address(0)) revert InvalidReceiver(); + if (shares == 0) revert ZeroAmount(); + if (shares > maxRedeem(owner)) revert MaxError(shares); - if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + if (msg.sender != owner) + _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); - _burn(owner, shares); + uint256 feeShares = shares.mulDiv( + uint256(fees.withdrawal), + 1e18, + Math.Rounding.Down + ); - if (feeShares > 0) _mint(feeRecipient, feeShares); + assets = _convertToAssets(shares - feeShares, Math.Rounding.Up); - adapter.withdraw(assets, receiver, address(this)); + _burn(owner, shares); - emit Withdraw(msg.sender, receiver, owner, assets, shares); - } + if (feeShares > 0) _mint(feeRecipient, feeShares); - function redeem(uint256 shares) external returns (uint256) { - return redeem(shares, msg.sender, msg.sender); - } + adapter.withdraw(assets, receiver, address(this)); - /** - * @notice Burn exactly `shares` vault shares from `owner` and send underlying `asset` tokens to `receiver`. - * @param shares Quantity of vault shares to exchange for underlying tokens. - * @param receiver Receiver of underlying tokens. - * @param owner Owner of burned vault shares. - * @return assets Quantity of `asset` sent to `receiver`. - */ - function redeem( - uint256 shares, - address receiver, - address owner - ) public override nonReentrant returns (uint256 assets) { - if (receiver == address(0)) revert InvalidReceiver(); - if (shares == 0) revert ZeroAmount(); - if (shares > maxRedeem(owner)) revert MaxError(shares); + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } - if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ - uint256 feeShares = shares.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); + /// @return Total amount of underlying `asset` token managed by vault. Delegates to adapter. + function totalAssets() public view override returns (uint256) { + return adapter.convertToAssets(adapter.balanceOf(address(this))); + } - assets = _convertToAssets(shares - feeShares, Math.Rounding.Up); + /** + * @notice Simulate the effects of a deposit at the current block, given current on-chain conditions. + * @param assets Exact amount of underlying `asset` token to deposit + * @return shares of the vault issued in exchange to the user for `assets` + * @dev This method accounts for issuance of accrued fee shares. + */ + function previewDeposit( + uint256 assets + ) public view override returns (uint256 shares) { + shares = adapter.previewDeposit( + assets - + assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down) + ); + } - _burn(owner, shares); + /** + * @notice Simulate the effects of a mint at the current block, given current on-chain conditions. + * @param shares Exact amount of vault shares to mint. + * @return assets quantity of underlying needed in exchange to mint `shares`. + * @dev This method accounts for issuance of accrued fee shares. + */ + function previewMint( + uint256 shares + ) public view override returns (uint256 assets) { + uint256 depositFee = uint256(fees.deposit); + + shares += shares.mulDiv( + depositFee, + 1e18 - depositFee, + Math.Rounding.Up + ); + + assets = adapter.previewMint(shares); + } - if (feeShares > 0) _mint(feeRecipient, feeShares); + /** + * @notice Simulate the effects of a withdrawal at the current block, given current on-chain conditions. + * @param assets Exact amount of `assets` to withdraw + * @return shares to be burned in exchange for `assets` + * @dev This method accounts for both issuance of fee shares and withdrawal fee. + */ + function previewWithdraw( + uint256 assets + ) public view override returns (uint256 shares) { + uint256 withdrawalFee = uint256(fees.withdrawal); + + assets += assets.mulDiv( + withdrawalFee, + 1e18 - withdrawalFee, + Math.Rounding.Up + ); + + shares = adapter.previewWithdraw(assets); + } - adapter.withdraw(assets, receiver, address(this)); + /** + * @notice Simulate the effects of a redemption at the current block, given current on-chain conditions. + * @param shares Exact amount of `shares` to redeem + * @return assets quantity of underlying returned in exchange for `shares`. + * @dev This method accounts for both issuance of fee shares and withdrawal fee. + */ + function previewRedeem( + uint256 shares + ) public view override returns (uint256 assets) { + assets = adapter.previewRedeem(shares); + + assets -= assets.mulDiv( + uint256(fees.withdrawal), + 1e18, + Math.Rounding.Down + ); + } - emit Withdraw(msg.sender, receiver, owner, assets, shares); - } + function _convertToShares( + uint256 assets, + Math.Rounding rounding + ) internal view virtual override returns (uint256 shares) { + return + assets.mulDiv( + totalSupply() + 10 ** decimalOffset, + totalAssets() + 1, + rounding + ); + } - /*////////////////////////////////////////////////////////////// - ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ + function _convertToAssets( + uint256 shares, + Math.Rounding rounding + ) internal view virtual override returns (uint256) { + return + shares.mulDiv( + totalAssets() + 1, + totalSupply() + 10 ** decimalOffset, + rounding + ); + } - /// @return Total amount of underlying `asset` token managed by vault. Delegates to adapter. - function totalAssets() public view override returns (uint256) { - return adapter.convertToAssets(adapter.balanceOf(address(this))); - } - - /** - * @notice Simulate the effects of a deposit at the current block, given current on-chain conditions. - * @param assets Exact amount of underlying `asset` token to deposit - * @return shares of the vault issued in exchange to the user for `assets` - * @dev This method accounts for issuance of accrued fee shares. - */ - function previewDeposit(uint256 assets) public view override returns (uint256 shares) { - shares = adapter.previewDeposit(assets - assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down)); - } - - /** - * @notice Simulate the effects of a mint at the current block, given current on-chain conditions. - * @param shares Exact amount of vault shares to mint. - * @return assets quantity of underlying needed in exchange to mint `shares`. - * @dev This method accounts for issuance of accrued fee shares. - */ - function previewMint(uint256 shares) public view override returns (uint256 assets) { - uint256 depositFee = uint256(fees.deposit); - - shares += shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Up); - - assets = adapter.previewMint(shares); - } - - /** - * @notice Simulate the effects of a withdrawal at the current block, given current on-chain conditions. - * @param assets Exact amount of `assets` to withdraw - * @return shares to be burned in exchange for `assets` - * @dev This method accounts for both issuance of fee shares and withdrawal fee. - */ - function previewWithdraw(uint256 assets) public view override returns (uint256 shares) { - uint256 withdrawalFee = uint256(fees.withdrawal); - - assets += assets.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); - - shares = adapter.previewWithdraw(assets); - } - - /** - * @notice Simulate the effects of a redemption at the current block, given current on-chain conditions. - * @param shares Exact amount of `shares` to redeem - * @return assets quantity of underlying returned in exchange for `shares`. - * @dev This method accounts for both issuance of fee shares and withdrawal fee. - */ - function previewRedeem(uint256 shares) public view override returns (uint256 assets) { - assets = adapter.previewRedeem(shares); - - assets -= assets.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); - } - - function _convertToShares( - uint256 assets, - Math.Rounding rounding - ) internal view virtual override returns (uint256 shares) { - return assets.mulDiv(totalSupply() + 10 ** decimalOffset, totalAssets() + 1, rounding); - } - - function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) { - return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** decimalOffset, rounding); - } - - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// DEPOSIT/WITHDRAWAL LIMIT LOGIC //////////////////////////////////////////////////////////////*/ - /// @return Maximum amount of underlying `asset` token that may be deposited for a given address. Delegates to adapter. - function maxDeposit(address) public view override returns (uint256) { - uint256 assets = totalAssets(); - uint256 depositLimit_ = depositLimit; - if (paused() || assets >= depositLimit_) return 0; - return Math.min(depositLimit_ - assets, adapter.maxDeposit(address(this))); - } - - /// @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. - function maxMint(address) public view override returns (uint256) { - uint256 assets = totalAssets(); - uint256 depositLimit_ = depositLimit; - if (paused() || assets >= depositLimit_) return 0; - return Math.min(depositLimit_ - assets, adapter.maxMint(address(this))); - } - - /// @return Maximum amount of underlying `asset` token that can be withdrawn by `caller` address. Delegates to adapter. - function maxWithdraw(address) public view override returns (uint256) { - return adapter.maxWithdraw(address(this)); - } - - /// @return Maximum amount of shares that may be redeemed by `caller` address. Delegates to adapter. - function maxRedeem(address) public view override returns (uint256) { - return adapter.maxRedeem(address(this)); - } - - /*////////////////////////////////////////////////////////////// - FEE ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Management fee that has accrued since last fee harvest. - * @return Accrued management fee in underlying `asset` token. - * @dev Management fee is annualized per minute, based on 525,600 minutes per year. Total assets are calculated using - * the average of their current value and the value at the previous fee harvest checkpoint. This method is similar to - * calculating a definite integral using the trapezoid rule. - */ - function accruedManagementFee() public view returns (uint256) { - uint256 managementFee = fees.management; - return - managementFee > 0 - ? managementFee.mulDiv( - totalAssets() * (block.timestamp - feesUpdatedAt), - SECONDS_PER_YEAR, - Math.Rounding.Down - ) / 1e18 - : 0; - } - - /** - * @notice Performance fee that has accrued since last fee harvest. - * @return Accrued performance fee in underlying `asset` token. - * @dev Performance fee is based on a high water mark value. If vault share value has increased above the - * HWM in a fee period, issue fee shares to the vault equal to the performance fee. - */ - function accruedPerformanceFee() public view returns (uint256) { - uint256 highWaterMark_ = highWaterMark; - uint256 shareValue = convertToAssets(1e18); - uint256 performanceFee = fees.performance; - - return - performanceFee > 0 && shareValue > highWaterMark_ - ? performanceFee.mulDiv((shareValue - highWaterMark_) * totalSupply(), 1e36, Math.Rounding.Down) - : 0; - } - - /*////////////////////////////////////////////////////////////// - FEE LOGIC - //////////////////////////////////////////////////////////////*/ + /// @return Maximum amount of underlying `asset` token that may be deposited for a given address. Delegates to adapter. + function maxDeposit(address) public view override returns (uint256) { + uint256 assets = totalAssets(); + uint256 depositLimit_ = depositLimit; + if (paused() || assets >= depositLimit_) return 0; + return + Math.min(depositLimit_ - assets, adapter.maxDeposit(address(this))); + } - uint256 public highWaterMark; - uint256 public assetsCheckpoint; - uint256 public feesUpdatedAt; + /// @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. + function maxMint(address) public view override returns (uint256) { + uint256 assets = totalAssets(); + uint256 depositLimit_ = depositLimit; + if (paused() || assets >= depositLimit_) return 0; + return Math.min(depositLimit_ - assets, adapter.maxMint(address(this))); + } - error InsufficientWithdrawalAmount(uint256 amount); + /// @return Maximum amount of underlying `asset` token that can be withdrawn by `caller` address. Delegates to adapter. + function maxWithdraw(address) public view override returns (uint256) { + return adapter.maxWithdraw(address(this)); + } - /// @notice Minimal function to call `takeFees` modifier. - function takeManagementAndPerformanceFees() external nonReentrant takeFees {} + /// @return Maximum amount of shares that may be redeemed by `caller` address. Delegates to adapter. + function maxRedeem(address) public view override returns (uint256) { + return adapter.maxRedeem(address(this)); + } - /// @notice Collect management and performance fees and update vault share high water mark. - modifier takeFees() { - uint256 managementFee = accruedManagementFee(); - uint256 totalFee = managementFee + accruedPerformanceFee(); - uint256 currentAssets = totalAssets(); - uint256 shareValue = convertToAssets(1e18); + /*////////////////////////////////////////////////////////////// + FEE ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ - if (shareValue > highWaterMark) highWaterMark = shareValue; + /** + * @notice Management fee that has accrued since last fee harvest. + * @return Accrued management fee in underlying `asset` token. + * @dev Management fee is annualized per minute, based on 525,600 minutes per year. Total assets are calculated using + * the average of their current value and the value at the previous fee harvest checkpoint. This method is similar to + * calculating a definite integral using the trapezoid rule. + */ + function accruedManagementFee() public view returns (uint256) { + uint256 managementFee = fees.management; + return + managementFee > 0 + ? managementFee.mulDiv( + totalAssets() * (block.timestamp - feesUpdatedAt), + SECONDS_PER_YEAR, + Math.Rounding.Down + ) / 1e18 + : 0; + } - if (totalFee > 0 && currentAssets > 0) { - uint256 supply = totalSupply(); - uint256 feeInShare = supply == 0 - ? totalFee - : totalFee.mulDiv(supply, currentAssets - totalFee, Math.Rounding.Down); - _mint(feeRecipient, feeInShare); + /** + * @notice Performance fee that has accrued since last fee harvest. + * @return Accrued performance fee in underlying `asset` token. + * @dev Performance fee is based on a high water mark value. If vault share value has increased above the + * HWM in a fee period, issue fee shares to the vault equal to the performance fee. + */ + function accruedPerformanceFee() public view returns (uint256) { + uint256 highWaterMark_ = highWaterMark; + uint256 shareValue = convertToAssets(1e18); + uint256 performanceFee = fees.performance; + + return + performanceFee > 0 && shareValue > highWaterMark_ + ? performanceFee.mulDiv( + (shareValue - highWaterMark_) * totalSupply(), + 1e36, + Math.Rounding.Down + ) + : 0; } - feesUpdatedAt = block.timestamp; + /*////////////////////////////////////////////////////////////// + FEE LOGIC + //////////////////////////////////////////////////////////////*/ - _; - } + uint256 public highWaterMark; + uint256 public assetsCheckpoint; + uint256 public feesUpdatedAt; + + error InsufficientWithdrawalAmount(uint256 amount); + + /// @notice Minimal function to call `takeFees` modifier. + function takeManagementAndPerformanceFees() + external + nonReentrant + takeFees + {} + + /// @notice Collect management and performance fees and update vault share high water mark. + modifier takeFees() { + uint256 managementFee = accruedManagementFee(); + uint256 totalFee = managementFee + accruedPerformanceFee(); + uint256 currentAssets = totalAssets(); + uint256 shareValue = convertToAssets(1e18); + + if (shareValue > highWaterMark) highWaterMark = shareValue; + + if (totalFee > 0 && currentAssets > 0) { + uint256 supply = totalSupply(); + uint256 feeInShare = supply == 0 + ? totalFee + : totalFee.mulDiv( + supply, + currentAssets - totalFee, + Math.Rounding.Down + ); + _mint(feeRecipient, feeInShare); + } + + feesUpdatedAt = block.timestamp; + + _; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// FEE MANAGEMENT LOGIC //////////////////////////////////////////////////////////////*/ - VaultFees public fees; + VaultFees public fees; - VaultFees public proposedFees; - uint256 public proposedFeeTime; + VaultFees public proposedFees; + uint256 public proposedFeeTime; - address public feeRecipient; + address public feeRecipient; - event NewFeesProposed(VaultFees newFees, uint256 timestamp); - event ChangedFees(VaultFees oldFees, VaultFees newFees); - event FeeRecipientUpdated(address oldFeeRecipient, address newFeeRecipient); + event NewFeesProposed(VaultFees newFees, uint256 timestamp); + event ChangedFees(VaultFees oldFees, VaultFees newFees); + event FeeRecipientUpdated(address oldFeeRecipient, address newFeeRecipient); - error InvalidVaultFees(); - error InvalidFeeRecipient(); - error NotPassedQuitPeriod(uint256 quitPeriod); + error InvalidVaultFees(); + error InvalidFeeRecipient(); + error NotPassedQuitPeriod(uint256 quitPeriod); - /** - * @notice Propose new fees for this vault. Caller must be owner. - * @param newFees Fees for depositing, withdrawal, management and performance in 1e18. - * @dev Fees can be 0 but never 1e18 (1e18 = 100%, 1e14 = 1 BPS) - */ - function proposeFees(VaultFees calldata newFees) external onlyOwner { - if ( - newFees.deposit >= 1e18 || newFees.withdrawal >= 1e18 || newFees.management >= 1e18 || newFees.performance >= 1e18 - ) revert InvalidVaultFees(); + /** + * @notice Propose new fees for this vault. Caller must be owner. + * @param newFees Fees for depositing, withdrawal, management and performance in 1e18. + * @dev Fees can be 0 but never 1e18 (1e18 = 100%, 1e14 = 1 BPS) + */ + function proposeFees(VaultFees calldata newFees) external onlyOwner { + if ( + newFees.deposit >= 1e18 || + newFees.withdrawal >= 1e18 || + newFees.management >= 1e18 || + newFees.performance >= 1e18 + ) revert InvalidVaultFees(); - proposedFees = newFees; - proposedFeeTime = block.timestamp; + proposedFees = newFees; + proposedFeeTime = block.timestamp; - emit NewFeesProposed(newFees, block.timestamp); - } + emit NewFeesProposed(newFees, block.timestamp); + } - /// @notice Change fees to the previously proposed fees after the quit period has passed. - function changeFees() external { - if (proposedFeeTime == 0 || block.timestamp < proposedFeeTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); + /// @notice Change fees to the previously proposed fees after the quit period has passed. + function changeFees() external takeFees { + if ( + proposedFeeTime == 0 || + block.timestamp < proposedFeeTime + quitPeriod + ) revert NotPassedQuitPeriod(quitPeriod); - emit ChangedFees(fees, proposedFees); + emit ChangedFees(fees, proposedFees); - fees = proposedFees; - feesUpdatedAt = block.timestamp; + fees = proposedFees; + feesUpdatedAt = block.timestamp; - delete proposedFees; - delete proposedFeeTime; - } + delete proposedFees; + delete proposedFeeTime; + } - /** - * @notice Change `feeRecipient`. Caller must be Owner. - * @param _feeRecipient The new fee recipient. - * @dev Accrued fees wont be transferred to the new feeRecipient. - */ - function setFeeRecipient(address _feeRecipient) external onlyOwner { - if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); + /** + * @notice Change `feeRecipient`. Caller must be Owner. + * @param _feeRecipient The new fee recipient. + * @dev Accrued fees wont be transferred to the new feeRecipient. + */ + function setFeeRecipient(address _feeRecipient) external onlyOwner { + if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); - emit FeeRecipientUpdated(feeRecipient, _feeRecipient); + emit FeeRecipientUpdated(feeRecipient, _feeRecipient); - feeRecipient = _feeRecipient; - } + feeRecipient = _feeRecipient; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ADAPTER LOGIC //////////////////////////////////////////////////////////////*/ - IERC4626 public adapter; - IERC4626 public proposedAdapter; - uint256 public proposedAdapterTime; + IERC4626 public adapter; + IERC4626 public proposedAdapter; + uint256 public proposedAdapterTime; - event NewAdapterProposed(IERC4626 newAdapter, uint256 timestamp); - event ChangedAdapter(IERC4626 oldAdapter, IERC4626 newAdapter); + event NewAdapterProposed(IERC4626 newAdapter, uint256 timestamp); + event ChangedAdapter(IERC4626 oldAdapter, IERC4626 newAdapter); - error VaultAssetMismatchNewAdapterAsset(); + error VaultAssetMismatchNewAdapterAsset(); - /** - * @notice Propose a new adapter for this vault. Caller must be Owner. - * @param newAdapter A new ERC4626 that should be used as a yield adapter for this asset. - */ - function proposeAdapter(IERC4626 newAdapter) external onlyOwner { - if (newAdapter.asset() != asset()) revert VaultAssetMismatchNewAdapterAsset(); + /** + * @notice Propose a new adapter for this vault. Caller must be Owner. + * @param newAdapter A new ERC4626 that should be used as a yield adapter for this asset. + */ + function proposeAdapter(IERC4626 newAdapter) external onlyOwner { + if (newAdapter.asset() != asset()) + revert VaultAssetMismatchNewAdapterAsset(); - proposedAdapter = newAdapter; - proposedAdapterTime = block.timestamp; + proposedAdapter = newAdapter; + proposedAdapterTime = block.timestamp; - emit NewAdapterProposed(newAdapter, block.timestamp); - } + emit NewAdapterProposed(newAdapter, block.timestamp); + } - /** - * @notice Set a new Adapter for this Vault after the quit period has passed. - * @dev This migration function will remove all assets from the old Vault and move them into the new vault - * @dev Additionally it will zero old allowances and set new ones - * @dev Last we update HWM and assetsCheckpoint for fees to make sure they adjust to the new adapter - */ - function changeAdapter() external takeFees { - if (proposedAdapterTime == 0 || block.timestamp < proposedAdapterTime + quitPeriod) - revert NotPassedQuitPeriod(quitPeriod); + /** + * @notice Set a new Adapter for this Vault after the quit period has passed. + * @dev This migration function will remove all assets from the old Vault and move them into the new vault + * @dev Additionally it will zero old allowances and set new ones + * @dev Last we update HWM and assetsCheckpoint for fees to make sure they adjust to the new adapter + */ + function changeAdapter() external takeFees { + if ( + proposedAdapterTime == 0 || + block.timestamp < proposedAdapterTime + quitPeriod + ) revert NotPassedQuitPeriod(quitPeriod); - adapter.redeem(adapter.balanceOf(address(this)), address(this), address(this)); + adapter.redeem( + adapter.balanceOf(address(this)), + address(this), + address(this) + ); - IERC20(asset()).approve(address(adapter), 0); + IERC20(asset()).approve(address(adapter), 0); - emit ChangedAdapter(adapter, proposedAdapter); + emit ChangedAdapter(adapter, proposedAdapter); - adapter = proposedAdapter; + adapter = proposedAdapter; - IERC20(asset()).approve(address(adapter), type(uint256).max); + IERC20(asset()).approve(address(adapter), type(uint256).max); - adapter.deposit(IERC20(asset()).balanceOf(address(this)), address(this)); + adapter.deposit( + IERC20(asset()).balanceOf(address(this)), + address(this) + ); - delete proposedAdapterTime; - delete proposedAdapter; - } + delete proposedAdapterTime; + delete proposedAdapter; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// RAGE QUIT LOGIC //////////////////////////////////////////////////////////////*/ - uint256 public quitPeriod; + uint256 public quitPeriod; - event QuitPeriodSet(uint256 quitPeriod); + event QuitPeriodSet(uint256 quitPeriod); - error InvalidQuitPeriod(); + error InvalidQuitPeriod(); - /** - * @notice Set a quitPeriod for rage quitting after new adapter or fees are proposed. Caller must be Owner. - * @param _quitPeriod Time to rage quit after proposal. - */ - function setQuitPeriod(uint256 _quitPeriod) external onlyOwner { - if (block.timestamp < proposedAdapterTime + quitPeriod || block.timestamp < proposedFeeTime + quitPeriod) - revert NotPassedQuitPeriod(quitPeriod); - if (_quitPeriod < 1 days || _quitPeriod > 7 days) revert InvalidQuitPeriod(); + /** + * @notice Set a quitPeriod for rage quitting after new adapter or fees are proposed. Caller must be Owner. + * @param _quitPeriod Time to rage quit after proposal. + */ + function setQuitPeriod(uint256 _quitPeriod) external onlyOwner { + if ( + block.timestamp < proposedAdapterTime + quitPeriod || + block.timestamp < proposedFeeTime + quitPeriod + ) revert NotPassedQuitPeriod(quitPeriod); + if (_quitPeriod < 1 days || _quitPeriod > 7 days) + revert InvalidQuitPeriod(); - quitPeriod = _quitPeriod; + quitPeriod = _quitPeriod; - emit QuitPeriodSet(quitPeriod); - } + emit QuitPeriodSet(quitPeriod); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// DEPOSIT LIMIT LOGIC //////////////////////////////////////////////////////////////*/ - uint256 public depositLimit; + uint256 public depositLimit; - event DepositLimitSet(uint256 depositLimit); + event DepositLimitSet(uint256 depositLimit); - /** - * @notice Sets a limit for deposits in assets. Caller must be Owner. - * @param _depositLimit Maximum amount of assets that can be deposited. - */ - function setDepositLimit(uint256 _depositLimit) external onlyOwner { - depositLimit = _depositLimit; + /** + * @notice Sets a limit for deposits in assets. Caller must be Owner. + * @param _depositLimit Maximum amount of assets that can be deposited. + */ + function setDepositLimit(uint256 _depositLimit) external onlyOwner { + depositLimit = _depositLimit; - emit DepositLimitSet(_depositLimit); - } + emit DepositLimitSet(_depositLimit); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// PAUSING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Pause deposits. Caller must be Owner. - function pause() external onlyOwner { - _pause(); - } + /// @notice Pause deposits. Caller must be Owner. + function pause() external onlyOwner { + _pause(); + } - /// @notice Unpause deposits. Caller must be Owner. - function unpause() external onlyOwner { - _unpause(); - } + /// @notice Unpause deposits. Caller must be Owner. + function unpause() external onlyOwner { + _unpause(); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-2612 LOGIC //////////////////////////////////////////////////////////////*/ - // EIP-2612 STORAGE - uint256 internal INITIAL_CHAIN_ID; - bytes32 internal INITIAL_DOMAIN_SEPARATOR; - mapping(address => uint256) public nonces; - - error PermitDeadlineExpired(uint256 deadline); - error InvalidSigner(address signer); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); - - // Unchecked because the only math done is incrementing - // the owner's nonce which cannot realistically overflow. - unchecked { - address recoveredAddress = ecrecover( - keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), + // EIP-2612 STORAGE + uint256 internal INITIAL_CHAIN_ID; + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + mapping(address => uint256) public nonces; + + error PermitDeadlineExpired(uint256 deadline); + error InvalidSigner(address signer); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + if (recoveredAddress == address(0) || recoveredAddress != owner) + revert InvalidSigner(recoveredAddress); + + _approve(recoveredAddress, spender, value); + } + } + + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return + block.chainid == INITIAL_CHAIN_ID + ? INITIAL_DOMAIN_SEPARATOR + : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - spender, - value, - nonces[owner]++, - deadline - ) - ) - ) - ), - v, - r, - s - ); - - if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress); - - _approve(recoveredAddress, spender, value); - } - } - - function DOMAIN_SEPARATOR() public view returns (bytes32) { - return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); - } - - function computeDomainSeparator() internal view virtual returns (bytes32) { - return - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name())), - keccak256("1"), - block.chainid, - address(this) - ) - ); - } + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(name())), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } } diff --git a/src/vault/adapter/abstracts/AdapterBase.sol b/src/vault/adapter/abstracts/AdapterBase.sol index 81f4ecf..5afc660 100644 --- a/src/vault/adapter/abstracts/AdapterBase.sol +++ b/src/vault/adapter/abstracts/AdapterBase.sol @@ -3,16 +3,16 @@ pragma solidity ^0.8.15; -import { ERC4626Upgradeable, IERC20Upgradeable as IERC20, IERC20MetadataUpgradeable as IERC20Metadata, ERC20Upgradeable as ERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import { ReentrancyGuardUpgradeable } from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import { MathUpgradeable as Math } from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; -import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/security/PausableUpgradeable.sol"; -import { IStrategy } from "../../../interfaces/vault/IStrategy.sol"; -import { IAdapter } from "../../../interfaces/vault/IAdapter.sol"; -import { EIP165 } from "../../../utils/EIP165.sol"; -import { OnlyStrategy } from "./OnlyStrategy.sol"; -import { OwnedUpgradeable } from "../../../utils/OwnedUpgradeable.sol"; +import {ERC4626Upgradeable, IERC20Upgradeable as IERC20, IERC20MetadataUpgradeable as IERC20Metadata, ERC20Upgradeable as ERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {MathUpgradeable as Math} from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/security/PausableUpgradeable.sol"; +import {IStrategy} from "../../../interfaces/vault/IStrategy.sol"; +import {IAdapter} from "../../../interfaces/vault/IAdapter.sol"; +import {EIP165} from "../../../utils/EIP165.sol"; +import {OnlyStrategy} from "./OnlyStrategy.sol"; +import {OwnedUpgradeable} from "../../../utils/OwnedUpgradeable.sol"; /** * @title AdapterBase @@ -25,442 +25,502 @@ import { OwnedUpgradeable } from "../../../utils/OwnedUpgradeable.sol"; * The adapter can be initialized with a strategy that can perform additional operations. (Leverage, Compounding, etc.) */ abstract contract AdapterBase is - ERC4626Upgradeable, - PausableUpgradeable, - OwnedUpgradeable, - ReentrancyGuardUpgradeable, - EIP165, - OnlyStrategy + ERC4626Upgradeable, + PausableUpgradeable, + OwnedUpgradeable, + ReentrancyGuardUpgradeable, + EIP165, + OnlyStrategy { - using SafeERC20 for IERC20; - using Math for uint256; - - uint8 internal _decimals; - uint8 public constant decimalOffset = 9; - - error StrategySetupFailed(); - - constructor() { - _disableInitializers(); - } - - /** - * @notice Initialize a new Adapter. - * @param popERC4626InitData Encoded data for the base adapter initialization. - * @dev `asset` - The underlying asset - * @dev `_owner` - Owner of the contract. Controls management functions. - * @dev `_strategy` - An optional strategy to enrich the adapter with additional functionality. - * @dev `_harvestCooldown` - Cooldown period between harvests. - * @dev `_requiredSigs` - Function signatures required by the strategy (EIP-165) - * @dev `_strategyConfig` - Additional data which can be used by the strategy on `harvest()` - * @dev This function is called by the factory contract when deploying a new vault. - * @dev Each Adapter implementation should implement checks to make sure that the adapter is wrapping the underlying protocol correctly. - * @dev If a strategy is provided, it will be verified to make sure it implements the required functions. - */ - function __AdapterBase_init(bytes memory popERC4626InitData) internal onlyInitializing { - ( - address asset, - address _owner, - address _strategy, - uint256 _harvestCooldown, - bytes4[8] memory _requiredSigs, - bytes memory _strategyConfig - ) = abi.decode(popERC4626InitData, (address, address, address, uint256, bytes4[8], bytes)); - __Owned_init(_owner); - __Pausable_init(); - __ERC4626_init(IERC20Metadata(asset)); - - INITIAL_CHAIN_ID = block.chainid; - INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); - - _decimals = IERC20Metadata(asset).decimals() + decimalOffset; // Asset decimals + decimal offset to combat inflation attacks - - strategy = IStrategy(_strategy); - strategyConfig = _strategyConfig; - harvestCooldown = _harvestCooldown; - - if (_strategy != address(0)) _verifyAndSetupStrategy(_requiredSigs); - - highWaterMark = 1e9; - lastHarvest = block.timestamp; - } - - function decimals() public view override(IERC20Metadata, ERC20) returns (uint8) { - return _decimals; - } - - /*////////////////////////////////////////////////////////////// + using SafeERC20 for IERC20; + using Math for uint256; + + uint8 internal _decimals; + uint8 public constant decimalOffset = 9; + + error StrategySetupFailed(); + + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize a new Adapter. + * @param popERC4626InitData Encoded data for the base adapter initialization. + * @dev `asset` - The underlying asset + * @dev `_owner` - Owner of the contract. Controls management functions. + * @dev `_strategy` - An optional strategy to enrich the adapter with additional functionality. + * @dev `_harvestCooldown` - Cooldown period between harvests. + * @dev `_requiredSigs` - Function signatures required by the strategy (EIP-165) + * @dev `_strategyConfig` - Additional data which can be used by the strategy on `harvest()` + * @dev This function is called by the factory contract when deploying a new vault. + * @dev Each Adapter implementation should implement checks to make sure that the adapter is wrapping the underlying protocol correctly. + * @dev If a strategy is provided, it will be verified to make sure it implements the required functions. + */ + function __AdapterBase_init( + bytes memory popERC4626InitData + ) internal onlyInitializing { + ( + address asset, + address _owner, + address _strategy, + uint256 _harvestCooldown, + bytes4[8] memory _requiredSigs, + bytes memory _strategyConfig + ) = abi.decode( + popERC4626InitData, + (address, address, address, uint256, bytes4[8], bytes) + ); + __Owned_init(_owner); + __Pausable_init(); + __ERC4626_init(IERC20Metadata(asset)); + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + + _decimals = IERC20Metadata(asset).decimals() + decimalOffset; // Asset decimals + decimal offset to combat inflation attacks + + strategy = IStrategy(_strategy); + strategyConfig = _strategyConfig; + harvestCooldown = _harvestCooldown; + + if (_strategy != address(0)) _verifyAndSetupStrategy(_requiredSigs); + + highWaterMark = 1e9; + lastHarvest = block.timestamp; + } + + function decimals() + public + view + override(IERC20Metadata, ERC20) + returns (uint8) + { + return _decimals; + } + + /*////////////////////////////////////////////////////////////// DEPOSIT/WITHDRAWAL LOGIC //////////////////////////////////////////////////////////////*/ - error MaxError(uint256 amount); - error ZeroAmount(); - - /** - * @notice Deposit `assets` into the underlying protocol and mints vault shares to `receiver`. - * @dev Executes harvest if `harvestCooldown` is passed since last invocation. - */ - function _deposit( - address caller, - address receiver, - uint256 assets, - uint256 shares - ) internal virtual override nonReentrant { - IERC20(asset()).safeTransferFrom(caller, address(this), assets); - - _protocolDeposit(assets, shares); - _mint(receiver, shares); - - harvest(); - - emit Deposit(caller, receiver, assets, shares); - } - - /** - * @notice Withdraws `assets` from the underlying protocol and burns vault shares from `owner`. - * @dev Executes harvest if `harvestCooldown` is passed since last invocation. - */ - function _withdraw( - address caller, - address receiver, - address owner, - uint256 assets, - uint256 shares - ) internal virtual override { - if (caller != owner) { - _spendAllowance(owner, caller, shares); - } + error MaxError(uint256 amount); + error ZeroAmount(); + + /** + * @notice Deposit `assets` into the underlying protocol and mints vault shares to `receiver`. + * @dev Executes harvest if `harvestCooldown` is passed since last invocation. + */ + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal virtual override nonReentrant { + IERC20(asset()).safeTransferFrom(caller, address(this), assets); - if (!paused()) { - _protocolWithdraw(assets, shares); + _protocolDeposit(assets, shares); + _mint(receiver, shares); + + harvest(); + + emit Deposit(caller, receiver, assets, shares); } - _burn(owner, shares); + /** + * @notice Withdraws `assets` from the underlying protocol and burns vault shares from `owner`. + * @dev Executes harvest if `harvestCooldown` is passed since last invocation. + */ + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + if (!paused()) { + _protocolWithdraw(assets, shares); + } - IERC20(asset()).safeTransfer(receiver, assets); + _burn(owner, shares); - harvest(); + IERC20(asset()).safeTransfer(receiver, assets); - emit Withdraw(caller, receiver, owner, assets, shares); - } + harvest(); - /*////////////////////////////////////////////////////////////// + emit Withdraw(caller, receiver, owner, assets, shares); + } + + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @notice Total amount of underlying `asset` token managed by adapter. - * @dev Return assets held by adapter if paused. - */ - function totalAssets() public view override returns (uint256) { - return paused() ? IERC20(asset()).balanceOf(address(this)) : _totalAssets(); - } - - /** - * @notice Total amount of underlying `asset` token managed by adapter through the underlying protocol. - */ - function _totalAssets() internal view virtual returns (uint256) {} - - /** - * @notice Convert either `assets` or `shares` into underlying shares. - * @dev This is an optional function for underlying protocols that require deposit/withdrawal amounts in their shares. - * @dev Returns shares if totalSupply is 0. - */ - function convertToUnderlyingShares(uint256 assets, uint256 shares) public view virtual returns (uint256) {} - - /** - * @notice Simulate the effects of a deposit at the current block, given current on-chain conditions. - * @dev Return 0 if paused since no further deposits are allowed. - * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. - */ - function previewDeposit(uint256 assets) public view virtual override returns (uint256) { - return paused() ? 0 : _convertToShares(assets, Math.Rounding.Down); - } - - /** - * @notice Simulate the effects of a mint at the current block, given current on-chain conditions. - * @dev Return 0 if paused since no further deposits are allowed. - * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. - */ - function previewMint(uint256 shares) public view virtual override returns (uint256) { - return paused() ? 0 : _convertToAssets(shares, Math.Rounding.Up); - } - - function _convertToShares(uint256 assets, Math.Rounding rounding) - internal - view - virtual - override - returns (uint256 shares) - { - return assets.mulDiv(totalSupply() + 10**decimalOffset, totalAssets() + 1, rounding); - } - - function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) { - return shares.mulDiv(totalAssets() + 1, totalSupply() + 10**decimalOffset, rounding); - } - - /*////////////////////////////////////////////////////////////// + /** + * @notice Total amount of underlying `asset` token managed by adapter. + * @dev Return assets held by adapter if paused. + */ + function totalAssets() public view override returns (uint256) { + return + paused() + ? IERC20(asset()).balanceOf(address(this)) + : _totalAssets(); + } + + /** + * @notice Total amount of underlying `asset` token managed by adapter through the underlying protocol. + */ + function _totalAssets() internal view virtual returns (uint256) {} + + /** + * @notice Convert either `assets` or `shares` into underlying shares. + * @dev This is an optional function for underlying protocols that require deposit/withdrawal amounts in their shares. + * @dev Returns shares if totalSupply is 0. + */ + function convertToUnderlyingShares( + uint256 assets, + uint256 shares + ) public view virtual returns (uint256) {} + + /** + * @notice Simulate the effects of a deposit at the current block, given current on-chain conditions. + * @dev Return 0 if paused since no further deposits are allowed. + * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. + */ + function previewDeposit( + uint256 assets + ) public view virtual override returns (uint256) { + return paused() ? 0 : _convertToShares(assets, Math.Rounding.Down); + } + + /** + * @notice Simulate the effects of a mint at the current block, given current on-chain conditions. + * @dev Return 0 if paused since no further deposits are allowed. + * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. + */ + function previewMint( + uint256 shares + ) public view virtual override returns (uint256) { + return paused() ? 0 : _convertToAssets(shares, Math.Rounding.Up); + } + + function _convertToShares( + uint256 assets, + Math.Rounding rounding + ) internal view virtual override returns (uint256 shares) { + return + assets.mulDiv( + totalSupply() + 10 ** decimalOffset, + totalAssets() + 1, + rounding + ); + } + + function _convertToAssets( + uint256 shares, + Math.Rounding rounding + ) internal view virtual override returns (uint256) { + return + shares.mulDiv( + totalAssets() + 1, + totalSupply() + 10 ** decimalOffset, + rounding + ); + } + + /*////////////////////////////////////////////////////////////// DEPOSIT/WITHDRAWAL LIMIT LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. - * @dev Return 0 if paused since no further deposits are allowed. - * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. - */ - function maxDeposit(address) public view virtual override returns (uint256) { - return paused() ? 0 : type(uint256).max; - } - - /** - * @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. - * @dev Return 0 if paused since no further deposits are allowed. - * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. - */ - function maxMint(address) public view virtual override returns (uint256) { - return paused() ? 0 : type(uint256).max; - } - - /*////////////////////////////////////////////////////////////// + /** + * @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. + * @dev Return 0 if paused since no further deposits are allowed. + * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. + */ + function maxDeposit( + address + ) public view virtual override returns (uint256) { + return paused() ? 0 : type(uint256).max; + } + + /** + * @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. + * @dev Return 0 if paused since no further deposits are allowed. + * @dev Override this function if the underlying protocol has a unique deposit logic and/or deposit fees. + */ + function maxMint(address) public view virtual override returns (uint256) { + return paused() ? 0 : type(uint256).max; + } + + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - IStrategy public strategy; - bytes public strategyConfig; - uint256 public lastHarvest; - - event Harvested(); - - /** - * @notice Execute Strategy and take fees. - * @dev Delegatecall to strategy's harvest() function. All necessary data is passed via `strategyConfig`. - * @dev Delegatecall is used to in case any logic requires the adapters address as a msg.sender. (e.g. Synthetix staking) - */ - function harvest() public takeFees { - if (address(strategy) != address(0) && ((lastHarvest + harvestCooldown) < block.timestamp)) { - // solhint-disable - (bool success, ) = address(strategy).delegatecall(abi.encodeWithSignature("harvest()")); - if (!success) revert(); - lastHarvest = block.timestamp; + IStrategy public strategy; + bytes public strategyConfig; + uint256 public lastHarvest; + + event Harvested(); + + /** + * @notice Execute Strategy and take fees. + * @dev Delegatecall to strategy's harvest() function. All necessary data is passed via `strategyConfig`. + * @dev Delegatecall is used to in case any logic requires the adapters address as a msg.sender. (e.g. Synthetix staking) + */ + function harvest() public takeFees { + if ( + address(strategy) != address(0) && + ((lastHarvest + harvestCooldown) < block.timestamp) + ) { + // solhint-disable + (bool success, ) = address(strategy).delegatecall( + abi.encodeWithSignature("harvest()") + ); + if (!success) revert(); + lastHarvest = block.timestamp; + } + + emit Harvested(); + } + + /** + * @notice Allows the strategy to deposit assets into the underlying protocol without minting new adapter shares. + * @dev This can be used e.g. for a compounding strategy to increase the value of each adapter share. + */ + function strategyDeposit( + uint256 amount, + uint256 shares + ) public onlyStrategy { + _protocolDeposit(amount, shares); + } + + /** + * @notice Allows the strategy to withdraw assets from the underlying protocol without burning adapter shares. + * @dev This can be used e.g. for a leverage strategy to reduce leverage without the need for the strategy to hold any adapter shares. + */ + function strategyWithdraw( + uint256 amount, + uint256 shares + ) public onlyStrategy { + _protocolWithdraw(amount, shares); + } + + /** + * @notice Verifies that the Adapter and Strategy are compatible and sets up the strategy. + * @dev This checks EIP165 compatibility and potentially other strategy specific checks (matching assets...). + * @dev It aftwards sets up anything required by the strategy to call `harvest()` like approvals etc. + */ + function _verifyAndSetupStrategy(bytes4[8] memory requiredSigs) internal { + strategy.verifyAdapterSelectorCompatibility(requiredSigs); + strategy.verifyAdapterCompatibility(strategyConfig); + strategy.setUp(strategyConfig); } - emit Harvested(); - } - - /** - * @notice Allows the strategy to deposit assets into the underlying protocol without minting new adapter shares. - * @dev This can be used e.g. for a compounding strategy to increase the value of each adapter share. - */ - function strategyDeposit(uint256 amount, uint256 shares) public onlyStrategy { - _protocolDeposit(amount, shares); - } - - /** - * @notice Allows the strategy to withdraw assets from the underlying protocol without burning adapter shares. - * @dev This can be used e.g. for a leverage strategy to reduce leverage without the need for the strategy to hold any adapter shares. - */ - function strategyWithdraw(uint256 amount, uint256 shares) public onlyStrategy { - _protocolWithdraw(amount, shares); - } - - /** - * @notice Verifies that the Adapter and Strategy are compatible and sets up the strategy. - * @dev This checks EIP165 compatibility and potentially other strategy specific checks (matching assets...). - * @dev It aftwards sets up anything required by the strategy to call `harvest()` like approvals etc. - */ - function _verifyAndSetupStrategy(bytes4[8] memory requiredSigs) internal { - strategy.verifyAdapterSelectorCompatibility(requiredSigs); - strategy.verifyAdapterCompatibility(strategyConfig); - strategy.setUp(strategyConfig); - } - - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// HARVEST COOLDOWN LOGIC //////////////////////////////////////////////////////////////*/ - uint256 public harvestCooldown; + uint256 public harvestCooldown; - event HarvestCooldownChanged(uint256 oldCooldown, uint256 newCooldown); + event HarvestCooldownChanged(uint256 oldCooldown, uint256 newCooldown); - error InvalidHarvestCooldown(uint256 cooldown); + error InvalidHarvestCooldown(uint256 cooldown); - /** - * @notice Set a new harvestCooldown for this adapter. Caller must be owner. - * @param newCooldown Time in seconds that must pass before a harvest can be called again. - * @dev Cant be longer than 1 day. - */ - function setHarvestCooldown(uint256 newCooldown) external onlyOwner { - // Dont wait more than X seconds - if (newCooldown >= 1 days) revert InvalidHarvestCooldown(newCooldown); + /** + * @notice Set a new harvestCooldown for this adapter. Caller must be owner. + * @param newCooldown Time in seconds that must pass before a harvest can be called again. + * @dev Cant be longer than 1 day. + */ + function setHarvestCooldown(uint256 newCooldown) external onlyOwner { + // Dont wait more than X seconds + if (newCooldown >= 1 days) revert InvalidHarvestCooldown(newCooldown); - emit HarvestCooldownChanged(harvestCooldown, newCooldown); + emit HarvestCooldownChanged(harvestCooldown, newCooldown); - harvestCooldown = newCooldown; - } + harvestCooldown = newCooldown; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// FEE LOGIC //////////////////////////////////////////////////////////////*/ - uint256 public performanceFee; - uint256 public highWaterMark; - - // TODO use deterministic fee recipient proxy - address public constant FEE_RECIPIENT = address(0x4444); - - event PerformanceFeeChanged(uint256 oldFee, uint256 newFee); - - error InvalidPerformanceFee(uint256 fee); - - /** - * @notice Performance fee that has accrued since last fee harvest. - * @return Accrued performance fee in underlying `asset` token. - * @dev Performance fee is based on a high water mark value. If vault share value has increased above the - * HWM in a fee period, issue fee shares to the vault equal to the performance fee. - */ - function accruedPerformanceFee() public view returns (uint256) { - uint256 highWaterMark_ = highWaterMark; - uint256 shareValue = convertToAssets(1e18); - uint256 performanceFee_ = performanceFee; - - return - performanceFee_ > 0 && shareValue > highWaterMark_ - ? performanceFee_.mulDiv((shareValue - highWaterMark_) * totalSupply(), 1e36, Math.Rounding.Down) - : 0; - } - - /** - * @notice Set a new performance fee for this adapter. Caller must be owner. - * @param newFee performance fee in 1e18. - * @dev Fees can be 0 but never more than 2e17 (1e18 = 100%, 1e14 = 1 BPS) - */ - function setPerformanceFee(uint256 newFee) public onlyOwner { - // Dont take more than 20% performanceFee - if (newFee > 2e17) revert InvalidPerformanceFee(newFee); + uint256 public performanceFee; + uint256 public highWaterMark; + + // TODO use deterministic fee recipient proxy + address public constant FEE_RECIPIENT = address(0x4444); + + event PerformanceFeeChanged(uint256 oldFee, uint256 newFee); + + error InvalidPerformanceFee(uint256 fee); + + /** + * @notice Performance fee that has accrued since last fee harvest. + * @return Accrued performance fee in underlying `asset` token. + * @dev Performance fee is based on a high water mark value. If vault share value has increased above the + * HWM in a fee period, issue fee shares to the vault equal to the performance fee. + */ + function accruedPerformanceFee() public view returns (uint256) { + uint256 highWaterMark_ = highWaterMark; + uint256 shareValue = convertToAssets(1e18); + uint256 performanceFee_ = performanceFee; + + return + performanceFee_ > 0 && shareValue > highWaterMark_ + ? performanceFee_.mulDiv( + (shareValue - highWaterMark_) * totalSupply(), + 1e36, + Math.Rounding.Down + ) + : 0; + } - emit PerformanceFeeChanged(performanceFee, newFee); + /** + * @notice Set a new performance fee for this adapter. Caller must be owner. + * @param newFee performance fee in 1e18. + * @dev Fees can be 0 but never more than 2e17 (1e18 = 100%, 1e14 = 1 BPS) + */ + function setPerformanceFee(uint256 newFee) public onlyOwner { + // Dont take more than 20% performanceFee + if (newFee > 2e17) revert InvalidPerformanceFee(newFee); - performanceFee = newFee; - } + emit PerformanceFeeChanged(performanceFee, newFee); - /// @notice Collect performance fees and update asset checkpoint. - modifier takeFees() { - _; + performanceFee = newFee; + } - uint256 fee = accruedPerformanceFee(); - if (fee > 0) _mint(FEE_RECIPIENT, convertToShares(fee)); + /// @notice Collect performance fees and update asset checkpoint. + modifier takeFees() { + _; + uint256 fee = accruedPerformanceFee(); + uint256 shareValue = convertToAssets(1e18); + + if (shareValue > highWaterMark) highWaterMark = shareValue; - uint256 shareValue = convertToAssets(1e18); - if (shareValue > highWaterMark) highWaterMark = shareValue; - } + if (fee > 0) _mint(FEE_RECIPIENT, convertToShares(fee)); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// PAUSING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Pause Deposits and withdraw all funds from the underlying protocol. Caller must be owner. - function pause() external onlyOwner { - _protocolWithdraw(totalAssets(), totalSupply()); - _pause(); - } + /// @notice Pause Deposits and withdraw all funds from the underlying protocol. Caller must be owner. + function pause() external onlyOwner { + _protocolWithdraw(totalAssets(), totalSupply()); + _pause(); + } - /// @notice Unpause Deposits and deposit all funds into the underlying protocol. Caller must be owner. - function unpause() external onlyOwner { - _protocolDeposit(totalAssets(), totalSupply()); - _unpause(); - } + /// @notice Unpause Deposits and deposit all funds into the underlying protocol. Caller must be owner. + function unpause() external onlyOwner { + _protocolDeposit(totalAssets(), totalSupply()); + _unpause(); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice deposit into the underlying protocol. - function _protocolDeposit(uint256 assets, uint256 shares) internal virtual { - // OPTIONAL - convertIntoUnderlyingShares(assets,shares) - } + /// @notice deposit into the underlying protocol. + function _protocolDeposit(uint256 assets, uint256 shares) internal virtual { + // OPTIONAL - convertIntoUnderlyingShares(assets,shares) + } - /// @notice Withdraw from the underlying protocol. - function _protocolWithdraw(uint256 assets, uint256 shares) internal virtual { - // OPTIONAL - convertIntoUnderlyingShares(assets,shares) - } + /// @notice Withdraw from the underlying protocol. + function _protocolWithdraw( + uint256 assets, + uint256 shares + ) internal virtual { + // OPTIONAL - convertIntoUnderlyingShares(assets,shares) + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return interfaceId == type(IAdapter).interfaceId; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-2612 LOGIC //////////////////////////////////////////////////////////////*/ - // EIP-2612 STORAGE - uint256 internal INITIAL_CHAIN_ID; - bytes32 internal INITIAL_DOMAIN_SEPARATOR; - mapping(address => uint256) public nonces; - - error PermitDeadlineExpired(uint256 deadline); - error InvalidSigner(address signer); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); - - // Unchecked because the only math done is incrementing - // the owner's nonce which cannot realistically overflow. - unchecked { - address recoveredAddress = ecrecover( - keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), + // EIP-2612 STORAGE + uint256 internal INITIAL_CHAIN_ID; + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + mapping(address => uint256) public nonces; + + error PermitDeadlineExpired(uint256 deadline); + error InvalidSigner(address signer); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + if (recoveredAddress == address(0) || recoveredAddress != owner) + revert InvalidSigner(recoveredAddress); + + _approve(recoveredAddress, spender, value); + } + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return + block.chainid == INITIAL_CHAIN_ID + ? INITIAL_DOMAIN_SEPARATOR + : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - spender, - value, - nonces[owner]++, - deadline - ) - ) - ) - ), - v, - r, - s - ); - - if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress); - - _approve(recoveredAddress, spender, value); + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(name())), + keccak256("1"), + block.chainid, + address(this) + ) + ); } - } - - function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { - return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); - } - - function computeDomainSeparator() internal view virtual returns (bytes32) { - return - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name())), - keccak256("1"), - block.chainid, - address(this) - ) - ); - } } From 52588f29bd3e4cd184ea4cbeeab35418a0cd28ca Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 5 Apr 2023 14:18:01 +0200 Subject: [PATCH 03/18] implemented recommendation-2 --- src/vault/VaultController.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vault/VaultController.sol b/src/vault/VaultController.sol index 014cf8d..e21267a 100644 --- a/src/vault/VaultController.sol +++ b/src/vault/VaultController.sol @@ -618,9 +618,9 @@ contract VaultController is Owned { * @param tokens Array of tokens. * @param fees Array of fees for `tokens` in 1e18. (1e18 = 100%, 1e14 = 1 BPS) * @dev See `MultiRewardEscrow` for more details. + * @dev We dont need to verify array length here since its done already in `MultiRewardEscrow` */ function setEscrowTokenFees(IERC20[] calldata tokens, uint256[] calldata fees) external onlyOwner { - _verifyEqualArrayLength(tokens.length, fees.length); (bool success, bytes memory returnData) = adminProxy.execute( address(escrow), abi.encodeWithSelector(IMultiRewardEscrow.setFees.selector, tokens, fees) From 553fbb2830bdfd638b152953e2d2d2805cb3d2d3 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 5 Apr 2023 14:19:19 +0200 Subject: [PATCH 04/18] implemented recommendation-4 --- lib/solmate | 2 +- src/utils/MultiRewardStaking.sol | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/solmate b/lib/solmate index 1b3adf6..2001af4 160000 --- a/lib/solmate +++ b/lib/solmate @@ -1 +1 @@ -Subproject commit 1b3adf677e7e383cc684b5d5bd441da86bf4bf1c +Subproject commit 2001af43aedb46fdc2335d2a7714fb2dae7cfcd1 diff --git a/src/utils/MultiRewardStaking.sol b/src/utils/MultiRewardStaking.sol index 1a3b7a9..4fc79db 100644 --- a/src/utils/MultiRewardStaking.sol +++ b/src/utils/MultiRewardStaking.sol @@ -333,8 +333,6 @@ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; } - rewardInfos[rewardToken].lastUpdatedTimestamp = block.timestamp.safeCastTo32(); - emit RewardInfoUpdate(rewardToken, rewards.rewardsPerSecond, rewardsEndTimestamp); } From d9eeddb2361c4676d56b388435d74417bb7704d2 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 5 Apr 2023 14:50:22 +0200 Subject: [PATCH 05/18] deleted managementFee var from takeFees --- src/vault/Vault.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index 7cfdb9f..0f89519 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -511,8 +511,7 @@ contract Vault is /// @notice Collect management and performance fees and update vault share high water mark. modifier takeFees() { - uint256 managementFee = accruedManagementFee(); - uint256 totalFee = managementFee + accruedPerformanceFee(); + uint256 totalFee = accruedManagementFee() + accruedPerformanceFee(); uint256 currentAssets = totalAssets(); uint256 shareValue = convertToAssets(1e18); From fd478b5381578e72fa5439bbaf9aeae9389fcaf0 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 11:31:23 +0200 Subject: [PATCH 06/18] adjusted addRewardToken for 0 rewardSpeed --- src/utils/MultiRewardStaking.sol | 978 ++++++++++++++++++------------- 1 file changed, 561 insertions(+), 417 deletions(-) diff --git a/src/utils/MultiRewardStaking.sol b/src/utils/MultiRewardStaking.sol index 4fc79db..cd6c1a0 100644 --- a/src/utils/MultiRewardStaking.sol +++ b/src/utils/MultiRewardStaking.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.15; -import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import { ERC4626Upgradeable, ERC20Upgradeable, IERC20Upgradeable as IERC20, IERC20MetadataUpgradeable as IERC20Metadata } from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -import { MathUpgradeable as Math } from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; -import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; -import { OwnedUpgradeable } from "./OwnedUpgradeable.sol"; -import { IMultiRewardEscrow } from "../interfaces/IMultiRewardEscrow.sol"; -import { RewardInfo, EscrowInfo } from "../interfaces/IMultiRewardStaking.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ERC4626Upgradeable, ERC20Upgradeable, IERC20Upgradeable as IERC20, IERC20MetadataUpgradeable as IERC20Metadata} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {MathUpgradeable as Math} from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; +import {OwnedUpgradeable} from "./OwnedUpgradeable.sol"; +import {IMultiRewardEscrow} from "../interfaces/IMultiRewardEscrow.sol"; +import {RewardInfo, EscrowInfo} from "../interfaces/IMultiRewardStaking.sol"; /** * @title MultiRewardStaking @@ -24,472 +24,616 @@ import { RewardInfo, EscrowInfo } from "../interfaces/IMultiRewardStaking.sol"; * Based on the flywheel implementation of fei-protocol https://github.com/fei-protocol/flywheel-v2 */ contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable { - using SafeERC20 for IERC20; - using SafeCastLib for uint256; - using Math for uint256; - - string private _name; - string private _symbol; - uint8 private _decimals; - - /** - * @notice Initialize a new MultiRewardStaking contract. - * @param _stakingToken The token to be staked. - * @param _escrow An optional escrow contract which can be used to lock rewards on claim. - * @param _owner Owner of the contract. Controls management functions. - */ - function initialize(IERC20 _stakingToken, IMultiRewardEscrow _escrow, address _owner) external initializer { - __ERC4626_init(IERC20Metadata(address(_stakingToken))); - __Owned_init(_owner); - - _name = string(abi.encodePacked("Staked ", IERC20Metadata(address(_stakingToken)).name())); - _symbol = string(abi.encodePacked("pst-", IERC20Metadata(address(_stakingToken)).symbol())); - _decimals = IERC20Metadata(address(_stakingToken)).decimals(); - - escrow = _escrow; - - INITIAL_CHAIN_ID = block.chainid; - INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); - } - - function name() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { - return _name; - } - - function symbol() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { - return _symbol; - } - - function decimals() public view override(ERC20Upgradeable, IERC20Metadata) returns (uint8) { - return _decimals; - } - - /*////////////////////////////////////////////////////////////// - ERC4626 MUTATIVE FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - function deposit(uint256 _amount) external returns (uint256) { - return deposit(_amount, msg.sender); - } - - function mint(uint256 _amount) external returns (uint256) { - return mint(_amount, msg.sender); - } - - function withdraw(uint256 _amount) external returns (uint256) { - return withdraw(_amount, msg.sender, msg.sender); - } + using SafeERC20 for IERC20; + using SafeCastLib for uint256; + using Math for uint256; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @notice Initialize a new MultiRewardStaking contract. + * @param _stakingToken The token to be staked. + * @param _escrow An optional escrow contract which can be used to lock rewards on claim. + * @param _owner Owner of the contract. Controls management functions. + */ + function initialize( + IERC20 _stakingToken, + IMultiRewardEscrow _escrow, + address _owner + ) external initializer { + __ERC4626_init(IERC20Metadata(address(_stakingToken))); + __Owned_init(_owner); + + _name = string( + abi.encodePacked( + "Staked ", + IERC20Metadata(address(_stakingToken)).name() + ) + ); + _symbol = string( + abi.encodePacked( + "pst-", + IERC20Metadata(address(_stakingToken)).symbol() + ) + ); + _decimals = IERC20Metadata(address(_stakingToken)).decimals(); - function redeem(uint256 _amount) external returns (uint256) { - return redeem(_amount, msg.sender, msg.sender); - } + escrow = _escrow; - /*////////////////////////////////////////////////////////////// - ERC4626 OVERRIDES - //////////////////////////////////////////////////////////////*/ + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } - error ZeroAddressTransfer(address from, address to); - error InsufficentBalance(); + function name() + public + view + override(ERC20Upgradeable, IERC20Metadata) + returns (string memory) + { + return _name; + } - function _convertToShares(uint256 assets, Math.Rounding) internal pure override returns (uint256) { - return assets; - } + function symbol() + public + view + override(ERC20Upgradeable, IERC20Metadata) + returns (string memory) + { + return _symbol; + } - function _convertToAssets(uint256 shares, Math.Rounding) internal pure override returns (uint256) { - return shares; - } + function decimals() + public + view + override(ERC20Upgradeable, IERC20Metadata) + returns (uint8) + { + return _decimals; + } - /// @notice Internal deposit function used by `deposit()` and `mint()`. Accrues rewards for the `caller` and `receiver`. - function _deposit( - address caller, - address receiver, - uint256 assets, - uint256 shares - ) internal override accrueRewards(caller, receiver) { - IERC20(asset()).safeTransferFrom(caller, address(this), assets); + /*////////////////////////////////////////////////////////////// + ERC4626 MUTATIVE FUNCTIONS + //////////////////////////////////////////////////////////////*/ - _mint(receiver, shares); + function deposit(uint256 _amount) external returns (uint256) { + return deposit(_amount, msg.sender); + } - emit Deposit(caller, receiver, assets, shares); - } + function mint(uint256 _amount) external returns (uint256) { + return mint(_amount, msg.sender); + } - /// @notice Internal withdraw function used by `withdraw()` and `redeem()`. Accrues rewards for the `caller` and `receiver`. - function _withdraw( - address caller, - address receiver, - address owner, - uint256 assets, - uint256 shares - ) internal override accrueRewards(owner, receiver) { - if (caller != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + function withdraw(uint256 _amount) external returns (uint256) { + return withdraw(_amount, msg.sender, msg.sender); + } - _burn(owner, shares); - IERC20(asset()).safeTransfer(receiver, assets); + function redeem(uint256 _amount) external returns (uint256) { + return redeem(_amount, msg.sender, msg.sender); + } - emit Withdraw(caller, receiver, owner, assets, shares); - } + /*////////////////////////////////////////////////////////////// + ERC4626 OVERRIDES + //////////////////////////////////////////////////////////////*/ - /// @notice Internal transfer function used by `transfer()` and `transferFrom()`. Accrues rewards for `from` and `to`. - function _transfer(address from, address to, uint256 amount) internal override accrueRewards(from, to) { - if (from == address(0) || to == address(0)) revert ZeroAddressTransfer(from, to); + error ZeroAddressTransfer(address from, address to); + error InsufficentBalance(); - uint256 fromBalance = balanceOf(from); - if (fromBalance < amount) revert InsufficentBalance(); + function _convertToShares( + uint256 assets, + Math.Rounding + ) internal pure override returns (uint256) { + return assets; + } - _burn(from, amount); - _mint(to, amount); + function _convertToAssets( + uint256 shares, + Math.Rounding + ) internal pure override returns (uint256) { + return shares; + } - emit Transfer(from, to, amount); - } + /// @notice Internal deposit function used by `deposit()` and `mint()`. Accrues rewards for the `caller` and `receiver`. + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal override accrueRewards(caller, receiver) { + IERC20(asset()).safeTransferFrom(caller, address(this), assets); - /*////////////////////////////////////////////////////////////// - CLAIM LOGIC - //////////////////////////////////////////////////////////////*/ + _mint(receiver, shares); - IMultiRewardEscrow public escrow; + emit Deposit(caller, receiver, assets, shares); + } - event RewardsClaimed(address indexed user, IERC20 rewardToken, uint256 amount, bool escrowed); + /// @notice Internal withdraw function used by `withdraw()` and `redeem()`. Accrues rewards for the `caller` and `receiver`. + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal override accrueRewards(owner, receiver) { + if (caller != owner) + _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + + _burn(owner, shares); + IERC20(asset()).safeTransfer(receiver, assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + } - error ZeroRewards(IERC20 rewardToken); + /// @notice Internal transfer function used by `transfer()` and `transferFrom()`. Accrues rewards for `from` and `to`. + function _transfer( + address from, + address to, + uint256 amount + ) internal override accrueRewards(from, to) { + if (from == address(0) || to == address(0)) + revert ZeroAddressTransfer(from, to); - /** - * @notice Claim rewards for a user in any amount of rewardTokens. - * @param user User for which rewards should be claimed. - * @param _rewardTokens Array of rewardTokens for which rewards should be claimed. - * @dev This function will revert if any of the rewardTokens have zero rewards accrued. - * @dev A percentage of each reward can be locked in an escrow contract if this was previously configured. - */ - function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) { - for (uint8 i; i < _rewardTokens.length; i++) { - uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; + uint256 fromBalance = balanceOf(from); + if (fromBalance < amount) revert InsufficentBalance(); - if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); + _burn(from, amount); + _mint(to, amount); - accruedRewards[user][_rewardTokens[i]] = 0; + emit Transfer(from, to, amount); + } - EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; + /*////////////////////////////////////////////////////////////// + CLAIM LOGIC + //////////////////////////////////////////////////////////////*/ - if (escrowInfo.escrowPercentage > 0) { - _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); - emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); - } else { - _rewardTokens[i].transfer(user, rewardAmount); - emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); - } + IMultiRewardEscrow public escrow; + + event RewardsClaimed( + address indexed user, + IERC20 rewardToken, + uint256 amount, + bool escrowed + ); + + error ZeroRewards(IERC20 rewardToken); + + /** + * @notice Claim rewards for a user in any amount of rewardTokens. + * @param user User for which rewards should be claimed. + * @param _rewardTokens Array of rewardTokens for which rewards should be claimed. + * @dev This function will revert if any of the rewardTokens have zero rewards accrued. + * @dev A percentage of each reward can be locked in an escrow contract if this was previously configured. + */ + function claimRewards( + address user, + IERC20[] memory _rewardTokens + ) external accrueRewards(msg.sender, user) { + for (uint8 i; i < _rewardTokens.length; i++) { + uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; + + if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); + + accruedRewards[user][_rewardTokens[i]] = 0; + + EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; + + if (escrowInfo.escrowPercentage > 0) { + _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); + emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); + } else { + _rewardTokens[i].transfer(user, rewardAmount); + emit RewardsClaimed( + user, + _rewardTokens[i], + rewardAmount, + false + ); + } + } } - } - - /// @notice Locks a percentage of a reward in an escrow contract. Pays out the rest to the user. - function _lockToken(address user, IERC20 rewardToken, uint256 rewardAmount, EscrowInfo memory escrowInfo) internal { - uint256 escrowed = rewardAmount.mulDiv(uint256(escrowInfo.escrowPercentage), 1e18, Math.Rounding.Down); - uint256 payout = rewardAmount - escrowed; - rewardToken.safeTransfer(user, payout); - escrow.lock(rewardToken, user, escrowed, escrowInfo.escrowDuration, escrowInfo.offset); - } + /// @notice Locks a percentage of a reward in an escrow contract. Pays out the rest to the user. + function _lockToken( + address user, + IERC20 rewardToken, + uint256 rewardAmount, + EscrowInfo memory escrowInfo + ) internal { + uint256 escrowed = rewardAmount.mulDiv( + uint256(escrowInfo.escrowPercentage), + 1e18, + Math.Rounding.Down + ); + uint256 payout = rewardAmount - escrowed; + + rewardToken.safeTransfer(user, payout); + escrow.lock( + rewardToken, + user, + escrowed, + escrowInfo.escrowDuration, + escrowInfo.offset + ); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// REWARDS MANAGEMENT LOGIC //////////////////////////////////////////////////////////////*/ - IERC20[] public rewardTokens; - - // rewardToken -> RewardInfo - mapping(IERC20 => RewardInfo) public rewardInfos; - // rewardToken -> EscrowInfo - mapping(IERC20 => EscrowInfo) public escrowInfos; - - // user => rewardToken -> rewardsIndex - mapping(address => mapping(IERC20 => uint256)) public userIndex; - // user => rewardToken -> accruedRewards - mapping(address => mapping(IERC20 => uint256)) public accruedRewards; - - event RewardInfoUpdate(IERC20 rewardToken, uint160 rewardsPerSecond, uint32 rewardsEndTimestamp); - - error RewardTokenAlreadyExist(IERC20 rewardToken); - error RewardTokenDoesntExist(IERC20 rewardToken); - error RewardTokenCantBeStakingToken(); - error ZeroAmount(); - error NotSubmitter(address submitter); - error RewardsAreDynamic(IERC20 rewardToken); - error ZeroRewardsSpeed(); - error InvalidConfig(); - - /** - * @notice Adds a new rewardToken which can be earned via staking. Caller must be owner. - * @param rewardToken Token that can be earned by staking. - * @param rewardsPerSecond The rate in which `rewardToken` will be accrued. - * @param amount Initial funding amount for this reward. - * @param useEscrow Bool if the rewards should be escrowed on claim. - * @param escrowPercentage The percentage of the reward that gets escrowed in 1e18. (1e18 = 100%, 1e14 = 1 BPS) - * @param escrowDuration The duration of the escrow. - * @param offset A cliff after claim before the escrow starts. - * @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`. - * @dev If `rewardsPerSecond` is 0 the rewards will be paid out instantly. In this case `amount` must be 0. - * @dev If `useEscrow` is `false` the `escrowDuration`, `escrowPercentage` and `offset` will be ignored. - * @dev The max amount of rewardTokens is 20. - */ - function addRewardToken( - IERC20 rewardToken, - uint160 rewardsPerSecond, - uint256 amount, - bool useEscrow, - uint192 escrowPercentage, - uint32 escrowDuration, - uint32 offset - ) external onlyOwner { - if (rewardTokens.length == 20) revert InvalidConfig(); - if (asset() == address(rewardToken)) revert RewardTokenCantBeStakingToken(); - - RewardInfo memory rewards = rewardInfos[rewardToken]; - if (rewards.lastUpdatedTimestamp > 0) revert RewardTokenAlreadyExist(rewardToken); - - if (amount > 0) { - if (rewardsPerSecond == 0 && totalSupply() == 0) revert InvalidConfig(); - rewardToken.safeTransferFrom(msg.sender, address(this), amount); + IERC20[] public rewardTokens; + + // rewardToken -> RewardInfo + mapping(IERC20 => RewardInfo) public rewardInfos; + // rewardToken -> EscrowInfo + mapping(IERC20 => EscrowInfo) public escrowInfos; + + // user => rewardToken -> rewardsIndex + mapping(address => mapping(IERC20 => uint256)) public userIndex; + // user => rewardToken -> accruedRewards + mapping(address => mapping(IERC20 => uint256)) public accruedRewards; + + event RewardInfoUpdate( + IERC20 rewardToken, + uint160 rewardsPerSecond, + uint32 rewardsEndTimestamp + ); + + error RewardTokenAlreadyExist(IERC20 rewardToken); + error RewardTokenDoesntExist(IERC20 rewardToken); + error RewardTokenCantBeStakingToken(); + error ZeroAmount(); + error NotSubmitter(address submitter); + error RewardsAreDynamic(IERC20 rewardToken); + error ZeroRewardsSpeed(); + error InvalidConfig(); + + /** + * @notice Adds a new rewardToken which can be earned via staking. Caller must be owner. + * @param rewardToken Token that can be earned by staking. + * @param rewardsPerSecond The rate in which `rewardToken` will be accrued. + * @param amount Initial funding amount for this reward. + * @param useEscrow Bool if the rewards should be escrowed on claim. + * @param escrowPercentage The percentage of the reward that gets escrowed in 1e18. (1e18 = 100%, 1e14 = 1 BPS) + * @param escrowDuration The duration of the escrow. + * @param offset A cliff after claim before the escrow starts. + * @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`. + * @dev If `rewardsPerSecond` is 0 the rewards will be paid out instantly. In this case `amount` must be 0. + * @dev If `useEscrow` is `false` the `escrowDuration`, `escrowPercentage` and `offset` will be ignored. + * @dev The max amount of rewardTokens is 20. + */ + function addRewardToken( + IERC20 rewardToken, + uint160 rewardsPerSecond, + uint256 amount, + bool useEscrow, + uint192 escrowPercentage, + uint32 escrowDuration, + uint32 offset + ) external onlyOwner { + if (rewardTokens.length == 20) revert InvalidConfig(); + if (asset() == address(rewardToken)) + revert RewardTokenCantBeStakingToken(); + + RewardInfo memory rewards = rewardInfos[rewardToken]; + if (rewards.lastUpdatedTimestamp > 0) + revert RewardTokenAlreadyExist(rewardToken); + + if (amount > 0) { + if (rewardsPerSecond == 0 && totalSupply() == 0) + revert InvalidConfig(); + rewardToken.safeTransferFrom(msg.sender, address(this), amount); + } + + // Add the rewardToken to all existing rewardToken + rewardTokens.push(rewardToken); + + if (useEscrow) { + if (escrowPercentage == 0 || escrowPercentage > 1e18) + revert InvalidConfig(); + escrowInfos[rewardToken] = EscrowInfo({ + escrowPercentage: escrowPercentage, + escrowDuration: escrowDuration, + offset: offset + }); + rewardToken.safeApprove(address(escrow), type(uint256).max); + } + + uint64 ONE = (10 ** IERC20Metadata(address(rewardToken)).decimals()) + .safeCastTo64(); + uint224 index = rewardsPerSecond == 0 && amount > 0 + ? ONE + + amount + .mulDiv( + uint256(10 ** decimals()), + totalSupply(), + Math.Rounding.Down + ) + .safeCastTo224() + : ONE; + uint32 rewardsEndTimestamp = rewardsPerSecond == 0 + ? block.timestamp.safeCastTo32() + : _calcRewardsEnd(0, rewardsPerSecond, amount); + + rewardInfos[rewardToken] = RewardInfo({ + ONE: ONE, + rewardsPerSecond: rewardsPerSecond, + rewardsEndTimestamp: rewardsEndTimestamp, + index: index, + lastUpdatedTimestamp: block.timestamp.safeCastTo32() + }); + + emit RewardInfoUpdate( + rewardToken, + rewardsPerSecond, + rewardsEndTimestamp + ); } - // Add the rewardToken to all existing rewardToken - rewardTokens.push(rewardToken); - - if (useEscrow) { - if (escrowPercentage == 0 || escrowPercentage > 1e18) revert InvalidConfig(); - escrowInfos[rewardToken] = EscrowInfo({ - escrowPercentage: escrowPercentage, - escrowDuration: escrowDuration, - offset: offset - }); - rewardToken.safeApprove(address(escrow), type(uint256).max); + /** + * @notice Changes rewards speed for a rewardToken. This works only for rewards that accrue over time. Caller must be owner. + * @param rewardToken Token that can be earned by staking. + * @param rewardsPerSecond The rate in which `rewardToken` will be accrued. + * @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`. + */ + function changeRewardSpeed( + IERC20 rewardToken, + uint160 rewardsPerSecond + ) external onlyOwner { + RewardInfo memory rewards = rewardInfos[rewardToken]; + + if (rewardsPerSecond == 0) revert ZeroAmount(); + if (rewards.lastUpdatedTimestamp == 0) + revert RewardTokenDoesntExist(rewardToken); + if (rewards.rewardsPerSecond == 0) + revert RewardsAreDynamic(rewardToken); + + _accrueRewards(rewardToken, _accrueStatic(rewards)); + + uint256 prevEndTime = uint256(rewards.rewardsEndTimestamp); + uint256 currTime = block.timestamp; + uint256 remainder = prevEndTime <= currTime + ? 0 + : uint256(rewards.rewardsPerSecond) * (prevEndTime - currTime); + + uint32 rewardsEndTimestamp = _calcRewardsEnd( + currTime.safeCastTo32(), + rewardsPerSecond, + remainder + ); + rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; + rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; } - uint64 ONE = (10 ** IERC20Metadata(address(rewardToken)).decimals()).safeCastTo64(); - uint32 rewardsEndTimestamp = rewardsPerSecond == 0 - ? block.timestamp.safeCastTo32() - : _calcRewardsEnd(0, rewardsPerSecond, amount); - - rewardInfos[rewardToken] = RewardInfo({ - ONE: ONE, - rewardsPerSecond: rewardsPerSecond, - rewardsEndTimestamp: rewardsEndTimestamp, - index: ONE, - lastUpdatedTimestamp: block.timestamp.safeCastTo32() - }); - - emit RewardInfoUpdate(rewardToken, rewardsPerSecond, rewardsEndTimestamp); - } - - /** - * @notice Changes rewards speed for a rewardToken. This works only for rewards that accrue over time. Caller must be owner. - * @param rewardToken Token that can be earned by staking. - * @param rewardsPerSecond The rate in which `rewardToken` will be accrued. - * @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`. - */ - function changeRewardSpeed(IERC20 rewardToken, uint160 rewardsPerSecond) external onlyOwner { - RewardInfo memory rewards = rewardInfos[rewardToken]; - - if (rewardsPerSecond == 0) revert ZeroAmount(); - if (rewards.lastUpdatedTimestamp == 0) revert RewardTokenDoesntExist(rewardToken); - if (rewards.rewardsPerSecond == 0) revert RewardsAreDynamic(rewardToken); - - _accrueRewards(rewardToken, _accrueStatic(rewards)); - - uint256 prevEndTime = uint256(rewards.rewardsEndTimestamp); - uint256 currTime = block.timestamp; - uint256 remainder = prevEndTime <= currTime ? 0 : uint256(rewards.rewardsPerSecond) * (prevEndTime - currTime); - - uint32 rewardsEndTimestamp = _calcRewardsEnd(currTime.safeCastTo32(), rewardsPerSecond, remainder); - rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; - rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; - } - - /** - * @notice Funds rewards for a rewardToken. - * @param rewardToken Token that can be earned by staking. - * @param amount The amount of rewardToken that will fund this reward. - * @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`. - * @dev If `rewardsPerSecond` is 0 the rewards will be paid out instantly. - */ - function fundReward(IERC20 rewardToken, uint256 amount) external { - if (amount == 0) revert ZeroAmount(); - - // Cache RewardInfo - RewardInfo memory rewards = rewardInfos[rewardToken]; - - if (rewards.rewardsPerSecond == 0 && totalSupply() == 0) revert InvalidConfig(); - - // Make sure that the reward exists - if (rewards.lastUpdatedTimestamp == 0) revert RewardTokenDoesntExist(rewardToken); - - // Transfer additional rewardToken to fund rewards of this vault - rewardToken.safeTransferFrom(msg.sender, address(this), amount); - - uint256 accrued = rewards.rewardsPerSecond == 0 ? amount : _accrueStatic(rewards); - - // Update the index of rewardInfo before updating the rewardInfo - _accrueRewards(rewardToken, accrued); - uint32 rewardsEndTimestamp = rewards.rewardsEndTimestamp; - if (rewards.rewardsPerSecond > 0) { - rewardsEndTimestamp = _calcRewardsEnd(rewards.rewardsEndTimestamp, rewards.rewardsPerSecond, amount); - rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; + /** + * @notice Funds rewards for a rewardToken. + * @param rewardToken Token that can be earned by staking. + * @param amount The amount of rewardToken that will fund this reward. + * @dev The `rewardsEndTimestamp` gets calculated based on `rewardsPerSecond` and `amount`. + * @dev If `rewardsPerSecond` is 0 the rewards will be paid out instantly. + */ + function fundReward(IERC20 rewardToken, uint256 amount) external { + if (amount == 0) revert ZeroAmount(); + + // Cache RewardInfo + RewardInfo memory rewards = rewardInfos[rewardToken]; + + if (rewards.rewardsPerSecond == 0 && totalSupply() == 0) + revert InvalidConfig(); + + // Make sure that the reward exists + if (rewards.lastUpdatedTimestamp == 0) + revert RewardTokenDoesntExist(rewardToken); + + // Transfer additional rewardToken to fund rewards of this vault + rewardToken.safeTransferFrom(msg.sender, address(this), amount); + + uint256 accrued = rewards.rewardsPerSecond == 0 + ? amount + : _accrueStatic(rewards); + + // Update the index of rewardInfo before updating the rewardInfo + _accrueRewards(rewardToken, accrued); + uint32 rewardsEndTimestamp = rewards.rewardsEndTimestamp; + if (rewards.rewardsPerSecond > 0) { + rewardsEndTimestamp = _calcRewardsEnd( + rewards.rewardsEndTimestamp, + rewards.rewardsPerSecond, + amount + ); + rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; + } + + emit RewardInfoUpdate( + rewardToken, + rewards.rewardsPerSecond, + rewardsEndTimestamp + ); } - emit RewardInfoUpdate(rewardToken, rewards.rewardsPerSecond, rewardsEndTimestamp); - } - - function _calcRewardsEnd( - uint32 rewardsEndTimestamp, - uint160 rewardsPerSecond, - uint256 amount - ) internal returns (uint32) { - if (rewardsEndTimestamp > block.timestamp) - amount += uint256(rewardsPerSecond) * (rewardsEndTimestamp - block.timestamp); - - return (block.timestamp + (amount / uint256(rewardsPerSecond))).safeCastTo32(); - } + function _calcRewardsEnd( + uint32 rewardsEndTimestamp, + uint160 rewardsPerSecond, + uint256 amount + ) internal returns (uint32) { + if (rewardsEndTimestamp > block.timestamp) + amount += + uint256(rewardsPerSecond) * + (rewardsEndTimestamp - block.timestamp); + + return + (block.timestamp + (amount / uint256(rewardsPerSecond))) + .safeCastTo32(); + } - function getAllRewardsTokens() external view returns (IERC20[] memory) { - return rewardTokens; - } + function getAllRewardsTokens() external view returns (IERC20[] memory) { + return rewardTokens; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// REWARDS ACCRUAL LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Accrue rewards for up to 2 users for all available reward tokens. - modifier accrueRewards(address _caller, address _receiver) { - IERC20[] memory _rewardTokens = rewardTokens; - for (uint8 i; i < _rewardTokens.length; i++) { - IERC20 rewardToken = _rewardTokens[i]; - RewardInfo memory rewards = rewardInfos[rewardToken]; - - if (rewards.rewardsPerSecond > 0) _accrueRewards(rewardToken, _accrueStatic(rewards)); - _accrueUser(_receiver, rewardToken); - - // If a deposit/withdraw operation gets called for another user we should accrue for both of them to avoid potential issues like in the Convex-Vulnerability - if (_receiver != _caller) _accrueUser(_caller, rewardToken); - } - _; - } - - /** - * @notice Accrue rewards over time. - * @dev Based on https://github.com/fei-protocol/flywheel-v2/blob/main/src/rewards/FlywheelStaticRewards.sol - */ - function _accrueStatic(RewardInfo memory rewards) internal view returns (uint256 accrued) { - uint256 elapsed; - if (rewards.rewardsEndTimestamp > block.timestamp) { - elapsed = block.timestamp - rewards.lastUpdatedTimestamp; - } else if (rewards.rewardsEndTimestamp > rewards.lastUpdatedTimestamp) { - elapsed = rewards.rewardsEndTimestamp - rewards.lastUpdatedTimestamp; + /// @notice Accrue rewards for up to 2 users for all available reward tokens. + modifier accrueRewards(address _caller, address _receiver) { + IERC20[] memory _rewardTokens = rewardTokens; + for (uint8 i; i < _rewardTokens.length; i++) { + IERC20 rewardToken = _rewardTokens[i]; + RewardInfo memory rewards = rewardInfos[rewardToken]; + + if (rewards.rewardsPerSecond > 0) + _accrueRewards(rewardToken, _accrueStatic(rewards)); + _accrueUser(_receiver, rewardToken); + + // If a deposit/withdraw operation gets called for another user we should accrue for both of them to avoid potential issues like in the Convex-Vulnerability + if (_receiver != _caller) _accrueUser(_caller, rewardToken); + } + _; } - accrued = uint256(rewards.rewardsPerSecond * elapsed); - } - - /// @notice Accrue global rewards for a rewardToken - function _accrueRewards(IERC20 _rewardToken, uint256 accrued) internal { - uint256 supplyTokens = totalSupply(); - uint224 deltaIndex; // DeltaIndex is the amount of rewardsToken paid out per stakeToken - if (supplyTokens != 0) - deltaIndex = accrued.mulDiv(uint256(10 ** decimals()), supplyTokens, Math.Rounding.Down).safeCastTo224(); - // rewardDecimals * stakeDecimals / stakeDecimals = rewardDecimals - // 1e18 * 1e6 / 10e6 = 0.1e18 | 1e6 * 1e18 / 10e18 = 0.1e6 + /** + * @notice Accrue rewards over time. + * @dev Based on https://github.com/fei-protocol/flywheel-v2/blob/main/src/rewards/FlywheelStaticRewards.sol + */ + function _accrueStatic( + RewardInfo memory rewards + ) internal view returns (uint256 accrued) { + uint256 elapsed; + if (rewards.rewardsEndTimestamp > block.timestamp) { + elapsed = block.timestamp - rewards.lastUpdatedTimestamp; + } else if (rewards.rewardsEndTimestamp > rewards.lastUpdatedTimestamp) { + elapsed = + rewards.rewardsEndTimestamp - + rewards.lastUpdatedTimestamp; + } + + accrued = uint256(rewards.rewardsPerSecond * elapsed); + } - rewardInfos[_rewardToken].index += deltaIndex; - rewardInfos[_rewardToken].lastUpdatedTimestamp = block.timestamp.safeCastTo32(); - } + /// @notice Accrue global rewards for a rewardToken + function _accrueRewards(IERC20 _rewardToken, uint256 accrued) internal { + uint256 supplyTokens = totalSupply(); + uint224 deltaIndex; // DeltaIndex is the amount of rewardsToken paid out per stakeToken + if (supplyTokens != 0) + deltaIndex = accrued + .mulDiv( + uint256(10 ** decimals()), + supplyTokens, + Math.Rounding.Down + ) + .safeCastTo224(); + // rewardDecimals * stakeDecimals / stakeDecimals = rewardDecimals + // 1e18 * 1e6 / 10e6 = 0.1e18 | 1e6 * 1e18 / 10e18 = 0.1e6 + + rewardInfos[_rewardToken].index += deltaIndex; + rewardInfos[_rewardToken].lastUpdatedTimestamp = block + .timestamp + .safeCastTo32(); + } - /// @notice Sync a user's rewards for a rewardToken with the global reward index for that token - function _accrueUser(address _user, IERC20 _rewardToken) internal { - RewardInfo memory rewards = rewardInfos[_rewardToken]; + /// @notice Sync a user's rewards for a rewardToken with the global reward index for that token + function _accrueUser(address _user, IERC20 _rewardToken) internal { + RewardInfo memory rewards = rewardInfos[_rewardToken]; - uint256 oldIndex = userIndex[_user][_rewardToken]; + uint256 oldIndex = userIndex[_user][_rewardToken]; - // If user hasn't yet accrued rewards, grant them interest from the strategy beginning if they have a balance - // Zero balances will have no effect other than syncing to global index - if (oldIndex == 0) { - oldIndex = rewards.ONE; - } + // If user hasn't yet accrued rewards, grant them interest from the strategy beginning if they have a balance + // Zero balances will have no effect other than syncing to global index + if (oldIndex == 0) { + oldIndex = rewards.ONE; + } - uint256 deltaIndex = rewards.index - oldIndex; + uint256 deltaIndex = rewards.index - oldIndex; - // Accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed - uint256 supplierDelta = balanceOf(_user).mulDiv(deltaIndex, uint256(10 ** decimals()), Math.Rounding.Down); - // stakeDecimals * rewardDecimals / stakeDecimals = rewardDecimals - // 1e18 * 1e6 / 10e18 = 0.1e18 | 1e6 * 1e18 / 10e18 = 0.1e6 + // Accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed + uint256 supplierDelta = balanceOf(_user).mulDiv( + deltaIndex, + uint256(10 ** decimals()), + Math.Rounding.Down + ); + // stakeDecimals * rewardDecimals / stakeDecimals = rewardDecimals + // 1e18 * 1e6 / 10e18 = 0.1e18 | 1e6 * 1e18 / 10e18 = 0.1e6 - userIndex[_user][_rewardToken] = rewards.index; + userIndex[_user][_rewardToken] = rewards.index; - accruedRewards[_user][_rewardToken] += supplierDelta; - } + accruedRewards[_user][_rewardToken] += supplierDelta; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// PERMIT LOGC //////////////////////////////////////////////////////////////*/ - uint256 internal INITIAL_CHAIN_ID; - bytes32 internal INITIAL_DOMAIN_SEPARATOR; - mapping(address => uint256) public nonces; - - error PermitDeadlineExpired(uint256 deadline); - error InvalidSigner(address signer); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); - - // Unchecked because the only math done is incrementing - // the owner's nonce which cannot realistically overflow. - unchecked { - address recoveredAddress = ecrecover( - keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - spender, - value, - nonces[owner]++, - deadline - ) - ) - ) - ), - v, - r, - s - ); + uint256 internal INITIAL_CHAIN_ID; + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + mapping(address => uint256) public nonces; + + error PermitDeadlineExpired(uint256 deadline); + error InvalidSigner(address signer); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + if (recoveredAddress == address(0) || recoveredAddress != owner) + revert InvalidSigner(recoveredAddress); + + _approve(recoveredAddress, spender, value); + } + } - if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress); + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return + block.chainid == INITIAL_CHAIN_ID + ? INITIAL_DOMAIN_SEPARATOR + : computeDomainSeparator(); + } - _approve(recoveredAddress, spender, value); + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(name())), + keccak256("1"), + block.chainid, + address(this) + ) + ); } - } - - function DOMAIN_SEPARATOR() public view returns (bytes32) { - return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); - } - - function computeDomainSeparator() internal view virtual returns (bytes32) { - return - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name())), - keccak256("1"), - block.chainid, - address(this) - ) - ); - } } From d4428ef9587e197c11847e280cbc34ec67642674 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 11:34:26 +0200 Subject: [PATCH 07/18] round assets affected by withdrawalFee down (beefy) --- src/vault/adapter/beefy/BeefyAdapter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vault/adapter/beefy/BeefyAdapter.sol b/src/vault/adapter/beefy/BeefyAdapter.sol index ed9a48c..9c4c957 100644 --- a/src/vault/adapter/beefy/BeefyAdapter.sol +++ b/src/vault/adapter/beefy/BeefyAdapter.sol @@ -113,7 +113,7 @@ contract BeefyAdapter is AdapterBase, WithRewards { beefyFee = strat.withdrawFee(); } - if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR, BPS_DENOMINATOR - beefyFee, Math.Rounding.Up); + if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR, BPS_DENOMINATOR - beefyFee, Math.Rounding.Down); return _convertToShares(assets, Math.Rounding.Up); } @@ -131,7 +131,7 @@ contract BeefyAdapter is AdapterBase, WithRewards { beefyFee = strat.withdrawFee(); } - if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR - beefyFee, BPS_DENOMINATOR, Math.Rounding.Up); + if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR - beefyFee, BPS_DENOMINATOR, Math.Rounding.Down); return assets; } From e0a3275615c9b1896656d1283d177f2aee385eba Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 11:50:29 +0200 Subject: [PATCH 08/18] only charge manangementFees from first deposit/mint --- src/vault/Vault.sol | 7 ++++++- test/vault/Vault.t.sol | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index 0f89519..fc020cb 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -98,7 +98,6 @@ contract Vault is abi.encodePacked("Popcorn", name(), block.timestamp, "Vault") ); - feesUpdatedAt = block.timestamp; highWaterMark = 1e9; quitPeriod = 3 days; depositLimit = depositLimit_; @@ -168,6 +167,9 @@ contract Vault is if (receiver == address(0)) revert InvalidReceiver(); if (assets > maxDeposit(receiver)) revert MaxError(assets); + // Inititalize account for managementFee on first deposit + if (totalSupply() == 0) feesUpdatedAt = block.timestamp; + uint256 feeShares = _convertToShares( assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), Math.Rounding.Down @@ -204,6 +206,9 @@ contract Vault is if (receiver == address(0)) revert InvalidReceiver(); if (shares == 0) revert ZeroAmount(); + // Inititalize account for managementFee on first deposit + if (totalSupply() == 0) feesUpdatedAt = block.timestamp; + uint256 depositFee = uint256(fees.deposit); uint256 feeShares = shares.mulDiv( diff --git a/test/vault/Vault.t.sol b/test/vault/Vault.t.sol index 23f0510..6fc0a6b 100644 --- a/test/vault/Vault.t.sol +++ b/test/vault/Vault.t.sol @@ -130,7 +130,6 @@ contract VaultTest is Test { assertEq(performance, 100); assertEq(newVault.feeRecipient(), feeRecipient); assertEq(newVault.highWaterMark(), 1e9); - assertEq(newVault.feesUpdatedAt(), callTime); assertEq(newVault.quitPeriod(), 3 days); assertEq(asset.allowance(address(newVault), address(adapter)), type(uint256).max); From 0cc913d2b3714fd7c551f2f6306a1e26c41cf15c Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 11:50:41 +0200 Subject: [PATCH 09/18] qa - updated incorrect comments --- src/vault/DeploymentController.sol | 2 +- src/vault/VaultController.sol | 4 ++-- src/vault/adapter/convex/ConvexAdapter.sol | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vault/DeploymentController.sol b/src/vault/DeploymentController.sol index 4373d08..108f0de 100644 --- a/src/vault/DeploymentController.sol +++ b/src/vault/DeploymentController.sol @@ -57,7 +57,7 @@ contract DeploymentController is Owned { } /** - * @notice Adds a new category for templates. Caller must be owner. (`VaultController` via `AdminProxy`) + * @notice Adds a new category for templates. * @param templateCategory Category of the new template. * @param templateId Unique Id of the new template. * @param template New template (See ITemplateRegistry for more details) diff --git a/src/vault/VaultController.sol b/src/vault/VaultController.sol index e21267a..373558d 100644 --- a/src/vault/VaultController.sol +++ b/src/vault/VaultController.sol @@ -680,7 +680,7 @@ contract VaultController is Owned { PAUSING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Pause Deposits and withdraw all funds from the underlying protocol. Caller must be owner or creator of the Vault. + /// @notice Pause Deposits and withdraw all funds from the underlying protocol. Caller must be owner. function pauseAdapters(address[] calldata vaults) external onlyOwner { uint8 len = uint8(vaults.length); for (uint256 i = 0; i < len; i++) { @@ -692,7 +692,7 @@ contract VaultController is Owned { } } - /// @notice Unpause Deposits and deposit all funds into the underlying protocol. Caller must be owner or creator of the Vault. + /// @notice Unpause Deposits and deposit all funds into the underlying protocol. Caller must be owner. function unpauseAdapters(address[] calldata vaults) external onlyOwner { uint8 len = uint8(vaults.length); for (uint256 i = 0; i < len; i++) { diff --git a/src/vault/adapter/convex/ConvexAdapter.sol b/src/vault/adapter/convex/ConvexAdapter.sol index 344f565..4e8f36a 100644 --- a/src/vault/adapter/convex/ConvexAdapter.sol +++ b/src/vault/adapter/convex/ConvexAdapter.sol @@ -12,7 +12,7 @@ import { IConvexBooster, IConvexRewards, IRewards } from "./IConvex.sol"; * @author amatureApe * @notice ERC4626 wrapper for Convex Vaults. * - * An ERC4626 compliant Wrapper for https://github.com/beefyfinance/beefy-contracts/blob/master/contracts/BIFI/vaults/BeefyVaultV6.sol. + * An ERC4626 compliant Wrapper for https://github.com/convex-eth/platform/blob/main/contracts/contracts/Booster.sol. * Allows wrapping Convex Vaults with or without an active convexBooster. * Allows for additional strategies to use rewardsToken in case of an active convexBooster. */ @@ -84,7 +84,7 @@ contract ConvexAdapter is AdapterBase, WithRewards { return convexRewards.balanceOf(address(this)); } - /// @notice The token rewarded if the aave liquidity mining is active + /// @notice The token rewarded from the convex reward contract function rewardTokens() external view override returns (address[] memory) { uint256 len = convexRewards.extraRewardsLength(); From 41fa715246b347feec2457c1d28eea11670e3ed7 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 11:58:08 +0200 Subject: [PATCH 10/18] moved UnderlyingError into AdminProxy --- src/vault/AdminProxy.sol | 22 +- src/vault/VaultController.sol | 1845 ++++++++++++++++++--------------- 2 files changed, 1003 insertions(+), 864 deletions(-) diff --git a/src/vault/AdminProxy.sol b/src/vault/AdminProxy.sol index 02d5088..18b7905 100644 --- a/src/vault/AdminProxy.sol +++ b/src/vault/AdminProxy.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.15; -import { Owned } from "../utils/Owned.sol"; +import {Owned} from "../utils/Owned.sol"; /** * @title AdminProxy @@ -13,14 +13,16 @@ import { Owned } from "../utils/Owned.sol"; * AdminProxy is controlled by VaultController. VaultController executes management functions on other contracts through `execute()` */ contract AdminProxy is Owned { - constructor(address _owner) Owned(_owner) {} + constructor(address _owner) Owned(_owner) {} - /// @notice Execute arbitrary management functions. - function execute(address target, bytes calldata callData) - external - onlyOwner - returns (bool success, bytes memory returndata) - { - return target.call(callData); - } + error UnderlyingError(bytes revertReason); + + /// @notice Execute arbitrary management functions. + function execute( + address target, + bytes calldata callData + ) external onlyOwner returns (bool success, bytes memory returnData) { + (success, returnData) = target.call(callData); + if (!success) revert UnderlyingError(returnData); + } } diff --git a/src/vault/VaultController.sol b/src/vault/VaultController.sol index 373558d..2974712 100644 --- a/src/vault/VaultController.sol +++ b/src/vault/VaultController.sol @@ -2,20 +2,20 @@ // Docgen-SOLC: 0.8.15 pragma solidity ^0.8.15; -import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import { Owned } from "../utils/Owned.sol"; -import { IVault, VaultInitParams, VaultFees, IERC4626, IERC20 } from "../interfaces/vault/IVault.sol"; -import { IMultiRewardStaking } from "../interfaces/IMultiRewardStaking.sol"; -import { IMultiRewardEscrow } from "../interfaces/IMultiRewardEscrow.sol"; -import { IDeploymentController, ICloneRegistry } from "../interfaces/vault/IDeploymentController.sol"; -import { ITemplateRegistry, Template } from "../interfaces/vault/ITemplateRegistry.sol"; -import { IPermissionRegistry, Permission } from "../interfaces/vault/IPermissionRegistry.sol"; -import { IVaultRegistry, VaultMetadata } from "../interfaces/vault/IVaultRegistry.sol"; -import { IAdminProxy } from "../interfaces/vault/IAdminProxy.sol"; -import { IStrategy } from "../interfaces/vault/IStrategy.sol"; -import { IAdapter } from "../interfaces/vault/IAdapter.sol"; -import { IPausable } from "../interfaces/IPausable.sol"; -import { DeploymentArgs } from "../interfaces/vault/IVaultController.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {Owned} from "../utils/Owned.sol"; +import {IVault, VaultInitParams, VaultFees, IERC4626, IERC20} from "../interfaces/vault/IVault.sol"; +import {IMultiRewardStaking} from "../interfaces/IMultiRewardStaking.sol"; +import {IMultiRewardEscrow} from "../interfaces/IMultiRewardEscrow.sol"; +import {IDeploymentController, ICloneRegistry} from "../interfaces/vault/IDeploymentController.sol"; +import {ITemplateRegistry, Template} from "../interfaces/vault/ITemplateRegistry.sol"; +import {IPermissionRegistry, Permission} from "../interfaces/vault/IPermissionRegistry.sol"; +import {IVaultRegistry, VaultMetadata} from "../interfaces/vault/IVaultRegistry.sol"; +import {IAdminProxy} from "../interfaces/vault/IAdminProxy.sol"; +import {IStrategy} from "../interfaces/vault/IStrategy.sol"; +import {IAdapter} from "../interfaces/vault/IAdapter.sol"; +import {IPausable} from "../interfaces/IPausable.sol"; +import {DeploymentArgs} from "../interfaces/vault/IVaultController.sol"; /** * @title VaultController @@ -26,932 +26,1069 @@ import { DeploymentArgs } from "../interfaces/vault/IVaultController.sol"; * Calls admin functions on deployed contracts. */ contract VaultController is Owned { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// IMMUTABLES //////////////////////////////////////////////////////////////*/ - bytes32 public immutable VAULT = "Vault"; - bytes32 public immutable ADAPTER = "Adapter"; - bytes32 public immutable STRATEGY = "Strategy"; - bytes32 public immutable STAKING = "Staking"; - bytes4 internal immutable DEPLOY_SIG = bytes4(keccak256("deploy(bytes32,bytes32,bytes)")); - - error UnderlyingError(bytes revertReason); - - /** - * @notice Constructor of this contract. - * @param _owner Owner of the contract. Controls management functions. - * @param _adminProxy `AdminProxy` ownes contracts in the vault ecosystem. - * @param _deploymentController `DeploymentController` with auxiliary deployment contracts. - * @param _vaultRegistry `VaultRegistry` to safe vault metadata. - * @param _permissionRegistry `permissionRegistry` to add endorsements and rejections. - * @param _escrow `MultiRewardEscrow` To escrow rewards of staking contracts. - */ - constructor( - address _owner, - IAdminProxy _adminProxy, - IDeploymentController _deploymentController, - IVaultRegistry _vaultRegistry, - IPermissionRegistry _permissionRegistry, - IMultiRewardEscrow _escrow - ) Owned(_owner) { - adminProxy = _adminProxy; - vaultRegistry = _vaultRegistry; - permissionRegistry = _permissionRegistry; - escrow = _escrow; - - _setDeploymentController(_deploymentController); - - activeTemplateId[STAKING] = "MultiRewardStaking"; - activeTemplateId[VAULT] = "V1"; - } - - /*////////////////////////////////////////////////////////////// + bytes32 public immutable VAULT = "Vault"; + bytes32 public immutable ADAPTER = "Adapter"; + bytes32 public immutable STRATEGY = "Strategy"; + bytes32 public immutable STAKING = "Staking"; + bytes4 internal immutable DEPLOY_SIG = + bytes4(keccak256("deploy(bytes32,bytes32,bytes)")); + + /** + * @notice Constructor of this contract. + * @param _owner Owner of the contract. Controls management functions. + * @param _adminProxy `AdminProxy` ownes contracts in the vault ecosystem. + * @param _deploymentController `DeploymentController` with auxiliary deployment contracts. + * @param _vaultRegistry `VaultRegistry` to safe vault metadata. + * @param _permissionRegistry `permissionRegistry` to add endorsements and rejections. + * @param _escrow `MultiRewardEscrow` To escrow rewards of staking contracts. + */ + constructor( + address _owner, + IAdminProxy _adminProxy, + IDeploymentController _deploymentController, + IVaultRegistry _vaultRegistry, + IPermissionRegistry _permissionRegistry, + IMultiRewardEscrow _escrow + ) Owned(_owner) { + adminProxy = _adminProxy; + vaultRegistry = _vaultRegistry; + permissionRegistry = _permissionRegistry; + escrow = _escrow; + + _setDeploymentController(_deploymentController); + + activeTemplateId[STAKING] = "MultiRewardStaking"; + activeTemplateId[VAULT] = "V1"; + } + + /*////////////////////////////////////////////////////////////// VAULT DEPLOYMENT LOGIC //////////////////////////////////////////////////////////////*/ - event VaultDeployed(address indexed vault, address indexed staking, address indexed adapter); - - error InvalidConfig(); - - /** - * @notice Deploy a new Vault. Optionally with an Adapter and Staking. Caller must be owner. - * @param vaultData Vault init params. - * @param adapterData Encoded adapter init data. - * @param strategyData Encoded strategy init data. - * @param deployStaking Should we deploy a staking contract for the vault? - * @param rewardsData Encoded data to add a rewards to the staking contract - * @param metadata Vault metadata for the `VaultRegistry` (Will be used by the frontend for additional informations) - * @param initialDeposit Initial deposit to the vault. If 0, no deposit will be made. - * @dev This function is the one stop solution to create a new vault with all necessary admin functions or auxiliery contracts. - * @dev If `rewardsData` is not empty `deployStaking` must be true - */ - function deployVault( - VaultInitParams memory vaultData, - DeploymentArgs memory adapterData, - DeploymentArgs memory strategyData, - bool deployStaking, - bytes memory rewardsData, - VaultMetadata memory metadata, - uint256 initialDeposit - ) external canCreate returns (address vault) { - IDeploymentController _deploymentController = deploymentController; - - _verifyToken(address(vaultData.asset)); - if ( - address(vaultData.adapter) != address(0) && - (adapterData.id > 0 || !cloneRegistry.cloneExists(address(vaultData.adapter))) - ) revert InvalidConfig(); - - if (adapterData.id > 0) - vaultData.adapter = IERC4626(_deployAdapter(vaultData.asset, adapterData, strategyData, _deploymentController)); - - vault = _deployVault(vaultData, _deploymentController); - - address staking; - if (deployStaking) staking = _deployStaking(IERC20(address(vault)), _deploymentController); - - _registerCreatedVault(vault, staking, metadata); - - if (rewardsData.length > 0) { - if (!deployStaking) revert InvalidConfig(); - _handleVaultStakingRewards(vault, rewardsData); - } - - emit VaultDeployed(vault, staking, address(vaultData.adapter)); - - _handleInitialDeposit(initialDeposit, IERC20(vaultData.asset), IERC4626(vault)); - } - - /// @notice Deploys a new vault contract using the `activeTemplateId`. - function _deployVault(VaultInitParams memory vaultData, IDeploymentController _deploymentController) - internal - returns (address vault) - { - vaultData.owner = address(adminProxy); - - (bool success, bytes memory returnData) = adminProxy.execute( - address(_deploymentController), - abi.encodeWithSelector( - DEPLOY_SIG, - VAULT, - activeTemplateId[VAULT], - abi.encodeWithSelector(IVault.initialize.selector, vaultData) - ) + event VaultDeployed( + address indexed vault, + address indexed staking, + address indexed adapter ); - if (!success) revert UnderlyingError(returnData); - - vault = abi.decode(returnData, (address)); - } - - /// @notice Registers newly created vault metadata. - function _registerCreatedVault( - address vault, - address staking, - VaultMetadata memory metadata - ) internal { - metadata.vault = vault; - metadata.staking = staking; - metadata.creator = msg.sender; - - _registerVault(vault, metadata); - } - - /// @notice Prepares and calls `addStakingRewardsTokens` for the newly created staking contract. - function _handleVaultStakingRewards(address vault, bytes memory rewardsData) internal { - address[] memory vaultContracts = new address[](1); - bytes[] memory rewardsDatas = new bytes[](1); - - vaultContracts[0] = vault; - rewardsDatas[0] = rewardsData; - - addStakingRewardsTokens(vaultContracts, rewardsDatas); - } - - function _handleInitialDeposit( - uint256 initialDeposit, - IERC20 asset, - IERC4626 target - ) internal { - if (initialDeposit > 0) { - asset.safeTransferFrom(msg.sender, address(this), initialDeposit); - asset.approve(address(target), initialDeposit); - target.deposit(initialDeposit, msg.sender); - } - } - - /*////////////////////////////////////////////////////////////// + + error InvalidConfig(); + + /** + * @notice Deploy a new Vault. Optionally with an Adapter and Staking. Caller must be owner. + * @param vaultData Vault init params. + * @param adapterData Encoded adapter init data. + * @param strategyData Encoded strategy init data. + * @param deployStaking Should we deploy a staking contract for the vault? + * @param rewardsData Encoded data to add a rewards to the staking contract + * @param metadata Vault metadata for the `VaultRegistry` (Will be used by the frontend for additional informations) + * @param initialDeposit Initial deposit to the vault. If 0, no deposit will be made. + * @dev This function is the one stop solution to create a new vault with all necessary admin functions or auxiliery contracts. + * @dev If `rewardsData` is not empty `deployStaking` must be true + */ + function deployVault( + VaultInitParams memory vaultData, + DeploymentArgs memory adapterData, + DeploymentArgs memory strategyData, + bool deployStaking, + bytes memory rewardsData, + VaultMetadata memory metadata, + uint256 initialDeposit + ) external canCreate returns (address vault) { + IDeploymentController _deploymentController = deploymentController; + + _verifyToken(address(vaultData.asset)); + if ( + address(vaultData.adapter) != address(0) && + (adapterData.id > 0 || + !cloneRegistry.cloneExists(address(vaultData.adapter))) + ) revert InvalidConfig(); + + if (adapterData.id > 0) + vaultData.adapter = IERC4626( + _deployAdapter( + vaultData.asset, + adapterData, + strategyData, + _deploymentController + ) + ); + + vault = _deployVault(vaultData, _deploymentController); + + address staking; + if (deployStaking) + staking = _deployStaking( + IERC20(address(vault)), + _deploymentController + ); + + _registerCreatedVault(vault, staking, metadata); + + if (rewardsData.length > 0) { + if (!deployStaking) revert InvalidConfig(); + _handleVaultStakingRewards(vault, rewardsData); + } + + emit VaultDeployed(vault, staking, address(vaultData.adapter)); + + _handleInitialDeposit( + initialDeposit, + IERC20(vaultData.asset), + IERC4626(vault) + ); + } + + /// @notice Deploys a new vault contract using the `activeTemplateId`. + function _deployVault( + VaultInitParams memory vaultData, + IDeploymentController _deploymentController + ) internal returns (address vault) { + vaultData.owner = address(adminProxy); + + (, bytes memory returnData) = adminProxy.execute( + address(_deploymentController), + abi.encodeWithSelector( + DEPLOY_SIG, + VAULT, + activeTemplateId[VAULT], + abi.encodeWithSelector(IVault.initialize.selector, vaultData) + ) + ); + + vault = abi.decode(returnData, (address)); + } + + /// @notice Registers newly created vault metadata. + function _registerCreatedVault( + address vault, + address staking, + VaultMetadata memory metadata + ) internal { + metadata.vault = vault; + metadata.staking = staking; + metadata.creator = msg.sender; + + _registerVault(vault, metadata); + } + + /// @notice Prepares and calls `addStakingRewardsTokens` for the newly created staking contract. + function _handleVaultStakingRewards( + address vault, + bytes memory rewardsData + ) internal { + address[] memory vaultContracts = new address[](1); + bytes[] memory rewardsDatas = new bytes[](1); + + vaultContracts[0] = vault; + rewardsDatas[0] = rewardsData; + + addStakingRewardsTokens(vaultContracts, rewardsDatas); + } + + function _handleInitialDeposit( + uint256 initialDeposit, + IERC20 asset, + IERC4626 target + ) internal { + if (initialDeposit > 0) { + asset.safeTransferFrom(msg.sender, address(this), initialDeposit); + asset.approve(address(target), initialDeposit); + target.deposit(initialDeposit, msg.sender); + } + } + + /*////////////////////////////////////////////////////////////// ADAPTER DEPLOYMENT LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @notice Deploy a new Adapter with our without a strategy. Caller must be owner. - * @param asset Asset which will be used by the adapter. - * @param adapterData Encoded adapter init data. - * @param strategyData Encoded strategy init data. - */ - function deployAdapter( - IERC20 asset, - DeploymentArgs memory adapterData, - DeploymentArgs memory strategyData, - uint256 initialDeposit - ) external canCreate returns (address adapter) { - _verifyToken(address(asset)); - - adapter = _deployAdapter(asset, adapterData, strategyData, deploymentController); - - _handleInitialDeposit(initialDeposit, asset, IERC4626(adapter)); - } - - /** - * @notice Deploys an adapter and optionally a strategy. - * @dev Adds the newly deployed strategy to the adapter. - */ - function _deployAdapter( - IERC20 asset, - DeploymentArgs memory adapterData, - DeploymentArgs memory strategyData, - IDeploymentController _deploymentController - ) internal returns (address) { - address strategy; - bytes4[8] memory requiredSigs; - if (strategyData.id > 0) { - strategy = _deployStrategy(strategyData, _deploymentController); - requiredSigs = templateRegistry.getTemplate(STRATEGY, strategyData.id).requiredSigs; - } - - return - __deployAdapter( - adapterData, - abi.encode(asset, address(adminProxy), IStrategy(strategy), harvestCooldown, requiredSigs, strategyData.data), - _deploymentController - ); - } - - /// @notice Deploys an adapter and sets the management fee via `AdminProxy` - function __deployAdapter( - DeploymentArgs memory adapterData, - bytes memory baseAdapterData, - IDeploymentController _deploymentController - ) internal returns (address adapter) { - (bool success, bytes memory returnData) = adminProxy.execute( - address(_deploymentController), - abi.encodeWithSelector(DEPLOY_SIG, ADAPTER, adapterData.id, _encodeAdapterData(adapterData, baseAdapterData)) - ); - if (!success) revert UnderlyingError(returnData); + /** + * @notice Deploy a new Adapter with our without a strategy. Caller must be owner. + * @param asset Asset which will be used by the adapter. + * @param adapterData Encoded adapter init data. + * @param strategyData Encoded strategy init data. + */ + function deployAdapter( + IERC20 asset, + DeploymentArgs memory adapterData, + DeploymentArgs memory strategyData, + uint256 initialDeposit + ) external canCreate returns (address adapter) { + _verifyToken(address(asset)); + + adapter = _deployAdapter( + asset, + adapterData, + strategyData, + deploymentController + ); + + _handleInitialDeposit(initialDeposit, asset, IERC4626(adapter)); + } + + /** + * @notice Deploys an adapter and optionally a strategy. + * @dev Adds the newly deployed strategy to the adapter. + */ + function _deployAdapter( + IERC20 asset, + DeploymentArgs memory adapterData, + DeploymentArgs memory strategyData, + IDeploymentController _deploymentController + ) internal returns (address) { + address strategy; + bytes4[8] memory requiredSigs; + if (strategyData.id > 0) { + strategy = _deployStrategy(strategyData, _deploymentController); + requiredSigs = templateRegistry + .getTemplate(STRATEGY, strategyData.id) + .requiredSigs; + } + + return + __deployAdapter( + adapterData, + abi.encode( + asset, + address(adminProxy), + IStrategy(strategy), + harvestCooldown, + requiredSigs, + strategyData.data + ), + _deploymentController + ); + } - adapter = abi.decode(returnData, (address)); + /// @notice Deploys an adapter and sets the management fee via `AdminProxy` + function __deployAdapter( + DeploymentArgs memory adapterData, + bytes memory baseAdapterData, + IDeploymentController _deploymentController + ) internal returns (address adapter) { + (, bytes memory returnData) = adminProxy.execute( + address(_deploymentController), + abi.encodeWithSelector( + DEPLOY_SIG, + ADAPTER, + adapterData.id, + _encodeAdapterData(adapterData, baseAdapterData) + ) + ); + + adapter = abi.decode(returnData, (address)); + + adminProxy.execute( + adapter, + abi.encodeWithSelector( + IAdapter.setPerformanceFee.selector, + performanceFee + ) + ); + } - (success, returnData) = adminProxy.execute( - adapter, - abi.encodeWithSelector(IAdapter.setPerformanceFee.selector, performanceFee) - ); - if (!success) revert UnderlyingError(returnData); - } - - /// @notice Encodes adapter init call. Was moved into its own function to fix "stack too deep" error. - function _encodeAdapterData(DeploymentArgs memory adapterData, bytes memory baseAdapterData) - internal - returns (bytes memory) - { - return - abi.encodeWithSelector( - IAdapter.initialize.selector, - baseAdapterData, - templateRegistry.getTemplate(ADAPTER, adapterData.id).registry, - adapterData.data - ); - } - - /// @notice Deploys a new strategy contract. - function _deployStrategy(DeploymentArgs memory strategyData, IDeploymentController _deploymentController) - internal - returns (address strategy) - { - (bool success, bytes memory returnData) = adminProxy.execute( - address(_deploymentController), - abi.encodeWithSelector(DEPLOY_SIG, STRATEGY, strategyData.id, "") - ); - if (!success) revert UnderlyingError(returnData); + /// @notice Encodes adapter init call. Was moved into its own function to fix "stack too deep" error. + function _encodeAdapterData( + DeploymentArgs memory adapterData, + bytes memory baseAdapterData + ) internal returns (bytes memory) { + return + abi.encodeWithSelector( + IAdapter.initialize.selector, + baseAdapterData, + templateRegistry.getTemplate(ADAPTER, adapterData.id).registry, + adapterData.data + ); + } - strategy = abi.decode(returnData, (address)); - } + /// @notice Deploys a new strategy contract. + function _deployStrategy( + DeploymentArgs memory strategyData, + IDeploymentController _deploymentController + ) internal returns (address strategy) { + (, bytes memory returnData) = adminProxy.execute( + address(_deploymentController), + abi.encodeWithSelector(DEPLOY_SIG, STRATEGY, strategyData.id, "") + ); + + strategy = abi.decode(returnData, (address)); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STAKING DEPLOYMENT LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @notice Deploy a new staking contract. Caller must be owner. - * @param asset The staking token for the new contract. - * @dev Deploys `MultiRewardsStaking` based on the latest templateTemplateKey. - */ - function deployStaking(IERC20 asset) external canCreate returns (address) { - _verifyToken(address(asset)); - return _deployStaking(asset, deploymentController); - } - - /// @notice Deploys a new staking contract using the activeTemplateId. - function _deployStaking(IERC20 asset, IDeploymentController _deploymentController) - internal - returns (address staking) - { - (bool success, bytes memory returnData) = adminProxy.execute( - address(_deploymentController), - abi.encodeWithSelector( - DEPLOY_SIG, - STAKING, - activeTemplateId[STAKING], - abi.encodeWithSelector(IMultiRewardStaking.initialize.selector, asset, escrow, adminProxy) - ) - ); - if (!success) revert UnderlyingError(returnData); + /** + * @notice Deploy a new staking contract. Caller must be owner. + * @param asset The staking token for the new contract. + * @dev Deploys `MultiRewardsStaking` based on the latest templateTemplateKey. + */ + function deployStaking(IERC20 asset) external canCreate returns (address) { + _verifyToken(address(asset)); + return _deployStaking(asset, deploymentController); + } - staking = abi.decode(returnData, (address)); - } + /// @notice Deploys a new staking contract using the activeTemplateId. + function _deployStaking( + IERC20 asset, + IDeploymentController _deploymentController + ) internal returns (address staking) { + (, bytes memory returnData) = adminProxy.execute( + address(_deploymentController), + abi.encodeWithSelector( + DEPLOY_SIG, + STAKING, + activeTemplateId[STAKING], + abi.encodeWithSelector( + IMultiRewardStaking.initialize.selector, + asset, + escrow, + adminProxy + ) + ) + ); + + staking = abi.decode(returnData, (address)); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// VAULT MANAGEMENT LOGIC //////////////////////////////////////////////////////////////*/ - error DoesntExist(address adapter); - - /** - * @notice Propose a new Adapter. Caller must be creator of the vaults. - * @param vaults Vaults to propose the new adapter for. - * @param newAdapter New adapters to propose. - */ - function proposeVaultAdapters(address[] calldata vaults, IERC4626[] calldata newAdapter) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, newAdapter.length); - - ICloneRegistry _cloneRegistry = cloneRegistry; - for (uint8 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - if (!_cloneRegistry.cloneExists(address(newAdapter[i]))) revert DoesntExist(address(newAdapter[i])); - - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.proposeAdapter.selector, newAdapter[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Change adapter of a vault to the previously proposed adapter. - * @param vaults Addresses of the vaults to change - */ - function changeVaultAdapters(address[] calldata vaults) external { - uint8 len = uint8(vaults.length); - for (uint8 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.changeAdapter.selector) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Sets new fees per vault. Caller must be creator of the vaults. - * @param vaults Addresses of the vaults to change - * @param fees New fee structures for these vaults - * @dev Value is in 1e18, e.g. 100% = 1e18 - 1 BPS = 1e12 - */ - function proposeVaultFees(address[] calldata vaults, VaultFees[] calldata fees) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, fees.length); - - for (uint8 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.proposeFees.selector, fees[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Change adapter of a vault to the previously proposed adapter. - * @param vaults Addresses of the vaults - */ - function changeVaultFees(address[] calldata vaults) external { - uint8 len = uint8(vaults.length); - for (uint8 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.changeFees.selector) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Sets new Quit Periods for Vaults. Caller must be creator of the vaults. - * @param vaults Addresses of the vaults to change - * @param quitPeriods QuitPeriod in seconds - * @dev Minimum value is 1 day max is 7 days. - * @dev Cant be called if recently a new fee or adapter has been proposed - */ - function setVaultQuitPeriods(address[] calldata vaults, uint256[] calldata quitPeriods) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, quitPeriods.length); - - for (uint8 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.setQuitPeriod.selector, quitPeriods[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Sets new Fee Recipients for Vaults. Caller must be creator of the vaults. - * @param vaults Addresses of the vaults to change - * @param feeRecipients fee recipient for this vault - * @dev address must not be 0 - */ - function setVaultFeeRecipients(address[] calldata vaults, address[] calldata feeRecipients) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, feeRecipients.length); - - for (uint8 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.setFeeRecipient.selector, feeRecipients[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Sets new DepositLimit for Vaults. Caller must be creator of the vaults. - * @param vaults Addresses of the vaults to change - * @param depositLimits Maximum amount of assets that can be deposited. - */ - function setVaultDepositLimits(address[] calldata vaults, uint256[] calldata depositLimits) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, depositLimits.length); - - for (uint8 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IVault.setDepositLimit.selector, depositLimits[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /*////////////////////////////////////////////////////////////// + error DoesntExist(address adapter); + + /** + * @notice Propose a new Adapter. Caller must be creator of the vaults. + * @param vaults Vaults to propose the new adapter for. + * @param newAdapter New adapters to propose. + */ + function proposeVaultAdapters( + address[] calldata vaults, + IERC4626[] calldata newAdapter + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, newAdapter.length); + + ICloneRegistry _cloneRegistry = cloneRegistry; + for (uint8 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + if (!_cloneRegistry.cloneExists(address(newAdapter[i]))) + revert DoesntExist(address(newAdapter[i])); + + adminProxy.execute( + vaults[i], + abi.encodeWithSelector( + IVault.proposeAdapter.selector, + newAdapter[i] + ) + ); + } + } + + /** + * @notice Change adapter of a vault to the previously proposed adapter. + * @param vaults Addresses of the vaults to change + */ + function changeVaultAdapters(address[] calldata vaults) external { + uint8 len = uint8(vaults.length); + for (uint8 i = 0; i < len; i++) { + adminProxy.execute( + vaults[i], + abi.encodeWithSelector(IVault.changeAdapter.selector) + ); + } + } + + /** + * @notice Sets new fees per vault. Caller must be creator of the vaults. + * @param vaults Addresses of the vaults to change + * @param fees New fee structures for these vaults + * @dev Value is in 1e18, e.g. 100% = 1e18 - 1 BPS = 1e12 + */ + function proposeVaultFees( + address[] calldata vaults, + VaultFees[] calldata fees + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, fees.length); + + for (uint8 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + + adminProxy.execute( + vaults[i], + abi.encodeWithSelector(IVault.proposeFees.selector, fees[i]) + ); + } + } + + /** + * @notice Change adapter of a vault to the previously proposed adapter. + * @param vaults Addresses of the vaults + */ + function changeVaultFees(address[] calldata vaults) external { + uint8 len = uint8(vaults.length); + for (uint8 i = 0; i < len; i++) { + adminProxy.execute( + vaults[i], + abi.encodeWithSelector(IVault.changeFees.selector) + ); + } + } + + /** + * @notice Sets new Quit Periods for Vaults. Caller must be creator of the vaults. + * @param vaults Addresses of the vaults to change + * @param quitPeriods QuitPeriod in seconds + * @dev Minimum value is 1 day max is 7 days. + * @dev Cant be called if recently a new fee or adapter has been proposed + */ + function setVaultQuitPeriods( + address[] calldata vaults, + uint256[] calldata quitPeriods + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, quitPeriods.length); + + for (uint8 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + + adminProxy.execute( + vaults[i], + abi.encodeWithSelector( + IVault.setQuitPeriod.selector, + quitPeriods[i] + ) + ); + } + } + + /** + * @notice Sets new Fee Recipients for Vaults. Caller must be creator of the vaults. + * @param vaults Addresses of the vaults to change + * @param feeRecipients fee recipient for this vault + * @dev address must not be 0 + */ + function setVaultFeeRecipients( + address[] calldata vaults, + address[] calldata feeRecipients + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, feeRecipients.length); + + for (uint8 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + + adminProxy.execute( + vaults[i], + abi.encodeWithSelector( + IVault.setFeeRecipient.selector, + feeRecipients[i] + ) + ); + } + } + + /** + * @notice Sets new DepositLimit for Vaults. Caller must be creator of the vaults. + * @param vaults Addresses of the vaults to change + * @param depositLimits Maximum amount of assets that can be deposited. + */ + function setVaultDepositLimits( + address[] calldata vaults, + uint256[] calldata depositLimits + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, depositLimits.length); + + for (uint8 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + + adminProxy.execute( + vaults[i], + abi.encodeWithSelector( + IVault.setDepositLimit.selector, + depositLimits[i] + ) + ); + } + } + + /*////////////////////////////////////////////////////////////// REGISTER VAULT //////////////////////////////////////////////////////////////*/ - IVaultRegistry public vaultRegistry; - - /// @notice Call the `VaultRegistry` to register a vault via `AdminProxy` - function _registerVault(address vault, VaultMetadata memory metadata) internal { - (bool success, bytes memory returnData) = adminProxy.execute( - address(vaultRegistry), - abi.encodeWithSelector(IVaultRegistry.registerVault.selector, metadata) - ); - if (!success) revert UnderlyingError(returnData); - } + IVaultRegistry public vaultRegistry; + + /// @notice Call the `VaultRegistry` to register a vault via `AdminProxy` + function _registerVault( + address vault, + VaultMetadata memory metadata + ) internal { + adminProxy.execute( + address(vaultRegistry), + abi.encodeWithSelector( + IVaultRegistry.registerVault.selector, + metadata + ) + ); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ENDORSEMENT / REJECTION LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @notice Set permissions for an array of target. Caller must be owner. - * @param targets `AdminProxy` - * @param newPermissions An array of permissions to set for the targets. - * @dev See `PermissionRegistry` for more details - */ - function setPermissions(address[] calldata targets, Permission[] calldata newPermissions) external onlyOwner { - // No need to check matching array length since its already done in the permissionRegistry - (bool success, bytes memory returnData) = adminProxy.execute( - address(permissionRegistry), - abi.encodeWithSelector(IPermissionRegistry.setPermissions.selector, targets, newPermissions) - ); - if (!success) revert UnderlyingError(returnData); - } + /** + * @notice Set permissions for an array of target. Caller must be owner. + * @param targets `AdminProxy` + * @param newPermissions An array of permissions to set for the targets. + * @dev See `PermissionRegistry` for more details + */ + function setPermissions( + address[] calldata targets, + Permission[] calldata newPermissions + ) external onlyOwner { + // No need to check matching array length since its already done in the permissionRegistry + adminProxy.execute( + address(permissionRegistry), + abi.encodeWithSelector( + IPermissionRegistry.setPermissions.selector, + targets, + newPermissions + ) + ); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STAKING MANAGEMENT LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @notice Adds a new rewardToken which can be earned via staking. Caller must be creator of the Vault or owner. - * @param vaults Vaults of which the staking contracts should be targeted - * @param rewardTokenData Token that can be earned by staking. - * @dev `rewardToken` - Token that can be earned by staking. - * @dev `rewardsPerSecond` - The rate in which `rewardToken` will be accrued. - * @dev `amount` - Initial funding amount for this reward. - * @dev `useEscrow Bool` - if the rewards should be escrowed on claim. - * @dev `escrowPercentage` - The percentage of the reward that gets escrowed in 1e18. (1e18 = 100%, 1e14 = 1 BPS) - * @dev `escrowDuration` - The duration of the escrow. - * @dev `offset` - A cliff after claim before the escrow starts. - * @dev See `MultiRewardsStaking` for more details. - */ - function addStakingRewardsTokens(address[] memory vaults, bytes[] memory rewardTokenData) public { - _verifyEqualArrayLength(vaults.length, rewardTokenData.length); - address staking; - uint8 len = uint8(vaults.length); - for (uint256 i = 0; i < len; i++) { - ( - address rewardsToken, - uint160 rewardsPerSecond, - uint256 amount, - bool useEscrow, - uint224 escrowDuration, - uint24 escrowPercentage, - uint256 offset - ) = abi.decode(rewardTokenData[i], (address, uint160, uint256, bool, uint224, uint24, uint256)); - _verifyToken(rewardsToken); - staking = _verifyCreatorOrOwner(vaults[i]).staking; - - (bool success, bytes memory returnData) = adminProxy.execute( - rewardsToken, - abi.encodeWithSelector(IERC20.approve.selector, staking, type(uint256).max) - ); - if (!success) revert UnderlyingError(returnData); - - IERC20(rewardsToken).approve(staking, type(uint256).max); - IERC20(rewardsToken).transferFrom(msg.sender, address(adminProxy), amount); - - (success, returnData) = adminProxy.execute( - staking, - abi.encodeWithSelector( - IMultiRewardStaking.addRewardToken.selector, - rewardsToken, - rewardsPerSecond, - amount, - useEscrow, - escrowDuration, - escrowPercentage, - offset - ) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Changes rewards speed for a rewardToken. This works only for rewards that accrue over time. Caller must be creator of the Vault. - * @param vaults Vaults of which the staking contracts should be targeted - * @param rewardTokens Token that can be earned by staking. - * @param rewardsSpeeds The rate in which `rewardToken` will be accrued. - * @dev See `MultiRewardsStaking` for more details. - */ - function changeStakingRewardsSpeeds( - address[] calldata vaults, - IERC20[] calldata rewardTokens, - uint160[] calldata rewardsSpeeds - ) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, rewardTokens.length); - _verifyEqualArrayLength(len, rewardsSpeeds.length); - - address staking; - for (uint256 i = 0; i < len; i++) { - staking = _verifyCreator(vaults[i]).staking; - - (bool success, bytes memory returnData) = adminProxy.execute( - staking, - abi.encodeWithSelector(IMultiRewardStaking.changeRewardSpeed.selector, rewardTokens[i], rewardsSpeeds[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Funds rewards for a rewardToken. - * @param vaults Vaults of which the staking contracts should be targeted - * @param rewardTokens Token that can be earned by staking. - * @param amounts The amount of rewardToken that will fund this reward. - * @dev See `MultiRewardStaking` for more details. - */ - function fundStakingRewards( - address[] calldata vaults, - IERC20[] calldata rewardTokens, - uint256[] calldata amounts - ) external { - uint8 len = uint8(vaults.length); - - _verifyEqualArrayLength(len, rewardTokens.length); - _verifyEqualArrayLength(len, amounts.length); - - address staking; - for (uint256 i = 0; i < len; i++) { - staking = vaultRegistry.getVault(vaults[i]).staking; - - rewardTokens[i].transferFrom(msg.sender, address(this), amounts[i]); - IMultiRewardStaking(staking).fundReward(rewardTokens[i], amounts[i]); - } - } - - /*////////////////////////////////////////////////////////////// + /** + * @notice Adds a new rewardToken which can be earned via staking. Caller must be creator of the Vault or owner. + * @param vaults Vaults of which the staking contracts should be targeted + * @param rewardTokenData Token that can be earned by staking. + * @dev `rewardToken` - Token that can be earned by staking. + * @dev `rewardsPerSecond` - The rate in which `rewardToken` will be accrued. + * @dev `amount` - Initial funding amount for this reward. + * @dev `useEscrow Bool` - if the rewards should be escrowed on claim. + * @dev `escrowPercentage` - The percentage of the reward that gets escrowed in 1e18. (1e18 = 100%, 1e14 = 1 BPS) + * @dev `escrowDuration` - The duration of the escrow. + * @dev `offset` - A cliff after claim before the escrow starts. + * @dev See `MultiRewardsStaking` for more details. + */ + function addStakingRewardsTokens( + address[] memory vaults, + bytes[] memory rewardTokenData + ) public { + _verifyEqualArrayLength(vaults.length, rewardTokenData.length); + address staking; + uint8 len = uint8(vaults.length); + for (uint256 i = 0; i < len; i++) { + ( + address rewardsToken, + uint160 rewardsPerSecond, + uint256 amount, + bool useEscrow, + uint224 escrowDuration, + uint24 escrowPercentage, + uint256 offset + ) = abi.decode( + rewardTokenData[i], + (address, uint160, uint256, bool, uint224, uint24, uint256) + ); + _verifyToken(rewardsToken); + staking = _verifyCreatorOrOwner(vaults[i]).staking; + + adminProxy.execute( + rewardsToken, + abi.encodeWithSelector( + IERC20.approve.selector, + staking, + type(uint256).max + ) + ); + + IERC20(rewardsToken).approve(staking, type(uint256).max); + IERC20(rewardsToken).transferFrom( + msg.sender, + address(adminProxy), + amount + ); + + adminProxy.execute( + staking, + abi.encodeWithSelector( + IMultiRewardStaking.addRewardToken.selector, + rewardsToken, + rewardsPerSecond, + amount, + useEscrow, + escrowDuration, + escrowPercentage, + offset + ) + ); + } + } + + /** + * @notice Changes rewards speed for a rewardToken. This works only for rewards that accrue over time. Caller must be creator of the Vault. + * @param vaults Vaults of which the staking contracts should be targeted + * @param rewardTokens Token that can be earned by staking. + * @param rewardsSpeeds The rate in which `rewardToken` will be accrued. + * @dev See `MultiRewardsStaking` for more details. + */ + function changeStakingRewardsSpeeds( + address[] calldata vaults, + IERC20[] calldata rewardTokens, + uint160[] calldata rewardsSpeeds + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, rewardTokens.length); + _verifyEqualArrayLength(len, rewardsSpeeds.length); + + address staking; + for (uint256 i = 0; i < len; i++) { + staking = _verifyCreator(vaults[i]).staking; + + adminProxy.execute( + staking, + abi.encodeWithSelector( + IMultiRewardStaking.changeRewardSpeed.selector, + rewardTokens[i], + rewardsSpeeds[i] + ) + ); + } + } + + /** + * @notice Funds rewards for a rewardToken. + * @param vaults Vaults of which the staking contracts should be targeted + * @param rewardTokens Token that can be earned by staking. + * @param amounts The amount of rewardToken that will fund this reward. + * @dev See `MultiRewardStaking` for more details. + */ + function fundStakingRewards( + address[] calldata vaults, + IERC20[] calldata rewardTokens, + uint256[] calldata amounts + ) external { + uint8 len = uint8(vaults.length); + + _verifyEqualArrayLength(len, rewardTokens.length); + _verifyEqualArrayLength(len, amounts.length); + + address staking; + for (uint256 i = 0; i < len; i++) { + staking = vaultRegistry.getVault(vaults[i]).staking; + + rewardTokens[i].transferFrom(msg.sender, address(this), amounts[i]); + IMultiRewardStaking(staking).fundReward( + rewardTokens[i], + amounts[i] + ); + } + } + + /*////////////////////////////////////////////////////////////// ESCROW MANAGEMENT LOGIC //////////////////////////////////////////////////////////////*/ - IMultiRewardEscrow public escrow; - - /** - * @notice Set fees for multiple tokens. Caller must be the owner. - * @param tokens Array of tokens. - * @param fees Array of fees for `tokens` in 1e18. (1e18 = 100%, 1e14 = 1 BPS) - * @dev See `MultiRewardEscrow` for more details. - * @dev We dont need to verify array length here since its done already in `MultiRewardEscrow` - */ - function setEscrowTokenFees(IERC20[] calldata tokens, uint256[] calldata fees) external onlyOwner { - (bool success, bytes memory returnData) = adminProxy.execute( - address(escrow), - abi.encodeWithSelector(IMultiRewardEscrow.setFees.selector, tokens, fees) - ); - if (!success) revert UnderlyingError(returnData); - } + IMultiRewardEscrow public escrow; + + /** + * @notice Set fees for multiple tokens. Caller must be the owner. + * @param tokens Array of tokens. + * @param fees Array of fees for `tokens` in 1e18. (1e18 = 100%, 1e14 = 1 BPS) + * @dev See `MultiRewardEscrow` for more details. + * @dev We dont need to verify array length here since its done already in `MultiRewardEscrow` + */ + function setEscrowTokenFees( + IERC20[] calldata tokens, + uint256[] calldata fees + ) external onlyOwner { + adminProxy.execute( + address(escrow), + abi.encodeWithSelector( + IMultiRewardEscrow.setFees.selector, + tokens, + fees + ) + ); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// TEMPLATE LOGIC //////////////////////////////////////////////////////////////*/ - /** - * @notice Adds a new templateCategory to the registry. Caller must be owner. - * @param templateCategories A new category of templates. - * @dev See `TemplateRegistry` for more details. - */ - function addTemplateCategories(bytes32[] calldata templateCategories) external onlyOwner { - address _deploymentController = address(deploymentController); - uint8 len = uint8(templateCategories.length); - for (uint256 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - _deploymentController, - abi.encodeWithSelector(IDeploymentController.addTemplateCategory.selector, templateCategories[i]) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /** - * @notice Toggles the endorsement of a templates. Caller must be owner. - * @param templateCategories TemplateCategory of the template to endorse. - * @param templateIds TemplateId of the template to endorse. - * @dev See `TemplateRegistry` for more details. - */ - function toggleTemplateEndorsements(bytes32[] calldata templateCategories, bytes32[] calldata templateIds) - external - onlyOwner - { - uint8 len = uint8(templateCategories.length); - _verifyEqualArrayLength(len, templateIds.length); - - address _deploymentController = address(deploymentController); - for (uint256 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - address(_deploymentController), - abi.encodeWithSelector( - ITemplateRegistry.toggleTemplateEndorsement.selector, - templateCategories[i], - templateIds[i] - ) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /*////////////////////////////////////////////////////////////// + /** + * @notice Adds a new templateCategory to the registry. Caller must be owner. + * @param templateCategories A new category of templates. + * @dev See `TemplateRegistry` for more details. + */ + function addTemplateCategories( + bytes32[] calldata templateCategories + ) external onlyOwner { + address _deploymentController = address(deploymentController); + uint8 len = uint8(templateCategories.length); + for (uint256 i = 0; i < len; i++) { + adminProxy.execute( + _deploymentController, + abi.encodeWithSelector( + IDeploymentController.addTemplateCategory.selector, + templateCategories[i] + ) + ); + } + } + + /** + * @notice Toggles the endorsement of a templates. Caller must be owner. + * @param templateCategories TemplateCategory of the template to endorse. + * @param templateIds TemplateId of the template to endorse. + * @dev See `TemplateRegistry` for more details. + */ + function toggleTemplateEndorsements( + bytes32[] calldata templateCategories, + bytes32[] calldata templateIds + ) external onlyOwner { + uint8 len = uint8(templateCategories.length); + _verifyEqualArrayLength(len, templateIds.length); + + address _deploymentController = address(deploymentController); + for (uint256 i = 0; i < len; i++) { + adminProxy.execute( + address(_deploymentController), + abi.encodeWithSelector( + ITemplateRegistry.toggleTemplateEndorsement.selector, + templateCategories[i], + templateIds[i] + ) + ); + } + } + + /*////////////////////////////////////////////////////////////// PAUSING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Pause Deposits and withdraw all funds from the underlying protocol. Caller must be owner. - function pauseAdapters(address[] calldata vaults) external onlyOwner { - uint8 len = uint8(vaults.length); - for (uint256 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - IVault(vaults[i]).adapter(), - abi.encodeWithSelector(IPausable.pause.selector) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /// @notice Unpause Deposits and deposit all funds into the underlying protocol. Caller must be owner. - function unpauseAdapters(address[] calldata vaults) external onlyOwner { - uint8 len = uint8(vaults.length); - for (uint256 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - IVault(vaults[i]).adapter(), - abi.encodeWithSelector(IPausable.unpause.selector) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /// @notice Pause deposits. Caller must be owner or creator of the Vault. - function pauseVaults(address[] calldata vaults) external { - uint8 len = uint8(vaults.length); - for (uint256 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IPausable.pause.selector) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /// @notice Unpause deposits. Caller must be owner or creator of the Vault. - function unpauseVaults(address[] calldata vaults) external { - uint8 len = uint8(vaults.length); - for (uint256 i = 0; i < len; i++) { - _verifyCreator(vaults[i]); - (bool success, bytes memory returnData) = adminProxy.execute( - vaults[i], - abi.encodeWithSelector(IPausable.unpause.selector) - ); - if (!success) revert UnderlyingError(returnData); - } - } - - /*////////////////////////////////////////////////////////////// + /// @notice Pause Deposits and withdraw all funds from the underlying protocol. Caller must be owner. + function pauseAdapters(address[] calldata vaults) external onlyOwner { + uint8 len = uint8(vaults.length); + for (uint256 i = 0; i < len; i++) { + adminProxy.execute( + IVault(vaults[i]).adapter(), + abi.encodeWithSelector(IPausable.pause.selector) + ); + } + } + + /// @notice Unpause Deposits and deposit all funds into the underlying protocol. Caller must be owner. + function unpauseAdapters(address[] calldata vaults) external onlyOwner { + uint8 len = uint8(vaults.length); + for (uint256 i = 0; i < len; i++) { + adminProxy.execute( + IVault(vaults[i]).adapter(), + abi.encodeWithSelector(IPausable.unpause.selector) + ); + } + } + + /// @notice Pause deposits. Caller must be owner or creator of the Vault. + function pauseVaults(address[] calldata vaults) external { + uint8 len = uint8(vaults.length); + for (uint256 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + adminProxy.execute( + vaults[i], + abi.encodeWithSelector(IPausable.pause.selector) + ); + } + } + + /// @notice Unpause deposits. Caller must be owner or creator of the Vault. + function unpauseVaults(address[] calldata vaults) external { + uint8 len = uint8(vaults.length); + for (uint256 i = 0; i < len; i++) { + _verifyCreator(vaults[i]); + adminProxy.execute( + vaults[i], + abi.encodeWithSelector(IPausable.unpause.selector) + ); + } + } + + /*////////////////////////////////////////////////////////////// VERIFICATION LOGIC //////////////////////////////////////////////////////////////*/ - error NotSubmitterNorOwner(address caller); - error NotSubmitter(address caller); - error NotAllowed(address subject); - error ArrayLengthMissmatch(); - - /// @notice Verify that the caller is the creator of the vault or owner of `VaultController` (admin rights). - function _verifyCreatorOrOwner(address vault) internal returns (VaultMetadata memory metadata) { - metadata = vaultRegistry.getVault(vault); - if (msg.sender != metadata.creator && msg.sender != owner) revert NotSubmitterNorOwner(msg.sender); - } - - /// @notice Verify that the caller is the creator of the vault. - function _verifyCreator(address vault) internal view returns (VaultMetadata memory metadata) { - metadata = vaultRegistry.getVault(vault); - if (msg.sender != metadata.creator) revert NotSubmitter(msg.sender); - } - - /// @notice Verify that the token is not rejected nor a clone. - function _verifyToken(address token) internal view { - if ( - ( - permissionRegistry.endorsed(address(0)) - ? !permissionRegistry.endorsed(token) - : permissionRegistry.rejected(token) - ) || - cloneRegistry.cloneExists(token) || - token == address(0) - ) revert NotAllowed(token); - } - - /// @notice Verify that the array lengths are equal. - function _verifyEqualArrayLength(uint256 length1, uint256 length2) internal pure { - if (length1 != length2) revert ArrayLengthMissmatch(); - } - - modifier canCreate() { - if ( - permissionRegistry.endorsed(address(1)) - ? !permissionRegistry.endorsed(msg.sender) - : permissionRegistry.rejected(msg.sender) - ) revert NotAllowed(msg.sender); - _; - } - - /*////////////////////////////////////////////////////////////// + error NotSubmitterNorOwner(address caller); + error NotSubmitter(address caller); + error NotAllowed(address subject); + error ArrayLengthMissmatch(); + + /// @notice Verify that the caller is the creator of the vault or owner of `VaultController` (admin rights). + function _verifyCreatorOrOwner( + address vault + ) internal returns (VaultMetadata memory metadata) { + metadata = vaultRegistry.getVault(vault); + if (msg.sender != metadata.creator && msg.sender != owner) + revert NotSubmitterNorOwner(msg.sender); + } + + /// @notice Verify that the caller is the creator of the vault. + function _verifyCreator( + address vault + ) internal view returns (VaultMetadata memory metadata) { + metadata = vaultRegistry.getVault(vault); + if (msg.sender != metadata.creator) revert NotSubmitter(msg.sender); + } + + /// @notice Verify that the token is not rejected nor a clone. + function _verifyToken(address token) internal view { + if ( + ( + permissionRegistry.endorsed(address(0)) + ? !permissionRegistry.endorsed(token) + : permissionRegistry.rejected(token) + ) || + cloneRegistry.cloneExists(token) || + token == address(0) + ) revert NotAllowed(token); + } + + /// @notice Verify that the array lengths are equal. + function _verifyEqualArrayLength( + uint256 length1, + uint256 length2 + ) internal pure { + if (length1 != length2) revert ArrayLengthMissmatch(); + } + + modifier canCreate() { + if ( + permissionRegistry.endorsed(address(1)) + ? !permissionRegistry.endorsed(msg.sender) + : permissionRegistry.rejected(msg.sender) + ) revert NotAllowed(msg.sender); + _; + } + + /*////////////////////////////////////////////////////////////// OWNERSHIP LOGIC //////////////////////////////////////////////////////////////*/ - IAdminProxy public adminProxy; - - /** - * @notice Nominates a new owner of `AdminProxy`. Caller must be owner. - * @dev Must be called if the `VaultController` gets swapped out or upgraded - */ - function nominateNewAdminProxyOwner(address newOwner) external onlyOwner { - adminProxy.nominateNewOwner(newOwner); - } - - /** - * @notice Accepts ownership of `AdminProxy`. Caller must be nominated owner. - * @dev Must be called after construction - */ - function acceptAdminProxyOwnership() external { - adminProxy.acceptOwnership(); - } - - /*////////////////////////////////////////////////////////////// + IAdminProxy public adminProxy; + + /** + * @notice Nominates a new owner of `AdminProxy`. Caller must be owner. + * @dev Must be called if the `VaultController` gets swapped out or upgraded + */ + function nominateNewAdminProxyOwner(address newOwner) external onlyOwner { + adminProxy.nominateNewOwner(newOwner); + } + + /** + * @notice Accepts ownership of `AdminProxy`. Caller must be nominated owner. + * @dev Must be called after construction + */ + function acceptAdminProxyOwnership() external { + adminProxy.acceptOwnership(); + } + + /*////////////////////////////////////////////////////////////// MANAGEMENT FEE LOGIC //////////////////////////////////////////////////////////////*/ - uint256 public performanceFee; + uint256 public performanceFee; - event PerformanceFeeChanged(uint256 oldFee, uint256 newFee); + event PerformanceFeeChanged(uint256 oldFee, uint256 newFee); - error InvalidPerformanceFee(uint256 fee); + error InvalidPerformanceFee(uint256 fee); - /** - * @notice Set a new performanceFee for all new adapters. Caller must be owner. - * @param newFee performance fee in 1e18. - * @dev Fees can be 0 but never more than 2e17 (1e18 = 100%, 1e14 = 1 BPS) - * @dev Can be retroactively applied to existing adapters. - */ - function setPerformanceFee(uint256 newFee) external onlyOwner { - // Dont take more than 20% performanceFee - if (newFee > 2e17) revert InvalidPerformanceFee(newFee); + /** + * @notice Set a new performanceFee for all new adapters. Caller must be owner. + * @param newFee performance fee in 1e18. + * @dev Fees can be 0 but never more than 2e17 (1e18 = 100%, 1e14 = 1 BPS) + * @dev Can be retroactively applied to existing adapters. + */ + function setPerformanceFee(uint256 newFee) external onlyOwner { + // Dont take more than 20% performanceFee + if (newFee > 2e17) revert InvalidPerformanceFee(newFee); - emit PerformanceFeeChanged(performanceFee, newFee); + emit PerformanceFeeChanged(performanceFee, newFee); - performanceFee = newFee; - } + performanceFee = newFee; + } - /** - * @notice Set a new performanceFee for existing adapters. Caller must be owner. - * @param adapters array of adapters to set the management fee for. - */ - function setAdapterPerformanceFees(address[] calldata adapters) external onlyOwner { - uint8 len = uint8(adapters.length); - for (uint256 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - adapters[i], - abi.encodeWithSelector(IAdapter.setPerformanceFee.selector, performanceFee) - ); - if (!success) revert UnderlyingError(returnData); + /** + * @notice Set a new performanceFee for existing adapters. Caller must be owner. + * @param adapters array of adapters to set the management fee for. + */ + function setAdapterPerformanceFees( + address[] calldata adapters + ) external onlyOwner { + uint8 len = uint8(adapters.length); + for (uint256 i = 0; i < len; i++) { + adminProxy.execute( + adapters[i], + abi.encodeWithSelector( + IAdapter.setPerformanceFee.selector, + performanceFee + ) + ); + } } - } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// HARVEST COOLDOWN LOGIC //////////////////////////////////////////////////////////////*/ - uint256 public harvestCooldown; + uint256 public harvestCooldown; - event HarvestCooldownChanged(uint256 oldCooldown, uint256 newCooldown); + event HarvestCooldownChanged(uint256 oldCooldown, uint256 newCooldown); - error InvalidHarvestCooldown(uint256 cooldown); + error InvalidHarvestCooldown(uint256 cooldown); - /** - * @notice Set a new harvestCooldown for all new adapters. Caller must be owner. - * @param newCooldown Time in seconds that must pass before a harvest can be called again. - * @dev Cant be longer than 1 day. - * @dev Can be retroactively applied to existing adapters. - */ - function setHarvestCooldown(uint256 newCooldown) external onlyOwner { - // Dont wait more than X seconds - if (newCooldown > 1 days) revert InvalidHarvestCooldown(newCooldown); + /** + * @notice Set a new harvestCooldown for all new adapters. Caller must be owner. + * @param newCooldown Time in seconds that must pass before a harvest can be called again. + * @dev Cant be longer than 1 day. + * @dev Can be retroactively applied to existing adapters. + */ + function setHarvestCooldown(uint256 newCooldown) external onlyOwner { + // Dont wait more than X seconds + if (newCooldown > 1 days) revert InvalidHarvestCooldown(newCooldown); - emit HarvestCooldownChanged(harvestCooldown, newCooldown); + emit HarvestCooldownChanged(harvestCooldown, newCooldown); - harvestCooldown = newCooldown; - } + harvestCooldown = newCooldown; + } - /** - * @notice Set a new harvestCooldown for existing adapters. Caller must be owner. - * @param adapters Array of adapters to set the cooldown for. - */ - function setAdapterHarvestCooldowns(address[] calldata adapters) external onlyOwner { - uint8 len = uint8(adapters.length); - for (uint256 i = 0; i < len; i++) { - (bool success, bytes memory returnData) = adminProxy.execute( - adapters[i], - abi.encodeWithSelector(IAdapter.setHarvestCooldown.selector, harvestCooldown) - ); - if (!success) revert UnderlyingError(returnData); + /** + * @notice Set a new harvestCooldown for existing adapters. Caller must be owner. + * @param adapters Array of adapters to set the cooldown for. + */ + function setAdapterHarvestCooldowns( + address[] calldata adapters + ) external onlyOwner { + uint8 len = uint8(adapters.length); + for (uint256 i = 0; i < len; i++) { + adminProxy.execute( + adapters[i], + abi.encodeWithSelector( + IAdapter.setHarvestCooldown.selector, + harvestCooldown + ) + ); + } } - } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// DEPLYOMENT CONTROLLER LOGIC //////////////////////////////////////////////////////////////*/ - IDeploymentController public deploymentController; - ICloneRegistry public cloneRegistry; - ITemplateRegistry public templateRegistry; - IPermissionRegistry public permissionRegistry; + IDeploymentController public deploymentController; + ICloneRegistry public cloneRegistry; + ITemplateRegistry public templateRegistry; + IPermissionRegistry public permissionRegistry; - event DeploymentControllerChanged(address oldController, address newController); - - error InvalidDeploymentController(address deploymentController); - - /** - * @notice Sets a new `DeploymentController` and saves its auxilary contracts. Caller must be owner. - * @param _deploymentController New DeploymentController. - */ - function setDeploymentController(IDeploymentController _deploymentController) external onlyOwner { - _setDeploymentController(_deploymentController); - } - - function _setDeploymentController(IDeploymentController _deploymentController) internal { - if (address(_deploymentController) == address(0) || address(deploymentController) == address(_deploymentController)) - revert InvalidDeploymentController(address(_deploymentController)); - - emit DeploymentControllerChanged(address(deploymentController), address(_deploymentController)); + event DeploymentControllerChanged( + address oldController, + address newController + ); - // Dont try to change ownership on construction - if (address(deploymentController) != address(0)) _transferDependencyOwnership(address(_deploymentController)); + error InvalidDeploymentController(address deploymentController); - deploymentController = _deploymentController; - cloneRegistry = _deploymentController.cloneRegistry(); - templateRegistry = _deploymentController.templateRegistry(); - } + /** + * @notice Sets a new `DeploymentController` and saves its auxilary contracts. Caller must be owner. + * @param _deploymentController New DeploymentController. + */ + function setDeploymentController( + IDeploymentController _deploymentController + ) external onlyOwner { + _setDeploymentController(_deploymentController); + } - function _transferDependencyOwnership(address _deploymentController) internal { - (bool success, bytes memory returnData) = adminProxy.execute( - address(deploymentController), - abi.encodeWithSelector(IDeploymentController.nominateNewDependencyOwner.selector, _deploymentController) - ); - if (!success) revert UnderlyingError(returnData); + function _setDeploymentController( + IDeploymentController _deploymentController + ) internal { + if ( + address(_deploymentController) == address(0) || + address(deploymentController) == address(_deploymentController) + ) revert InvalidDeploymentController(address(_deploymentController)); + + emit DeploymentControllerChanged( + address(deploymentController), + address(_deploymentController) + ); + + // Dont try to change ownership on construction + if (address(deploymentController) != address(0)) + _transferDependencyOwnership(address(_deploymentController)); + + deploymentController = _deploymentController; + cloneRegistry = _deploymentController.cloneRegistry(); + templateRegistry = _deploymentController.templateRegistry(); + } - (success, returnData) = adminProxy.execute( - _deploymentController, - abi.encodeWithSelector(IDeploymentController.acceptDependencyOwnership.selector, "") - ); - if (!success) revert UnderlyingError(returnData); - } + function _transferDependencyOwnership( + address _deploymentController + ) internal { + adminProxy.execute( + address(deploymentController), + abi.encodeWithSelector( + IDeploymentController.nominateNewDependencyOwner.selector, + _deploymentController + ) + ); + + adminProxy.execute( + _deploymentController, + abi.encodeWithSelector( + IDeploymentController.acceptDependencyOwnership.selector, + "" + ) + ); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// TEMPLATE KEY LOGIC //////////////////////////////////////////////////////////////*/ - mapping(bytes32 => bytes32) public activeTemplateId; + mapping(bytes32 => bytes32) public activeTemplateId; - event ActiveTemplateIdChanged(bytes32 oldKey, bytes32 newKey); + event ActiveTemplateIdChanged(bytes32 oldKey, bytes32 newKey); - error SameKey(bytes32 templateKey); + error SameKey(bytes32 templateKey); - /** - * @notice Set a templateId which shall be used for deploying certain contracts. Caller must be owner. - * @param templateCategory TemplateCategory to set an active key for. - * @param templateId TemplateId that should be used when creating a new contract of `templateCategory` - * @dev Currently `Vault` and `Staking` use a template set via `activeTemplateId`. - * @dev If this contract should deploy Vaults of a second generation this can be set via the `activeTemplateId`. - */ - function setActiveTemplateId(bytes32 templateCategory, bytes32 templateId) external onlyOwner { - bytes32 oldTemplateId = activeTemplateId[templateCategory]; - if (oldTemplateId == templateId) revert SameKey(templateId); + /** + * @notice Set a templateId which shall be used for deploying certain contracts. Caller must be owner. + * @param templateCategory TemplateCategory to set an active key for. + * @param templateId TemplateId that should be used when creating a new contract of `templateCategory` + * @dev Currently `Vault` and `Staking` use a template set via `activeTemplateId`. + * @dev If this contract should deploy Vaults of a second generation this can be set via the `activeTemplateId`. + */ + function setActiveTemplateId( + bytes32 templateCategory, + bytes32 templateId + ) external onlyOwner { + bytes32 oldTemplateId = activeTemplateId[templateCategory]; + if (oldTemplateId == templateId) revert SameKey(templateId); - emit ActiveTemplateIdChanged(oldTemplateId, templateId); + emit ActiveTemplateIdChanged(oldTemplateId, templateId); - activeTemplateId[templateCategory] = templateId; - } + activeTemplateId[templateCategory] = templateId; + } } From 8b1e5d148315abb08b421002f4bbd9905bfb10e9 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 13:30:28 +0200 Subject: [PATCH 11/18] added maxLoss to yearnAdapter --- src/vault/adapter/yearn/IYearn.sol | 30 +- src/vault/adapter/yearn/YearnAdapter.sol | 281 +++++++----- .../integration/yearn/YearnAdapter.t.sol | 413 +++++++++++------- .../yearn/YearnTestConfigStorage.sol | 55 ++- 4 files changed, 480 insertions(+), 299 deletions(-) diff --git a/src/vault/adapter/yearn/IYearn.sol b/src/vault/adapter/yearn/IYearn.sol index 3bc82ba..b93dc23 100644 --- a/src/vault/adapter/yearn/IYearn.sol +++ b/src/vault/adapter/yearn/IYearn.sol @@ -3,32 +3,36 @@ pragma solidity ^0.8.15; -import { IERC20Upgradeable as IERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20Upgradeable as IERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; interface VaultAPI is IERC20 { - function deposit(uint256 amount) external returns (uint256); + function deposit(uint256 amount) external returns (uint256); - function withdraw(uint256 maxShares) external returns (uint256); + function withdraw( + uint256 maxShares, + address recipient, + uint256 maxLoss + ) external returns (uint256); - function pricePerShare() external view returns (uint256); + function pricePerShare() external view returns (uint256); - function totalAssets() external view returns (uint256); + function totalAssets() external view returns (uint256); - function totalSupply() external view returns (uint256); + function totalSupply() external view returns (uint256); - function depositLimit() external view returns (uint256); + function depositLimit() external view returns (uint256); - function token() external view returns (address); + function token() external view returns (address); - function lastReport() external view returns (uint256); + function lastReport() external view returns (uint256); - function lockedProfit() external view returns (uint256); + function lockedProfit() external view returns (uint256); - function lockedProfitDegradation() external view returns (uint256); + function lockedProfitDegradation() external view returns (uint256); - function totalDebt() external view returns (uint256); + function totalDebt() external view returns (uint256); } interface IYearnRegistry { - function latestVault(address token) external view returns (address); + function latestVault(address token) external view returns (address); } diff --git a/src/vault/adapter/yearn/YearnAdapter.sol b/src/vault/adapter/yearn/YearnAdapter.sol index 48c1335..6018e6b 100644 --- a/src/vault/adapter/yearn/YearnAdapter.sol +++ b/src/vault/adapter/yearn/YearnAdapter.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.15; -import { AdapterBase, ERC4626Upgradeable as ERC4626, IERC20, IERC20Metadata, ERC20, SafeERC20, Math, IStrategy, IAdapter } from "../abstracts/AdapterBase.sol"; -import { VaultAPI, IYearnRegistry } from "./IYearn.sol"; +import {AdapterBase, ERC4626Upgradeable as ERC4626, IERC20, IERC20Metadata, ERC20, SafeERC20, Math, IStrategy, IAdapter} from "../abstracts/AdapterBase.sol"; +import {VaultAPI, IYearnRegistry} from "./IYearn.sol"; /** * @title Yearn Adapter @@ -15,129 +15,194 @@ import { VaultAPI, IYearnRegistry } from "./IYearn.sol"; * Allows wrapping Yearn Vaults. */ contract YearnAdapter is AdapterBase { - using SafeERC20 for IERC20; - using Math for uint256; + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + VaultAPI public yVault; + uint256 public maxLoss; + + uint256 constant DEGRADATION_COEFFICIENT = 10 ** 18; + + error MaxLossTooHigh(); + + /** + * @notice Initialize a new Yearn Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @param externalRegistry Yearn registry address. + * @param yearnData `MaxLoss` for yVault encoded in bytes. + * @dev This function is called by the factory contract when deploying a new vault. + * @dev The yearn registry will be used given the `asset` from `adapterInitData` to find the latest yVault. + */ + function initialize( + bytes memory adapterInitData, + address externalRegistry, + bytes memory yearnData + ) external initializer { + (address _asset, , , , , ) = abi.decode( + adapterInitData, + (address, address, address, uint256, bytes4[8], bytes) + ); + __AdapterBase_init(adapterInitData); + + yVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); + + _name = string.concat( + "Popcorn Yearn", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popY-", IERC20Metadata(asset()).symbol()); + + maxLoss = abi.decode(yearnData, (uint256)); + if (maxLoss > 10_000) revert MaxLossTooHigh(); + + IERC20(_asset).approve(address(yVault), type(uint256).max); + } - string internal _name; - string internal _symbol; + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } - VaultAPI public yVault; - uint256 constant DEGRADATION_COEFFICIENT = 10 ** 18; + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } - /** - * @notice Initialize a new Yearn Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @param externalRegistry Yearn registry address. - * @dev This function is called by the factory contract when deploying a new vault. - * @dev The yearn registry will be used given the `asset` from `adapterInitData` to find the latest yVault. - */ - function initialize(bytes memory adapterInitData, address externalRegistry, bytes memory) external initializer { - (address _asset, , , , , ) = abi.decode(adapterInitData, (address, address, address, uint256, bytes4[8], bytes)); - __AdapterBase_init(adapterInitData); + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ - yVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); + /// @notice Emulate yearns total asset calculation to return the total assets of the vault. + function _totalAssets() internal view override returns (uint256) { + return _shareValue(yVault.balanceOf(address(this))); + } - _name = string.concat("Popcorn Yearn", IERC20Metadata(asset()).name(), " Adapter"); - _symbol = string.concat("popY-", IERC20Metadata(asset()).symbol()); + /// @notice Determines the current value of `yShares` in assets + function _shareValue(uint256 yShares) internal view returns (uint256) { + if (yVault.totalSupply() == 0) return yShares; - IERC20(_asset).approve(address(yVault), type(uint256).max); - } + return + yShares.mulDiv( + _freeFunds(), + yVault.totalSupply(), + Math.Rounding.Down + ); + } - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } + /// @notice The amount of assets that are free to be withdrawn from the yVault after locked profts. + function _freeFunds() internal view returns (uint256) { + return _yTotalAssets() - _calculateLockedProfit(); + } - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } + /** + * @notice Returns the total quantity of all assets under control of this Vault, + * whether they're loaned out to a Strategy, or currently held in the Vault. + */ + function _yTotalAssets() internal view returns (uint256) { + return IERC20(asset()).balanceOf(address(yVault)) + yVault.totalDebt(); + } - /*////////////////////////////////////////////////////////////// - ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ + /// @notice Calculates how much profit is locked and cant be withdrawn. + function _calculateLockedProfit() internal view returns (uint256) { + uint256 lockedFundsRatio = (block.timestamp - yVault.lastReport()) * + yVault.lockedProfitDegradation(); + + if (lockedFundsRatio < DEGRADATION_COEFFICIENT) { + uint256 lockedProfit = yVault.lockedProfit(); + return + lockedProfit - + ((lockedFundsRatio * lockedProfit) / DEGRADATION_COEFFICIENT); + } else { + return 0; + } + } - /// @notice Emulate yearns total asset calculation to return the total assets of the vault. - function _totalAssets() internal view override returns (uint256) { - return _shareValue(yVault.balanceOf(address(this))); - } - - /// @notice Determines the current value of `yShares` in assets - function _shareValue(uint256 yShares) internal view returns (uint256) { - if (yVault.totalSupply() == 0) return yShares; - - return yShares.mulDiv(_freeFunds(), yVault.totalSupply(), Math.Rounding.Down); - } - - /// @notice The amount of assets that are free to be withdrawn from the yVault after locked profts. - function _freeFunds() internal view returns (uint256) { - return _yTotalAssets() - _calculateLockedProfit(); - } - - /** - * @notice Returns the total quantity of all assets under control of this Vault, - * whether they're loaned out to a Strategy, or currently held in the Vault. - */ - function _yTotalAssets() internal view returns (uint256) { - return IERC20(asset()).balanceOf(address(yVault)) + yVault.totalDebt(); - } - - /// @notice Calculates how much profit is locked and cant be withdrawn. - function _calculateLockedProfit() internal view returns (uint256) { - uint256 lockedFundsRatio = (block.timestamp - yVault.lastReport()) * yVault.lockedProfitDegradation(); - - if (lockedFundsRatio < DEGRADATION_COEFFICIENT) { - uint256 lockedProfit = yVault.lockedProfit(); - return lockedProfit - ((lockedFundsRatio * lockedProfit) / DEGRADATION_COEFFICIENT); - } else { - return 0; - } - } - - /// @notice The amount of aave shares to withdraw given an mount of adapter shares - function convertToUnderlyingShares(uint256, uint256 shares) public view override returns (uint256) { - uint256 supply = totalSupply(); - return supply == 0 ? shares : shares.mulDiv(yVault.balanceOf(address(this)), supply, Math.Rounding.Up); - } - - function previewDeposit(uint256 assets) public view override returns (uint256) { - return paused() ? 0 : _convertToShares(assets - 0, Math.Rounding.Down); - } - - function previewMint(uint256 shares) public view override returns (uint256) { - return paused() ? 0 : _convertToAssets(shares + 0, Math.Rounding.Up); - } - - function previewWithdraw(uint256 assets) public view override returns (uint256) { - return _convertToShares(assets + 0, Math.Rounding.Up); - } - - function previewRedeem(uint256 shares) public view override returns (uint256) { - return _convertToAssets(shares - 0, Math.Rounding.Down); - } - - /*////////////////////////////////////////////////////////////// + /// @notice The amount of aave shares to withdraw given an mount of adapter shares + function convertToUnderlyingShares( + uint256, + uint256 shares + ) public view override returns (uint256) { + uint256 supply = totalSupply(); + return + supply == 0 + ? shares + : shares.mulDiv( + yVault.balanceOf(address(this)), + supply, + Math.Rounding.Up + ); + } + + function previewDeposit( + uint256 assets + ) public view override returns (uint256) { + return paused() ? 0 : _convertToShares(assets - 0, Math.Rounding.Down); + } + + function previewMint( + uint256 shares + ) public view override returns (uint256) { + return paused() ? 0 : _convertToAssets(shares + 0, Math.Rounding.Up); + } + + function previewWithdraw( + uint256 assets + ) public view override returns (uint256) { + return _convertToShares(assets + 0, Math.Rounding.Up); + } + + function previewRedeem( + uint256 shares + ) public view override returns (uint256) { + return _convertToAssets(shares - 0, Math.Rounding.Down); + } + + /*////////////////////////////////////////////////////////////// DEPOSIT/WITHDRAWAL LIMIT LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Applies the yVault deposit limit to the adapter. - function maxDeposit(address) public view override returns (uint256) { - if (paused()) return 0; + /// @notice Applies the yVault deposit limit to the adapter. + function maxDeposit(address) public view override returns (uint256) { + if (paused()) return 0; - VaultAPI _bestVault = yVault; - uint256 assets = _bestVault.totalAssets(); - uint256 _depositLimit = _bestVault.depositLimit(); - if (assets >= _depositLimit) return 0; - return _depositLimit - assets; - } + VaultAPI _bestVault = yVault; + uint256 assets = _bestVault.totalAssets(); + uint256 _depositLimit = _bestVault.depositLimit(); + if (assets >= _depositLimit) return 0; + return _depositLimit - assets; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - function _protocolDeposit(uint256 amount, uint256) internal virtual override { - yVault.deposit(amount); - } + function _protocolDeposit( + uint256 amount, + uint256 + ) internal virtual override { + yVault.deposit(amount); + } - function _protocolWithdraw(uint256 assets, uint256 shares) internal virtual override { - yVault.withdraw(convertToUnderlyingShares(assets, shares)); - } + function _protocolWithdraw( + uint256 assets, + uint256 shares + ) internal virtual override { + yVault.withdraw( + convertToUnderlyingShares(assets, shares), + address(this), + maxLoss + ); + } } diff --git a/test/vault/integration/yearn/YearnAdapter.t.sol b/test/vault/integration/yearn/YearnAdapter.t.sol index 2292ac4..324884e 100644 --- a/test/vault/integration/yearn/YearnAdapter.t.sol +++ b/test/vault/integration/yearn/YearnAdapter.t.sol @@ -3,175 +3,276 @@ pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; -import { YearnAdapter, SafeERC20, IERC20, IERC20Metadata, Math, VaultAPI, IYearnRegistry } from "../../../../src/vault/adapter/yearn/YearnAdapter.sol"; -import { YearnTestConfigStorage, YearnTestConfig } from "./YearnTestConfigStorage.sol"; -import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../abstract/AbstractAdapterTest.sol"; +import {YearnAdapter, SafeERC20, IERC20, IERC20Metadata, Math, VaultAPI, IYearnRegistry} from "../../../../src/vault/adapter/yearn/YearnAdapter.sol"; +import {YearnTestConfigStorage, YearnTestConfig} from "./YearnTestConfigStorage.sol"; +import {AbstractAdapterTest, ITestConfigStorage, IAdapter} from "../abstract/AbstractAdapterTest.sol"; contract YearnAdapterTest is AbstractAdapterTest { - using Math for uint256; - - VaultAPI yearnVault; - - function setUp() public { - uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); - vm.selectFork(forkId); - - testConfigStorage = ITestConfigStorage(address(new YearnTestConfigStorage())); - - _setUpTest(testConfigStorage.getTestConfig(0)); - } - - function overrideSetup(bytes memory testConfig) public override { - _setUpTest(testConfig); - } - - function _setUpTest(bytes memory testConfig) internal { - address _asset = abi.decode(testConfig, (address)); - - setUpBaseTest(IERC20(_asset), address(new YearnAdapter()), 0x50c1a2eA0a861A967D9d0FFE2AE4012c2E053804, 10, "Yearn ", false); - - yearnVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); - - vm.label(address(yearnVault), "yearnVault"); - vm.label(address(asset), "asset"); - vm.label(address(this), "test"); - - adapter.initialize(abi.encode(asset, address(this), address(0), 0, sigs, ""), externalRegistry, ""); - - defaultAmount = 10**IERC20Metadata(address(asset)).decimals(); - minFuzz = defaultAmount * 10_000; - raise = defaultAmount * 100_000_000; - maxAssets = defaultAmount * 1_000_000; - maxShares = maxAssets / 2; - } - - /*////////////////////////////////////////////////////////////// + using Math for uint256; + + VaultAPI yearnVault; + + function setUp() public { + uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); + vm.selectFork(forkId); + + testConfigStorage = ITestConfigStorage( + address(new YearnTestConfigStorage()) + ); + + _setUpTest(testConfigStorage.getTestConfig(0)); + } + + function overrideSetup(bytes memory testConfig) public override { + _setUpTest(testConfig); + } + + function _setUpTest(bytes memory testConfig) internal { + (address _asset, uint256 _maxLoss) = abi.decode( + testConfig, + (address, uint256) + ); + + setUpBaseTest( + IERC20(_asset), + address(new YearnAdapter()), + 0x50c1a2eA0a861A967D9d0FFE2AE4012c2E053804, + 10, + "Yearn ", + false + ); + + yearnVault = VaultAPI( + IYearnRegistry(externalRegistry).latestVault(_asset) + ); + + vm.label(address(yearnVault), "yearnVault"); + vm.label(address(asset), "asset"); + vm.label(address(this), "test"); + + adapter.initialize( + abi.encode(asset, address(this), address(0), 0, sigs, ""), + externalRegistry, + abi.encode(_maxLoss) + ); + + defaultAmount = 10 ** IERC20Metadata(address(asset)).decimals(); + minFuzz = defaultAmount * 10_000; + raise = defaultAmount * 100_000_000; + maxAssets = defaultAmount * 1_000_000; + maxShares = maxAssets / 2; + } + + /*////////////////////////////////////////////////////////////// HELPER //////////////////////////////////////////////////////////////*/ - function increasePricePerShare(uint256 amount) public override { - deal(address(asset), address(yearnVault), asset.balanceOf(address(yearnVault)) + amount); - } - - function iouBalance() public view override returns (uint256) { - return yearnVault.balanceOf(address(adapter)); - } - - // Verify that totalAssets returns the expected amount - function verify_totalAssets() public override { - // Make sure totalAssets isnt 0 - deal(address(asset), bob, defaultAmount); - vm.startPrank(bob); - asset.approve(address(adapter), defaultAmount); - adapter.deposit(defaultAmount, bob); - vm.stopPrank(); - - assertApproxEqAbs( - adapter.totalAssets(), - adapter.convertToAssets(adapter.totalSupply()), - _delta_, - string.concat("totalSupply converted != totalAssets", baseTestId) - ); - - assertApproxEqAbs( - adapter.totalAssets(), - iouBalance().mulDiv(yearnVault.pricePerShare(), 10**IERC20Metadata(address(asset)).decimals(), Math.Rounding.Up), - _delta_, - string.concat("totalAssets != yearn assets", baseTestId) - ); - } - - /*////////////////////////////////////////////////////////////// + function increasePricePerShare(uint256 amount) public override { + deal( + address(asset), + address(yearnVault), + asset.balanceOf(address(yearnVault)) + amount + ); + } + + function iouBalance() public view override returns (uint256) { + return yearnVault.balanceOf(address(adapter)); + } + + // Verify that totalAssets returns the expected amount + function verify_totalAssets() public override { + // Make sure totalAssets isnt 0 + deal(address(asset), bob, defaultAmount); + vm.startPrank(bob); + asset.approve(address(adapter), defaultAmount); + adapter.deposit(defaultAmount, bob); + vm.stopPrank(); + + assertApproxEqAbs( + adapter.totalAssets(), + adapter.convertToAssets(adapter.totalSupply()), + _delta_, + string.concat("totalSupply converted != totalAssets", baseTestId) + ); + + assertApproxEqAbs( + adapter.totalAssets(), + iouBalance().mulDiv( + yearnVault.pricePerShare(), + 10 ** IERC20Metadata(address(asset)).decimals(), + Math.Rounding.Up + ), + _delta_, + string.concat("totalAssets != yearn assets", baseTestId) + ); + } + + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ - function verify_adapterInit() public override { - assertEq(adapter.asset(), yearnVault.token(), "asset"); - assertEq( - IERC20Metadata(address(adapter)).name(), - string.concat("Popcorn Yearn", IERC20Metadata(address(asset)).name(), " Adapter"), - "name" - ); - assertEq( - IERC20Metadata(address(adapter)).symbol(), - string.concat("popY-", IERC20Metadata(address(asset)).symbol()), - "symbol" - ); - - assertEq(asset.allowance(address(adapter), address(yearnVault)), type(uint256).max, "allowance"); - } - - /*////////////////////////////////////////////////////////////// + function test__initialization() public override { + createAdapter(); + uint256 callTime = block.timestamp; + + (, uint256 maxLoss) = abi.decode( + testConfigStorage.getTestConfig(0), + (address, uint256) + ); + + if (address(strategy) != address(0)) { + vm.expectEmit(false, false, false, true, address(strategy)); + emit SelectorsVerified(); + vm.expectEmit(false, false, false, true, address(strategy)); + emit AdapterVerified(); + vm.expectEmit(false, false, false, true, address(strategy)); + emit StrategySetup(); + } + vm.expectEmit(false, false, false, true, address(adapter)); + emit Initialized(uint8(1)); + + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + abi.encode(maxLoss) + ); + + assertEq(adapter.owner(), address(this), "owner"); + assertEq(adapter.strategy(), address(strategy), "strategy"); + assertEq(adapter.harvestCooldown(), 0, "harvestCooldown"); + assertEq(adapter.strategyConfig(), "", "strategyConfig"); + assertEq( + IERC20Metadata(address(adapter)).decimals(), + IERC20Metadata(address(asset)).decimals() + adapter.decimalOffset(), + "decimals" + ); + + verify_adapterInit(); + } + + function verify_adapterInit() public override { + assertEq(adapter.asset(), yearnVault.token(), "asset"); + assertEq( + IERC20Metadata(address(adapter)).name(), + string.concat( + "Popcorn Yearn", + IERC20Metadata(address(asset)).name(), + " Adapter" + ), + "name" + ); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popY-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + + assertEq( + asset.allowance(address(adapter), address(yearnVault)), + type(uint256).max, + "allowance" + ); + + // Revert if MaxLoss is too high + createAdapter(); + vm.expectRevert(YearnAdapter.MaxLossTooHigh.selector); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + abi.encode(uint256(10_001)) + ); + } + + /*////////////////////////////////////////////////////////////// ROUNDTRIP TESTS //////////////////////////////////////////////////////////////*/ - // NOTE - The yearn adapter suffers often from an off-by-one error which "steals" 1 wei from the user - function test__RT_deposit_withdraw() public override { - _mintFor(minFuzz, bob); - - vm.startPrank(bob); - uint256 shares1 = adapter.deposit(minFuzz, bob); - uint256 shares2 = adapter.withdraw(adapter.maxWithdraw(bob), bob, bob); - vm.stopPrank(); - - // We compare assets here with maxWithdraw since the shares of withdraw will always be lower than `compoundDefaultAmount` - // This tests the same assumption though. As long as you can withdraw less or equal assets to the input amount you cant round trip - assertGe(minFuzz, adapter.maxWithdraw(bob), testId); - } - - // NOTE - The yearn adapter suffers often from an off-by-one error which "steals" 1 wei from the user - function test__RT_mint_withdraw() public override { - _mintFor(adapter.previewMint(minFuzz), bob); - - vm.startPrank(bob); - uint256 assets = adapter.mint(minFuzz, bob); - uint256 shares = adapter.withdraw(adapter.maxWithdraw(bob), bob, bob); - vm.stopPrank(); - // We compare assets here with maxWithdraw since the shares of withdraw will always be lower than `compoundDefaultAmount` - // This tests the same assumption though. As long as you can withdraw less or equal assets to the input amount you cant round trip - assertGe(adapter.previewMint(minFuzz), adapter.maxWithdraw(bob), testId); - } - - function test__RT_mint_redeem() public override { - _mintFor(adapter.previewMint(minFuzz), bob); - - vm.startPrank(bob); - uint256 assets1 = adapter.mint(minFuzz, bob); - uint256 assets2 = adapter.redeem(minFuzz, bob, bob); - vm.stopPrank(); - - assertLe(assets2, assets1, testId); - } - - /*////////////////////////////////////////////////////////////// + // NOTE - The yearn adapter suffers often from an off-by-one error which "steals" 1 wei from the user + function test__RT_deposit_withdraw() public override { + _mintFor(minFuzz, bob); + + vm.startPrank(bob); + uint256 shares1 = adapter.deposit(minFuzz, bob); + uint256 shares2 = adapter.withdraw(adapter.maxWithdraw(bob), bob, bob); + vm.stopPrank(); + + // We compare assets here with maxWithdraw since the shares of withdraw will always be lower than `compoundDefaultAmount` + // This tests the same assumption though. As long as you can withdraw less or equal assets to the input amount you cant round trip + assertGe(minFuzz, adapter.maxWithdraw(bob), testId); + } + + // NOTE - The yearn adapter suffers often from an off-by-one error which "steals" 1 wei from the user + function test__RT_mint_withdraw() public override { + _mintFor(adapter.previewMint(minFuzz), bob); + + vm.startPrank(bob); + uint256 assets = adapter.mint(minFuzz, bob); + uint256 shares = adapter.withdraw(adapter.maxWithdraw(bob), bob, bob); + vm.stopPrank(); + // We compare assets here with maxWithdraw since the shares of withdraw will always be lower than `compoundDefaultAmount` + // This tests the same assumption though. As long as you can withdraw less or equal assets to the input amount you cant round trip + assertGe( + adapter.previewMint(minFuzz), + adapter.maxWithdraw(bob), + testId + ); + } + + function test__RT_mint_redeem() public override { + _mintFor(adapter.previewMint(minFuzz), bob); + + vm.startPrank(bob); + uint256 assets1 = adapter.mint(minFuzz, bob); + uint256 assets2 = adapter.redeem(minFuzz, bob, bob); + vm.stopPrank(); + + assertLe(assets2, assets1, testId); + } + + /*////////////////////////////////////////////////////////////// PAUSE //////////////////////////////////////////////////////////////*/ - function test__unpause() public override { - _mintFor(minFuzz * 3, bob); - - vm.prank(bob); - adapter.deposit(minFuzz, bob); - - uint256 oldTotalAssets = adapter.totalAssets(); - uint256 oldTotalSupply = adapter.totalSupply(); - uint256 oldIouBalance = iouBalance(); - - adapter.pause(); - adapter.unpause(); - - // We simply deposit back into the external protocol - // TotalSupply and Assets dont change - assertApproxEqAbs(oldTotalAssets, adapter.totalAssets(), _delta_, "totalAssets"); - assertApproxEqAbs(oldTotalSupply, adapter.totalSupply(), _delta_, "totalSupply"); - assertApproxEqAbs(asset.balanceOf(address(adapter)), 0, _delta_, "asset balance"); - assertApproxEqAbs(iouBalance(), oldIouBalance, _delta_, "iou balance"); - - // Deposit and mint dont revert - vm.startPrank(bob); - adapter.deposit(minFuzz, bob); - adapter.mint(minFuzz, bob); - } + function test__unpause() public override { + _mintFor(minFuzz * 3, bob); + + vm.prank(bob); + adapter.deposit(minFuzz, bob); + + uint256 oldTotalAssets = adapter.totalAssets(); + uint256 oldTotalSupply = adapter.totalSupply(); + uint256 oldIouBalance = iouBalance(); + + adapter.pause(); + adapter.unpause(); + + // We simply deposit back into the external protocol + // TotalSupply and Assets dont change + assertApproxEqAbs( + oldTotalAssets, + adapter.totalAssets(), + _delta_, + "totalAssets" + ); + assertApproxEqAbs( + oldTotalSupply, + adapter.totalSupply(), + _delta_, + "totalSupply" + ); + assertApproxEqAbs( + asset.balanceOf(address(adapter)), + 0, + _delta_, + "asset balance" + ); + assertApproxEqAbs(iouBalance(), oldIouBalance, _delta_, "iou balance"); + + // Deposit and mint dont revert + vm.startPrank(bob); + adapter.deposit(minFuzz, bob); + adapter.mint(minFuzz, bob); + } } diff --git a/test/vault/integration/yearn/YearnTestConfigStorage.sol b/test/vault/integration/yearn/YearnTestConfigStorage.sol index 3c29e97..1b672c3 100644 --- a/test/vault/integration/yearn/YearnTestConfigStorage.sol +++ b/test/vault/integration/yearn/YearnTestConfigStorage.sol @@ -3,31 +3,42 @@ pragma solidity ^0.8.15; -import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; +import {ITestConfigStorage} from "../abstract/ITestConfigStorage.sol"; struct YearnTestConfig { - address asset; + address asset; + uint256 maxLoss; } contract YearnTestConfigStorage is ITestConfigStorage { - YearnTestConfig[] internal testConfigs; - - constructor() { - // USDC - testConfigs.push(YearnTestConfig(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)); - - // WETH - // testConfigs.push(YearnTestConfig(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); - - // DAI - testConfigs.push(YearnTestConfig(0x6B175474E89094C44Da98b954EedeAC495271d0F)); - } - - function getTestConfig(uint256 i) public view returns (bytes memory) { - return abi.encode(testConfigs[i].asset); - } - - function getTestConfigLength() public view returns (uint256) { - return testConfigs.length; - } + YearnTestConfig[] internal testConfigs; + + constructor() { + // USDC + testConfigs.push( + YearnTestConfig( + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, + uint256(1) + ) + ); + + // WETH + // testConfigs.push(YearnTestConfig(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, uint256(1))); + + // DAI + testConfigs.push( + YearnTestConfig( + 0x6B175474E89094C44Da98b954EedeAC495271d0F, + uint256(1) + ) + ); + } + + function getTestConfig(uint256 i) public view returns (bytes memory) { + return abi.encode(testConfigs[i].asset, testConfigs[i].maxLoss); + } + + function getTestConfigLength() public view returns (uint256) { + return testConfigs.length; + } } From 5f1b3e9995cd4561c42a740d1e1fde8a59609b05 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 11 Apr 2023 16:02:47 +0200 Subject: [PATCH 12/18] wip - added MasterChefV2 --- .../IMasterChefV1.sol} | 6 +- .../MasterChefV1Adapter.sol} | 24 +-- .../sushi/masterChefV2/IMasterChefV2.sol | 47 ++++++ .../masterChefV2/MasterChefV2Adapter.sol | 148 +++++++++++++++++ .../{ => aave}/aaveV2/AaveV2Adapter.t.sol | 6 +- .../aaveV2/AaveV2TestConfigStorage.sol | 2 +- .../{ => aave}/aaveV3/AaveV3Adapter.t.sol | 4 +- .../aaveV3/AaveV3TestConfigStorage.sol | 2 +- .../MasterChefV1Adapter.t.sol} | 22 +-- .../MasterChefV1TestConfigStorage.sol} | 10 +- .../masterChefV2/MasterChefV2Adapter.t.sol | 149 ++++++++++++++++++ .../MasterChefV2TestConfigStorage.sol | 24 +++ 12 files changed, 406 insertions(+), 38 deletions(-) rename src/vault/adapter/sushi/{IMasterChef.sol => masterChefV1/IMasterChefV1.sol} (89%) rename src/vault/adapter/sushi/{MasterChefAdapter.sol => masterChefV1/MasterChefV1Adapter.sol} (84%) create mode 100644 src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol create mode 100644 src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol rename test/vault/integration/{ => aave}/aaveV2/AaveV2Adapter.t.sol (94%) rename test/vault/integration/{ => aave}/aaveV2/AaveV2TestConfigStorage.sol (90%) rename test/vault/integration/{ => aave}/aaveV3/AaveV3Adapter.t.sol (97%) rename test/vault/integration/{ => aave}/aaveV3/AaveV3TestConfigStorage.sol (90%) rename test/vault/integration/sushi/{MasterChefAdapter.t.sol => masterChefV1/MasterChefV1Adapter.t.sol} (74%) rename test/vault/integration/sushi/{MasterChefTestConfigStorage.sol => masterChefV1/MasterChefV1TestConfigStorage.sol} (52%) create mode 100644 test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol create mode 100644 test/vault/integration/sushi/masterChefV2/MasterChefV2TestConfigStorage.sol diff --git a/src/vault/adapter/sushi/IMasterChef.sol b/src/vault/adapter/sushi/masterChefV1/IMasterChefV1.sol similarity index 89% rename from src/vault/adapter/sushi/IMasterChef.sol rename to src/vault/adapter/sushi/masterChefV1/IMasterChefV1.sol index 41fb1df..7063cb7 100644 --- a/src/vault/adapter/sushi/IMasterChef.sol +++ b/src/vault/adapter/sushi/masterChefV1/IMasterChefV1.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.15; -interface IMasterChef { +interface IMasterChefV1 { struct PoolInfo { address lpToken; uint256 allocPoint; @@ -16,9 +16,9 @@ interface IMasterChef { uint256 rewardDebt; } - function poolInfo(uint256 pid) external view returns (IMasterChef.PoolInfo memory); + function poolInfo(uint256 pid) external view returns (IMasterChefV1.PoolInfo memory); - function userInfo(uint256 pid, address adapterAddress) external view returns (IMasterChef.UserInfo memory); + function userInfo(uint256 pid, address adapterAddress) external view returns (IMasterChefV1.UserInfo memory); function totalAllocPoint() external view returns (uint256); diff --git a/src/vault/adapter/sushi/MasterChefAdapter.sol b/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol similarity index 84% rename from src/vault/adapter/sushi/MasterChefAdapter.sol rename to src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol index 89413df..0dc092e 100644 --- a/src/vault/adapter/sushi/MasterChefAdapter.sol +++ b/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol @@ -3,18 +3,18 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../abstracts/WithRewards.sol"; -import { IMasterChef } from "./IMasterChef.sol"; +import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../../abstracts/AdapterBase.sol"; +import { WithRewards, IWithRewards } from "../../abstracts/WithRewards.sol"; +import { IMasterChefV1 } from "./IMasterChefV1.sol"; /** - * @title MasterChef Adapter - * @notice ERC4626 wrapper for MasterChef Vaults. + * @title MasterChefV1 Adapter + * @notice ERC4626 wrapper for MasterChefV1 Vaults. * - * An ERC4626 compliant Wrapper for https://github.com/sushiswap/sushiswap/blob/archieve/canary/contracts/MasterChefV2.sol. - * Allows wrapping MasterChef Vaults. + * An ERC4626 compliant Wrapper for https://github.com/sushiswap/sushiswap/blob/archieve/canary/contracts/MasterChef.sol. + * Allows wrapping MasterChefV1 Vaults. */ -contract MasterChefAdapter is AdapterBase, WithRewards { +contract MasterChefV1Adapter is AdapterBase, WithRewards { using SafeERC20 for IERC20; using Math for uint256; @@ -22,7 +22,7 @@ contract MasterChefAdapter is AdapterBase, WithRewards { string internal _symbol; // @notice The MasterChef contract - IMasterChef public masterChef; + IMasterChefV1 public masterChef; // @notice The address of the reward token address public rewardsToken; @@ -53,8 +53,8 @@ contract MasterChefAdapter is AdapterBase, WithRewards { (uint256 _pid, address _rewardsToken) = abi.decode(masterchefInitData, (uint256, address)); - masterChef = IMasterChef(registry); - IMasterChef.PoolInfo memory pool = masterChef.poolInfo(_pid); + masterChef = IMasterChefV1(registry); + IMasterChefV1.PoolInfo memory pool = masterChef.poolInfo(_pid); if (pool.lpToken != asset()) revert InvalidAsset(); @@ -83,7 +83,7 @@ contract MasterChefAdapter is AdapterBase, WithRewards { /// @return The total amount of underlying tokens the Vault holds. function _totalAssets() internal view override returns (uint256) { - IMasterChef.UserInfo memory user = masterChef.userInfo(pid, address(this)); + IMasterChefV1.UserInfo memory user = masterChef.userInfo(pid, address(this)); return user.amount; } diff --git a/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol b/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol new file mode 100644 index 0000000..c84e908 --- /dev/null +++ b/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +interface IMasterChefV2 { + /// @notice Info of each MCV2 user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt` The amount of SUSHI entitled to the user. + struct UserInfo { + uint256 amount; + int256 rewardDebt; + } + + /// @notice Info of each MCV2 pool. + /// `allocPoint` The amount of allocation points assigned to the pool. + /// Also known as the amount of SUSHI to distribute per block. + struct PoolInfo { + uint128 accSushiPerShare; + uint64 lastRewardBlock; + uint64 allocPoint; + } + + function poolInfo( + uint256 pid + ) external view returns (IMasterChefV2.PoolInfo memory); + + function userInfo( + uint256 pid, + address adapterAddress + ) external view returns (IMasterChefV2.UserInfo memory); + + function lpToken(uint256 pid) external view returns (address); + + function totalAllocPoint() external view returns (uint256); + + function deposit(uint256 _pid, uint256 _amount, address _to) external; + + function withdraw(uint256 _pid, uint256 _amount, address _to) external; + + function harvest(uint256 _pid, address _to) external; + + function pendingSushi( + uint256 _pid, + address _user + ) external view returns (uint256); +} diff --git a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol new file mode 100644 index 0000000..2f51733 --- /dev/null +++ b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../../abstracts/WithRewards.sol"; +import {IMasterChefV2} from "./IMasterChefV2.sol"; + +/** + * @title MasterChefV2 Adapter + * @notice ERC4626 wrapper for MasterChefV2 Vaults. + * + * An ERC4626 compliant Wrapper for https://github.com/sushiswap/sushiswap/blob/archieve/canary/contracts/MasterChefV2.sol. + * Allows wrapping MasterChefV2 Vaults. + */ +contract MasterChefV2Adapter is AdapterBase, WithRewards { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + // @notice The MasterChef contract + IMasterChefV2 public masterChef; + + // @notice The address of the reward token + address public rewardsToken; + + // @notice The pool ID + uint256 public pid; + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + error InvalidAsset(); + + /** + * @notice Initialize a new MasterChef Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @dev `_pid` - The poolId for lpToken. + * @dev `_rewardsToken` - The token rewarded by the MasterChef contract (Sushi, Cake...) + * @dev This function is called by the factory contract when deploying a new vault. + */ + + function initialize( + bytes memory adapterInitData, + address registry, + bytes memory masterchefInitData + ) external initializer { + __AdapterBase_init(adapterInitData); + + (uint256 _pid, address _rewardsToken) = abi.decode( + masterchefInitData, + (uint256, address) + ); + + masterChef = IMasterChefV2(registry); + address lpToken = masterChef.lpToken(_pid); + + if (lpToken != asset()) revert InvalidAsset(); + + pid = _pid; + rewardsToken = _rewardsToken; + + _name = string.concat( + "Popcorn MasterChef", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + IERC20(lpToken).approve(address(masterChef), type(uint256).max); + } + + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } + + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculates the total amount of underlying tokens the Vault holds. + /// @return The total amount of underlying tokens the Vault holds. + + function _totalAssets() internal view override returns (uint256) { + IMasterChefV2.UserInfo memory user = masterChef.userInfo( + pid, + address(this) + ); + return user.amount; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit(uint256 amount, uint256) internal override { + masterChef.deposit(pid, amount, address(this)); + } + + function _protocolWithdraw(uint256 amount, uint256) internal override { + masterChef.withdraw(pid, amount, address(this)); + } + + /*////////////////////////////////////////////////////////////// + STRATEGY LOGIC + //////////////////////////////////////////////////////////////*/ + /// @notice Claim rewards from the masterChef + function claim() public override onlyStrategy { + masterChef.harvest(pid, address(this)); + } + + /// @notice The token rewarded + function rewardTokens() external view override returns (address[] memory) { + address[] memory _rewardTokens = new address[](1); + _rewardTokens[0] = rewardsToken; + return _rewardTokens; + } + + /*////////////////////////////////////////////////////////////// + EIP-165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } +} diff --git a/test/vault/integration/aaveV2/AaveV2Adapter.t.sol b/test/vault/integration/aave/aaveV2/AaveV2Adapter.t.sol similarity index 94% rename from test/vault/integration/aaveV2/AaveV2Adapter.t.sol rename to test/vault/integration/aave/aaveV2/AaveV2Adapter.t.sol index e24b8db..60c0b12 100644 --- a/test/vault/integration/aaveV2/AaveV2Adapter.t.sol +++ b/test/vault/integration/aave/aaveV2/AaveV2Adapter.t.sol @@ -5,10 +5,10 @@ pragma solidity ^0.8.15; import { Test } from "forge-std/Test.sol"; -import { AaveV2Adapter, SafeERC20, IERC20, IERC20Metadata, Math, ILendingPool, IAaveMining, IAToken, IProtocolDataProvider, DataTypes, IStrategy, IWithRewards } from "../../../../src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol"; +import { AaveV2Adapter, SafeERC20, IERC20, IERC20Metadata, Math, ILendingPool, IAaveMining, IAToken, IProtocolDataProvider, DataTypes, IStrategy, IWithRewards } from "../../../../../src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol"; import { AaveV2TestConfigStorage, AaveV2TestConfig } from "./AaveV2TestConfigStorage.sol"; -import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../abstract/AbstractAdapterTest.sol"; -import { MockStrategyClaimer } from "../../../utils/mocks/MockStrategyClaimer.sol"; +import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../../abstract/AbstractAdapterTest.sol"; +import { MockStrategyClaimer } from "../../../../utils/mocks/MockStrategyClaimer.sol"; contract AaveV2AdapterTest is AbstractAdapterTest { using Math for uint256; diff --git a/test/vault/integration/aaveV2/AaveV2TestConfigStorage.sol b/test/vault/integration/aave/aaveV2/AaveV2TestConfigStorage.sol similarity index 90% rename from test/vault/integration/aaveV2/AaveV2TestConfigStorage.sol rename to test/vault/integration/aave/aaveV2/AaveV2TestConfigStorage.sol index af1dc8e..06d7d04 100644 --- a/test/vault/integration/aaveV2/AaveV2TestConfigStorage.sol +++ b/test/vault/integration/aave/aaveV2/AaveV2TestConfigStorage.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.15; -import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; +import { ITestConfigStorage } from "../../abstract/ITestConfigStorage.sol"; struct AaveV2TestConfig { address asset; diff --git a/test/vault/integration/aaveV3/AaveV3Adapter.t.sol b/test/vault/integration/aave/aaveV3/AaveV3Adapter.t.sol similarity index 97% rename from test/vault/integration/aaveV3/AaveV3Adapter.t.sol rename to test/vault/integration/aave/aaveV3/AaveV3Adapter.t.sol index ce4a3fb..4445ad9 100644 --- a/test/vault/integration/aaveV3/AaveV3Adapter.t.sol +++ b/test/vault/integration/aave/aaveV3/AaveV3Adapter.t.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.15; import { Test } from "forge-std/Test.sol"; -import { AaveV3Adapter, SafeERC20, IERC20, IERC20Metadata, Math, ILendingPool, IAaveIncentives, IAToken, IProtocolDataProvider, DataTypes } from "../../../../src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol"; +import { AaveV3Adapter, SafeERC20, IERC20, IERC20Metadata, Math, ILendingPool, IAaveIncentives, IAToken, IProtocolDataProvider, DataTypes } from "../../../../../src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol"; import { AaveV3TestConfigStorage, AaveV3TestConfig } from "./AaveV3TestConfigStorage.sol"; -import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../abstract/AbstractAdapterTest.sol"; +import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../../abstract/AbstractAdapterTest.sol"; contract AaveV3AdapterTest is AbstractAdapterTest { using Math for uint256; diff --git a/test/vault/integration/aaveV3/AaveV3TestConfigStorage.sol b/test/vault/integration/aave/aaveV3/AaveV3TestConfigStorage.sol similarity index 90% rename from test/vault/integration/aaveV3/AaveV3TestConfigStorage.sol rename to test/vault/integration/aave/aaveV3/AaveV3TestConfigStorage.sol index cc04b20..23b33a5 100644 --- a/test/vault/integration/aaveV3/AaveV3TestConfigStorage.sol +++ b/test/vault/integration/aave/aaveV3/AaveV3TestConfigStorage.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.15; -import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; +import { ITestConfigStorage } from "../../abstract/ITestConfigStorage.sol"; struct AaveV3TestConfig { address asset; diff --git a/test/vault/integration/sushi/MasterChefAdapter.t.sol b/test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol similarity index 74% rename from test/vault/integration/sushi/MasterChefAdapter.t.sol rename to test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol index d94a9c4..b86b1da 100644 --- a/test/vault/integration/sushi/MasterChefAdapter.t.sol +++ b/test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol @@ -3,15 +3,15 @@ pragma solidity ^0.8.15; import { Test } from "forge-std/Test.sol"; -import { MasterChefAdapter, SafeERC20, IERC20, IERC20Metadata, Math, IMasterChef, IStrategy, IAdapter, IWithRewards } from "../../../../src/vault/adapter/sushi/MasterChefAdapter.sol"; -import { MasterChefTestConfigStorage, MasterChefTestConfig } from "./MasterChefTestConfigStorage.sol"; -import { AbstractAdapterTest, ITestConfigStorage } from "../abstract/AbstractAdapterTest.sol"; -import { MockStrategyClaimer } from "../../../utils/mocks/MockStrategyClaimer.sol"; +import { MasterChefV1Adapter, SafeERC20, IERC20, IERC20Metadata, Math, IMasterChefV1, IStrategy, IAdapter, IWithRewards } from "../../../../../../src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol"; +import { MasterChefV1TestConfigStorage, MasterChefV1TestConfig } from "./MasterChefV1TestConfigStorage.sol"; +import { AbstractAdapterTest, ITestConfigStorage } from "../../abstract/AbstractAdapterTest.sol"; +import { MockStrategyClaimer } from "../../../../utils/mocks/MockStrategyClaimer.sol"; -contract MasterChefAdapterTest is AbstractAdapterTest { +contract MasterChefV1AdapterTest is AbstractAdapterTest { using Math for uint256; - IMasterChef public masterChef = IMasterChef(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd); + IMasterChefV1 public MasterChefV1 = IMasterChefV1(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd); address public rewardsToken; uint256 pid; @@ -20,7 +20,7 @@ contract MasterChefAdapterTest is AbstractAdapterTest { uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); vm.selectFork(forkId); - testConfigStorage = ITestConfigStorage(address(new MasterChefTestConfigStorage())); + testConfigStorage = ITestConfigStorage(address(new MasterChefV1TestConfigStorage())); _setUpTest(testConfigStorage.getTestConfig(0)); } @@ -34,11 +34,11 @@ contract MasterChefAdapterTest is AbstractAdapterTest { pid = _pid; rewardsToken = _rewardsToken; - IMasterChef.PoolInfo memory info = masterChef.poolInfo(_pid); + IMasterChefV1.PoolInfo memory info = MasterChefV1.poolInfo(_pid); - setUpBaseTest(IERC20(info.lpToken), address(new MasterChefAdapter()), address(masterChef), 10, "MasterChef", true); + setUpBaseTest(IERC20(info.lpToken), address(new MasterChefV1Adapter()), address(MasterChefV1), 10, "MasterChefV1", true); - vm.label(address(masterChef), "masterChef"); + vm.label(address(MasterChefV1), "MasterChefV1"); vm.label(address(asset), "asset"); vm.label(address(this), "test"); @@ -81,7 +81,7 @@ contract MasterChefAdapterTest is AbstractAdapterTest { "symbol" ); - assertEq(asset.allowance(address(adapter), address(masterChef)), type(uint256).max, "allowance"); + assertEq(asset.allowance(address(adapter), address(MasterChefV1)), type(uint256).max, "allowance"); } /*////////////////////////////////////////////////////////////// diff --git a/test/vault/integration/sushi/MasterChefTestConfigStorage.sol b/test/vault/integration/sushi/masterChefV1/MasterChefV1TestConfigStorage.sol similarity index 52% rename from test/vault/integration/sushi/MasterChefTestConfigStorage.sol rename to test/vault/integration/sushi/masterChefV1/MasterChefV1TestConfigStorage.sol index 20f159e..337fd40 100644 --- a/test/vault/integration/sushi/MasterChefTestConfigStorage.sol +++ b/test/vault/integration/sushi/masterChefV1/MasterChefV1TestConfigStorage.sol @@ -1,17 +1,17 @@ pragma solidity ^0.8.15; -import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; +import { ITestConfigStorage } from "../../abstract/ITestConfigStorage.sol"; -struct MasterChefTestConfig { +struct MasterChefV1TestConfig { uint256 pid; address rewardsToken; } -contract MasterChefTestConfigStorage is ITestConfigStorage { - MasterChefTestConfig[] internal testConfigs; +contract MasterChefV1TestConfigStorage is ITestConfigStorage { + MasterChefV1TestConfig[] internal testConfigs; constructor() { - testConfigs.push(MasterChefTestConfig(2, 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2)); + testConfigs.push(MasterChefV1TestConfig(2, 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2)); } function getTestConfig(uint256 i) public view returns (bytes memory) { diff --git a/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol b/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol new file mode 100644 index 0000000..acd2099 --- /dev/null +++ b/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import {Test} from "forge-std/Test.sol"; + +import {MasterChefV2Adapter, SafeERC20, IERC20, IERC20Metadata, Math, IMasterChefV2, IStrategy, IAdapter, IWithRewards} from "../../../../../../src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol"; +import {MasterChefV2TestConfigStorage, MasterChefV2TestConfig} from "./MasterChefV2TestConfigStorage.sol"; +import {AbstractAdapterTest, ITestConfigStorage} from "../../abstract/AbstractAdapterTest.sol"; +import {MockStrategyClaimer} from "../../../../utils/mocks/MockStrategyClaimer.sol"; + +contract MasterChefV2AdapterTest is AbstractAdapterTest { + using Math for uint256; + + IMasterChefV2 public masterChef = + IMasterChefV2(0xEF0881eC094552b2e128Cf945EF17a6752B4Ec5d); + + address public rewardsToken; + uint256 pid; + + function setUp() public { + uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); + vm.selectFork(forkId); + + testConfigStorage = ITestConfigStorage( + address(new MasterChefV2TestConfigStorage()) + ); + + _setUpTest(testConfigStorage.getTestConfig(0)); + } + + function overrideSetup(bytes memory testConfig) public override { + _setUpTest(testConfig); + } + + function _setUpTest(bytes memory testConfig) internal { + (uint256 _pid, address _rewardsToken) = abi.decode( + testConfig, + (uint256, address) + ); + + pid = _pid; + rewardsToken = _rewardsToken; + + setUpBaseTest( + IERC20(masterChef.lpToken(_pid)), + address(new MasterChefV2Adapter()), + address(masterChef), + 10, + "MasterChefV2", + true + ); + + vm.label(address(masterChef), "MasterChefV2"); + vm.label(address(asset), "asset"); + vm.label(address(this), "test"); + + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfig + ); + } + + /*////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////*/ + + // Verify that totalAssets returns the expected amount + function verify_totalAssets() public override { + deal(address(asset), bob, defaultAmount); + vm.startPrank(bob); + asset.approve(address(adapter), defaultAmount); + adapter.deposit(defaultAmount, bob); + vm.stopPrank(); + + assertEq( + adapter.totalAssets(), + adapter.convertToAssets(adapter.totalSupply()), + string.concat("totalSupply converted != totalAssets", baseTestId) + ); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function verify_adapterInit() public override { + assertEq(adapter.asset(), address(asset), "asset"); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + + assertEq( + asset.allowance(address(adapter), address(masterChef)), + type(uint256).max, + "allowance" + ); + } + + /*////////////////////////////////////////////////////////////// + CLAIM + //////////////////////////////////////////////////////////////*/ + + function test__claim() public override { + strategy = IStrategy(address(new MockStrategyClaimer())); + createAdapter(); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfigStorage.getTestConfig(0) + ); + + _mintFor(1000e18, bob); + + vm.prank(bob); + adapter.deposit(1000e18, bob); + + vm.roll(block.number + 10); + + emit log_uint(masterChef.pendingSushi(pid, address(adapter))); + + vm.prank(bob); + adapter.withdraw(1, bob, bob); + + address[] memory rewardTokens = IWithRewards(address(adapter)) + .rewardTokens(); + assertEq(rewardTokens[0], rewardsToken); + + assertGt( + IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2).balanceOf( + address(adapter) + ), + 0 + ); + assertGt( + IERC20(0x471Ea49dd8E60E697f4cac262b5fafCc307506e4).balanceOf( + address(adapter) + ), + 0 + ); + } +} diff --git a/test/vault/integration/sushi/masterChefV2/MasterChefV2TestConfigStorage.sol b/test/vault/integration/sushi/masterChefV2/MasterChefV2TestConfigStorage.sol new file mode 100644 index 0000000..f49f2a6 --- /dev/null +++ b/test/vault/integration/sushi/masterChefV2/MasterChefV2TestConfigStorage.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.8.15; + +import { ITestConfigStorage } from "../../abstract/ITestConfigStorage.sol"; + +struct MasterChefV2TestConfig { + uint256 pid; + address rewardsToken; +} + +contract MasterChefV2TestConfigStorage is ITestConfigStorage { + MasterChefV2TestConfig[] internal testConfigs; + + constructor() { + testConfigs.push(MasterChefV2TestConfig(60, 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2)); + } + + function getTestConfig(uint256 i) public view returns (bytes memory) { + return abi.encode(testConfigs[i].pid, testConfigs[i].rewardsToken); + } + + function getTestConfigLength() public view returns (uint256) { + return testConfigs.length; + } +} From 0f94ef1221fce57af18b5f6fe477297680fad351 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 12 Apr 2023 12:18:41 +0200 Subject: [PATCH 13/18] MasterChefV2 adapter working --- src/interfaces/vault/IAdapter.sol | 48 ++-- .../sushi/masterChefV2/IMasterChefV2.sol | 6 + .../masterChefV2/MasterChefV2Adapter.sol | 13 +- .../masterChefV1/MasterChefV1Adapter.t.sol | 226 ++++++++++-------- .../masterChefV2/MasterChefV2Adapter.t.sol | 5 +- 5 files changed, 171 insertions(+), 127 deletions(-) diff --git a/src/interfaces/vault/IAdapter.sol b/src/interfaces/vault/IAdapter.sol index 9278d5f..772f390 100644 --- a/src/interfaces/vault/IAdapter.sol +++ b/src/interfaces/vault/IAdapter.sol @@ -3,43 +3,45 @@ pragma solidity ^0.8.15; -import { IOwned } from "../IOwned.sol"; -import { IERC4626Upgradeable as IERC4626 } from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; -import { IPermit } from "../IPermit.sol"; -import { IPausable } from "../IPausable.sol"; +import {IOwned} from "../IOwned.sol"; +import {IERC4626Upgradeable as IERC4626} from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; +import {IPermit} from "../IPermit.sol"; +import {IPausable} from "../IPausable.sol"; interface IAdapter is IERC4626, IOwned, IPermit, IPausable { - function strategy() external view returns (address); + function strategy() external view returns (address); - function strategyConfig() external view returns (bytes memory); + function strategyConfig() external view returns (bytes memory); - function strategyDeposit(uint256 assets, uint256 shares) external; + function strategyDeposit(uint256 assets, uint256 shares) external; - function strategyWithdraw(uint256 assets, uint256 shares) external; + function strategyWithdraw(uint256 assets, uint256 shares) external; - function supportsInterface(bytes4 interfaceId) external view returns (bool); + function supportsInterface(bytes4 interfaceId) external view returns (bool); - function setPerformanceFee(uint256 fee) external; + function setPerformanceFee(uint256 fee) external; - function performanceFee() external view returns (uint256); + function performanceFee() external view returns (uint256); - function highWaterMark() external view returns (uint256); + function highWaterMark() external view returns (uint256); - function accruedPerformanceFee() external view returns (uint256); + function accruedPerformanceFee() external view returns (uint256); - function harvest() external; + function harvest() external; - function harvestCooldown() external view returns (uint256); + function lastHarvest() external view returns (uint256); - function setHarvestCooldown(uint256 harvestCooldown) external; + function harvestCooldown() external view returns (uint256); - function initialize( - bytes memory adapterBaseData, - address externalRegistry, - bytes memory adapterData - ) external; + function setHarvestCooldown(uint256 harvestCooldown) external; - function decimals() external view returns (uint8); + function initialize( + bytes memory adapterBaseData, + address externalRegistry, + bytes memory adapterData + ) external; - function decimalOffset() external view returns (uint8); + function decimals() external view returns (uint8); + + function decimalOffset() external view returns (uint8); } diff --git a/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol b/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol index c84e908..093f0a3 100644 --- a/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol +++ b/src/vault/adapter/sushi/masterChefV2/IMasterChefV2.sol @@ -32,6 +32,8 @@ interface IMasterChefV2 { function lpToken(uint256 pid) external view returns (address); + function rewarder(uint256 pid) external view returns (address); + function totalAllocPoint() external view returns (uint256); function deposit(uint256 _pid, uint256 _amount, address _to) external; @@ -45,3 +47,7 @@ interface IMasterChefV2 { address _user ) external view returns (uint256); } + +interface IRewarder { + function rewardToken() external view returns (address); +} diff --git a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol index 2f51733..f5c7669 100644 --- a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol +++ b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.15; import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../../abstracts/AdapterBase.sol"; import {WithRewards, IWithRewards} from "../../abstracts/WithRewards.sol"; -import {IMasterChefV2} from "./IMasterChefV2.sol"; +import {IMasterChefV2, IRewarder} from "./IMasterChefV2.sol"; /** * @title MasterChefV2 Adapter @@ -129,8 +129,17 @@ contract MasterChefV2Adapter is AdapterBase, WithRewards { /// @notice The token rewarded function rewardTokens() external view override returns (address[] memory) { - address[] memory _rewardTokens = new address[](1); + address rewarder = masterChef.rewarder(pid); + + address[] memory _rewardTokens; + if (rewarder == address(0)) { + _rewardTokens = new address[](1); + } else { + _rewardTokens = new address[](2); + _rewardTokens[1] = IRewarder(rewarder).rewardToken(); + } _rewardTokens[0] = rewardsToken; + return _rewardTokens; } diff --git a/test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol b/test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol index b86b1da..a3148d5 100644 --- a/test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol +++ b/test/vault/integration/sushi/masterChefV1/MasterChefV1Adapter.t.sol @@ -1,115 +1,143 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.15; -import { Test } from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; -import { MasterChefV1Adapter, SafeERC20, IERC20, IERC20Metadata, Math, IMasterChefV1, IStrategy, IAdapter, IWithRewards } from "../../../../../../src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol"; -import { MasterChefV1TestConfigStorage, MasterChefV1TestConfig } from "./MasterChefV1TestConfigStorage.sol"; -import { AbstractAdapterTest, ITestConfigStorage } from "../../abstract/AbstractAdapterTest.sol"; -import { MockStrategyClaimer } from "../../../../utils/mocks/MockStrategyClaimer.sol"; +import {MasterChefV1Adapter, SafeERC20, IERC20, IERC20Metadata, Math, IMasterChefV1, IStrategy, IAdapter, IWithRewards} from "../../../../../../src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol"; +import {MasterChefV1TestConfigStorage, MasterChefV1TestConfig} from "./MasterChefV1TestConfigStorage.sol"; +import {AbstractAdapterTest, ITestConfigStorage} from "../../abstract/AbstractAdapterTest.sol"; +import {MockStrategyClaimer} from "../../../../utils/mocks/MockStrategyClaimer.sol"; contract MasterChefV1AdapterTest is AbstractAdapterTest { - using Math for uint256; - - IMasterChefV1 public MasterChefV1 = IMasterChefV1(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd); - - address public rewardsToken; - uint256 pid; - - function setUp() public { - uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); - vm.selectFork(forkId); - - testConfigStorage = ITestConfigStorage(address(new MasterChefV1TestConfigStorage())); - - _setUpTest(testConfigStorage.getTestConfig(0)); - } - - function overrideSetup(bytes memory testConfig) public override { - _setUpTest(testConfig); - } - - function _setUpTest(bytes memory testConfig) internal { - (uint256 _pid, address _rewardsToken) = abi.decode(testConfig, (uint256, address)); - - pid = _pid; - rewardsToken = _rewardsToken; - IMasterChefV1.PoolInfo memory info = MasterChefV1.poolInfo(_pid); - - setUpBaseTest(IERC20(info.lpToken), address(new MasterChefV1Adapter()), address(MasterChefV1), 10, "MasterChefV1", true); - - vm.label(address(MasterChefV1), "MasterChefV1"); - vm.label(address(asset), "asset"); - vm.label(address(this), "test"); - - adapter.initialize(abi.encode(asset, address(this), strategy, 0, sigs, ""), externalRegistry, testConfig); - } - - /*////////////////////////////////////////////////////////////// + using Math for uint256; + + IMasterChefV1 public MasterChefV1 = + IMasterChefV1(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd); + + address public rewardsToken; + uint256 pid; + + function setUp() public { + uint256 forkId = vm.createSelectFork(vm.rpcUrl("mainnet")); + vm.selectFork(forkId); + + testConfigStorage = ITestConfigStorage( + address(new MasterChefV1TestConfigStorage()) + ); + + _setUpTest(testConfigStorage.getTestConfig(0)); + } + + function overrideSetup(bytes memory testConfig) public override { + _setUpTest(testConfig); + } + + function _setUpTest(bytes memory testConfig) internal { + (uint256 _pid, address _rewardsToken) = abi.decode( + testConfig, + (uint256, address) + ); + + pid = _pid; + rewardsToken = _rewardsToken; + IMasterChefV1.PoolInfo memory info = MasterChefV1.poolInfo(_pid); + + setUpBaseTest( + IERC20(info.lpToken), + address(new MasterChefV1Adapter()), + address(MasterChefV1), + 10, + "MasterChefV1", + true + ); + + vm.label(address(MasterChefV1), "MasterChefV1"); + vm.label(address(asset), "asset"); + vm.label(address(this), "test"); + + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfig + ); + } + + /*////////////////////////////////////////////////////////////// HELPER //////////////////////////////////////////////////////////////*/ - // Verify that totalAssets returns the expected amount - function verify_totalAssets() public override { - deal(address(asset), bob, defaultAmount); - vm.startPrank(bob); - asset.approve(address(adapter), defaultAmount); - adapter.deposit(defaultAmount, bob); - vm.stopPrank(); - - assertEq( - adapter.totalAssets(), - adapter.convertToAssets(adapter.totalSupply()), - string.concat("totalSupply converted != totalAssets", baseTestId) - ); - } - - /*////////////////////////////////////////////////////////////// + // Verify that totalAssets returns the expected amount + function verify_totalAssets() public override { + deal(address(asset), bob, defaultAmount); + vm.startPrank(bob); + asset.approve(address(adapter), defaultAmount); + adapter.deposit(defaultAmount, bob); + vm.stopPrank(); + + assertEq( + adapter.totalAssets(), + adapter.convertToAssets(adapter.totalSupply()), + string.concat("totalSupply converted != totalAssets", baseTestId) + ); + } + + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ - function verify_adapterInit() public override { - assertEq(adapter.asset(), address(asset), "asset"); - assertEq( - IERC20Metadata(address(adapter)).symbol(), - string.concat("popB-", IERC20Metadata(address(asset)).symbol()), - "symbol" - ); - assertEq( - IERC20Metadata(address(adapter)).symbol(), - string.concat("popB-", IERC20Metadata(address(asset)).symbol()), - "symbol" - ); - - assertEq(asset.allowance(address(adapter), address(MasterChefV1)), type(uint256).max, "allowance"); - } - - /*////////////////////////////////////////////////////////////// + function verify_adapterInit() public override { + assertEq(adapter.asset(), address(asset), "asset"); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + + assertEq( + asset.allowance(address(adapter), address(MasterChefV1)), + type(uint256).max, + "allowance" + ); + } + + /*////////////////////////////////////////////////////////////// CLAIM //////////////////////////////////////////////////////////////*/ - function test__claim() public override { - strategy = IStrategy(address(new MockStrategyClaimer())); - createAdapter(); - adapter.initialize( - abi.encode(asset, address(this), strategy, 0, sigs, ""), - externalRegistry, - testConfigStorage.getTestConfig(0) - ); - - _mintFor(1000e18, bob); - - vm.prank(bob); - adapter.deposit(1000e18, bob); - - vm.roll(block.number + 30); - - vm.prank(bob); - adapter.withdraw(0, bob, bob); - - address[] memory rewardTokens = IWithRewards(address(adapter)).rewardTokens(); - assertEq(rewardTokens[0], rewardsToken); - - assertGt(IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2).balanceOf(address(adapter)), 0); - } + function test__claim() public override { + strategy = IStrategy(address(new MockStrategyClaimer())); + createAdapter(); + adapter.initialize( + abi.encode(asset, address(this), strategy, 0, sigs, ""), + externalRegistry, + testConfigStorage.getTestConfig(0) + ); + + _mintFor(1000e18, bob); + + vm.prank(bob); + adapter.deposit(1000e18, bob); + + vm.roll(block.number + 30); + vm.warp(block.timestamp + 2); + + vm.prank(bob); + adapter.withdraw(0, bob, bob); + + address[] memory rewardTokens = IWithRewards(address(adapter)) + .rewardTokens(); + assertEq(rewardTokens[0], rewardsToken); + + assertGt( + IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2).balanceOf( + address(adapter) + ), + 0 + ); + } } diff --git a/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol b/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol index acd2099..9948296 100644 --- a/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol +++ b/test/vault/integration/sushi/masterChefV2/MasterChefV2Adapter.t.sol @@ -122,9 +122,8 @@ contract MasterChefV2AdapterTest is AbstractAdapterTest { vm.prank(bob); adapter.deposit(1000e18, bob); - vm.roll(block.number + 10); - - emit log_uint(masterChef.pendingSushi(pid, address(adapter))); + vm.roll(block.number + 3000); + vm.warp(block.timestamp + 200); vm.prank(bob); adapter.withdraw(1, bob, bob); From 1819f2bf9e0d116b03e9f4d6e156267d9c7ab4bf Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 12 Apr 2023 12:21:47 +0200 Subject: [PATCH 14/18] fixed compoundV2 rewardsCheck --- .../compound/compoundV2/CompoundV2Adapter.sol | 240 ++++++++++-------- .../compound/compoundV2/ICompoundV2.sol | 103 ++++---- 2 files changed, 189 insertions(+), 154 deletions(-) diff --git a/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol b/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol index 36f5f78..8926815 100644 --- a/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol +++ b/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../../abstracts/WithRewards.sol"; -import { ICToken, IComptroller } from "./ICompoundV2.sol"; -import { LibCompound } from "./LibCompound.sol"; +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../../abstracts/WithRewards.sol"; +import {ICToken, IComptroller} from "./ICompoundV2.sol"; +import {LibCompound} from "./LibCompound.sol"; /** * @title CompoundV2 Adapter @@ -18,131 +18,171 @@ import { LibCompound } from "./LibCompound.sol"; * Allows for additional strategies to use rewardsToken in case of an active Booster. */ contract CompoundV2Adapter is AdapterBase, WithRewards { - using SafeERC20 for IERC20; - using Math for uint256; + using SafeERC20 for IERC20; + using Math for uint256; - string internal _name; - string internal _symbol; + string internal _name; + string internal _symbol; - /// @notice The Compound cToken contract - ICToken public cToken; + /// @notice The Compound cToken contract + ICToken public cToken; - /// @notice The Compound Comptroller contract - IComptroller public comptroller; + /// @notice The Compound Comptroller contract + IComptroller public comptroller; - /// @notice Check to see if Compound liquidity mining is active on this market - bool public isActiveCompRewards; + /// @notice Check to see if Compound liquidity mining is active on this market + bool public isActiveCompRewards; - /// @notice Check to see if cToken is cETH to wrap/unwarp on deposit/withdrawal - bool public isCETH; + /// @notice Check to see if cToken is cETH to wrap/unwarp on deposit/withdrawal + bool public isCETH; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ - error DifferentAssets(address asset, address underlying); - error InvalidAsset(address asset); - - /** - * @notice Initialize a new CompoundV2 Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @param comptroller_ The Compound comptroller. - * @param compoundV2InitData Encoded data for the beefy adapter initialization. - * @dev `_cToken` - The underlying asset supplied to and wrapped by Compound. - * @dev This function is called by the factory contract when deploying a new vault. - */ - function initialize( - bytes memory adapterInitData, - address comptroller_, - bytes memory compoundV2InitData - ) external initializer { - __AdapterBase_init(adapterInitData); - - _name = string.concat("Popcorn Compound", IERC20Metadata(asset()).name(), " Adapter"); - _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); - - cToken = ICToken(abi.decode(compoundV2InitData, (address))); - if (keccak256(abi.encode(cToken.symbol())) != keccak256(abi.encode("cETH"))) { - if (cToken.underlying() != asset()) revert DifferentAssets(cToken.underlying(), asset()); + error DifferentAssets(address asset, address underlying); + error InvalidAsset(address asset); + + /** + * @notice Initialize a new CompoundV2 Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @param comptroller_ The Compound comptroller. + * @param compoundV2InitData Encoded data for the beefy adapter initialization. + * @dev `_cToken` - The underlying asset supplied to and wrapped by Compound. + * @dev This function is called by the factory contract when deploying a new vault. + */ + function initialize( + bytes memory adapterInitData, + address comptroller_, + bytes memory compoundV2InitData + ) external initializer { + __AdapterBase_init(adapterInitData); + + _name = string.concat( + "Popcorn Compound", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + cToken = ICToken(abi.decode(compoundV2InitData, (address))); + if ( + keccak256(abi.encode(cToken.symbol())) != + keccak256(abi.encode("cETH")) + ) { + if (cToken.underlying() != asset()) + revert DifferentAssets(cToken.underlying(), asset()); + } + + comptroller = IComptroller(comptroller_); + + (bool isListed, , ) = comptroller.markets(address(cToken)); + if (isListed == false) revert InvalidAsset(address(cToken)); + + IERC20(asset()).approve(address(cToken), type(uint256).max); + + isActiveCompRewards = comptroller.compSupplySpeeds(address(cToken)) > 0; } - comptroller = IComptroller(comptroller_); + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } - (bool isListed, , ) = comptroller.markets(address(cToken)); - if (isListed == false) revert InvalidAsset(address(cToken)); + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } - IERC20(asset()).approve(address(cToken), type(uint256).max); + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ - uint256 compSpeed = comptroller.compSpeeds(address(cToken)); - isActiveCompRewards = compSpeed > 0 ? true : false; - } + function _totalAssets() internal view override returns (uint256) { + return _viewUnderlyingBalanceOf(address(cToken), address(this)); + } - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } + function _viewUnderlyingBalanceOf( + address token, + address user + ) internal view returns (uint256) { + ICToken token = ICToken(token); + return LibCompound.viewUnderlyingBalanceOf(token, user); + } - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } + /// @notice The amount of compound shares to withdraw given an mount of adapter shares + function convertToUnderlyingShares( + uint256 assets, + uint256 shares + ) public view override returns (uint256) { + uint256 supply = totalSupply(); + return + supply == 0 + ? shares + : shares.mulDiv( + cToken.balanceOf(address(this)), + supply, + Math.Rounding.Up + ); + } - /*////////////////////////////////////////////////////////////// - ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ + /// @notice The token rewarded if compound liquidity mining is active + function rewardTokens() external view override returns (address[] memory) { + address[] memory _rewardTokens = new address[](1); + if (isActiveCompRewards == false) return _rewardTokens; + _rewardTokens[0] = comptroller.getCompAddress(); + } - function _totalAssets() internal view override returns (uint256) { - return _viewUnderlyingBalanceOf(address(cToken), address(this)); - } - - function _viewUnderlyingBalanceOf(address token, address user) internal view returns (uint256) { - ICToken token = ICToken(token); - return LibCompound.viewUnderlyingBalanceOf(token, user); - } - - /// @notice The amount of compound shares to withdraw given an mount of adapter shares - function convertToUnderlyingShares(uint256 assets, uint256 shares) public view override returns (uint256) { - uint256 supply = totalSupply(); - return supply == 0 ? shares : shares.mulDiv(cToken.balanceOf(address(this)), supply, Math.Rounding.Up); - } - - /// @notice The token rewarded if compound liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - address[] memory _rewardTokens = new address[](1); - if (isActiveCompRewards == false) return _rewardTokens; - _rewardTokens[0] = comptroller.getCompAddress(); - } - - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Deposit into compound cToken contract - function _protocolDeposit(uint256 amount, uint256) internal virtual override { - cToken.mint(amount); - } + /// @notice Deposit into compound cToken contract + function _protocolDeposit( + uint256 amount, + uint256 + ) internal virtual override { + cToken.mint(amount); + } - /// @notice Withdraw from compound cToken contract - function _protocolWithdraw(uint256, uint256 shares) internal virtual override { - uint256 compoundShares = convertToUnderlyingShares(0, shares); - cToken.redeem(compoundShares); - } + /// @notice Withdraw from compound cToken contract + function _protocolWithdraw( + uint256, + uint256 shares + ) internal virtual override { + uint256 compoundShares = convertToUnderlyingShares(0, shares); + cToken.redeem(compoundShares); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - error IncentivesNotActive(); + error IncentivesNotActive(); - /// @notice Claim additional rewards given that it's active. - function claim() public override onlyStrategy { - if (isActiveCompRewards == false) revert IncentivesNotActive(); - comptroller.claimComp(address(this)); - } + /// @notice Claim additional rewards given that it's active. + function claim() public override onlyStrategy { + if (isActiveCompRewards == false) revert IncentivesNotActive(); + comptroller.claimComp(address(this)); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/compound/compoundV2/ICompoundV2.sol b/src/vault/adapter/compound/compoundV2/ICompoundV2.sol index b7093da..b9acde4 100644 --- a/src/vault/adapter/compound/compoundV2/ICompoundV2.sol +++ b/src/vault/adapter/compound/compoundV2/ICompoundV2.sol @@ -2,76 +2,71 @@ pragma solidity ^0.8.15; interface ICToken { - /** - * @dev Returns the address of the underlying asset of this cToken - **/ - function underlying() external view returns (address); + /** + * @dev Returns the address of the underlying asset of this cToken + **/ + function underlying() external view returns (address); - /** - * @dev Returns the symbol of this cToken - **/ - function symbol() external view returns (string memory); + /** + * @dev Returns the symbol of this cToken + **/ + function symbol() external view returns (string memory); - /** - * @dev Returns the address of the comptroller - **/ - function comptroller() external view returns (address); + /** + * @dev Returns the address of the comptroller + **/ + function comptroller() external view returns (address); - function balanceOf(address) external view returns (uint256); + function balanceOf(address) external view returns (uint256); - /** - * @dev Send underlying to mint cToken. - **/ - function mint(uint256) external; + /** + * @dev Send underlying to mint cToken. + **/ + function mint(uint256) external; - function redeem(uint256) external; + function redeem(uint256) external; - /** - * @dev Returns exchange rate from the underlying to the cToken. - **/ - function exchangeRateStored() external view returns (uint256); + /** + * @dev Returns exchange rate from the underlying to the cToken. + **/ + function exchangeRateStored() external view returns (uint256); - function getCash() external view returns (uint256); + function getCash() external view returns (uint256); - function totalBorrows() external view returns (uint256); + function totalBorrows() external view returns (uint256); - function totalReserves() external view returns (uint256); + function totalReserves() external view returns (uint256); - function borrowRatePerBlock() external view returns (uint256); + function borrowRatePerBlock() external view returns (uint256); - function reserveFactorMantissa() external view returns (uint256); + function reserveFactorMantissa() external view returns (uint256); - function totalSupply() external view returns (uint256); + function totalSupply() external view returns (uint256); - function accrualBlockNumber() external view returns (uint256); + function accrualBlockNumber() external view returns (uint256); - function balanceOfUnderlying(address owner) external view returns (uint256); + function balanceOfUnderlying(address owner) external view returns (uint256); - function exchangeRateCurrent() external; + function exchangeRateCurrent() external; } interface IComptroller { - /** - * @dev Returns the address of the underlying asset of this cToken - **/ - function getCompAddress() external view returns (address); - - /** - * @dev Returns the address of the underlying asset of this cToken - **/ - function compSpeeds(address) external view returns (uint256); - - /** - * @dev Returns the isListed, collateralFactorMantissa, and isCompred of the cToken market - **/ - function markets(address) - external - view - returns ( - bool, - uint256, - bool - ); - - function claimComp(address holder) external; + /** + * @dev Returns the address of the underlying asset of this cToken + **/ + function getCompAddress() external view returns (address); + + /** + * @dev Returns the address of the underlying asset of this cToken + **/ + function compSpeeds(address) external view returns (uint256); + + function compSupplySpeeds(address) external view returns (uint256); + + /** + * @dev Returns the isListed, collateralFactorMantissa, and isCompred of the cToken market + **/ + function markets(address) external view returns (bool, uint256, bool); + + function claimComp(address holder) external; } From afaa6ac32e5d678c6cbb688719bd1d586d383b6c Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 12 Apr 2023 12:59:27 +0200 Subject: [PATCH 15/18] renamed missmatch to mismatch --- test/vault/PermissionRegistry.t.sol | 2 +- test/vault/Vault.t.sol | 2 +- test/vault/VaultController.t.sol | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/vault/PermissionRegistry.t.sol b/test/vault/PermissionRegistry.t.sol index 6621784..2ba1731 100644 --- a/test/vault/PermissionRegistry.t.sol +++ b/test/vault/PermissionRegistry.t.sol @@ -52,7 +52,7 @@ contract PermissionRegistryTest is Test { assertTrue(registry.rejected(target2)); } - function testFail__setPermissions_array_missmatch() public { + function testFail__setPermissions_array_mismatch() public { targets.push(target1); registry.setPermissions(targets, newPermissions); } diff --git a/test/vault/Vault.t.sol b/test/vault/Vault.t.sol index 6fc0a6b..050ddae 100644 --- a/test/vault/Vault.t.sol +++ b/test/vault/Vault.t.sol @@ -708,7 +708,7 @@ contract VaultTest is Test { vault.proposeAdapter(IERC4626(address(newAdapter))); } - function testFail__proposeAdapter_asset_missmatch() public { + function testFail__proposeAdapter_asset_mismatch() public { MockERC20 newAsset = new MockERC20("New Mock Token", "NTKN", 18); MockERC4626 newAdapter = _createAdapter(IERC20(address(newAsset))); diff --git a/test/vault/VaultController.t.sol b/test/vault/VaultController.t.sol index f1a4abe..e4e52e0 100644 --- a/test/vault/VaultController.t.sol +++ b/test/vault/VaultController.t.sol @@ -874,7 +874,7 @@ contract VaultControllerTest is Test { assertEq(IVault(vault).proposedAdapterTime(), callTime); } - function testFail__proposeVaultAdapters_missmatching_arrays() public { + function testFail__proposeVaultAdapters_mismatching_arrays() public { address[] memory targets = new address[](2); IERC4626[] memory adapters = new IERC4626[](1); @@ -962,7 +962,7 @@ contract VaultControllerTest is Test { assertEq(IVault(vault).proposedFeeTime(), callTime); } - function testFail__proposeVaultFees_missmatching_arrays() public { + function testFail__proposeVaultFees_mismatching_arrays() public { address[] memory targets = new address[](2); VaultFees[] memory fees = new VaultFees[](1); @@ -1028,7 +1028,7 @@ contract VaultControllerTest is Test { assertEq(IVault(vault).quitPeriod(), 1 days); } - function testFail__setVaultQuitPeriods_missmatching_arrays() public { + function testFail__setVaultQuitPeriods_mismatching_arrays() public { address[] memory targets = new address[](2); uint256[] memory quitPeriods = new uint256[](1); @@ -1070,7 +1070,7 @@ contract VaultControllerTest is Test { assertEq(IVault(vault).feeRecipient(), address(0x44444)); } - function testFail__setVaultFeeRecipients_missmatching_arrays() public { + function testFail__setVaultFeeRecipients_mismatching_arrays() public { address[] memory targets = new address[](2); address[] memory feeRecipients = new address[](1); @@ -1110,7 +1110,7 @@ contract VaultControllerTest is Test { assertEq(IVault(vault).depositLimit(), uint256(10)); } - function testFail__setVaultDepositLimits_missmatching_arrays() public { + function testFail__setVaultDepositLimits_mismatching_arrays() public { address[] memory targets = new address[](2); uint256[] memory depositLimits = new uint256[](1); @@ -1192,7 +1192,7 @@ contract VaultControllerTest is Test { assertEq(uint256(IMultiRewardStaking(staking).escrowInfos(iRewardToken2).offset), 1 days); } - function testFail__addStakingRewardsTokens_missmatching_arrays() public { + function testFail__addStakingRewardsTokens_mismatching_arrays() public { address[] memory targets = new address[](2); bytes[] memory rewardsData = new bytes[](1); @@ -1292,7 +1292,7 @@ contract VaultControllerTest is Test { assertEq(IMultiRewardStaking(staking).rewardInfos(iRewardToken).rewardsPerSecond, 0.2 ether); } - function testFail__changeStakingRewardsSpeeds_missmatching_arrays() public { + function testFail__changeStakingRewardsSpeeds_mismatching_arrays() public { address[] memory targets = new address[](1); IERC20[] memory rewardTokens = new IERC20[](2); uint160[] memory rewardSpeeds = new uint160[](2); @@ -1346,7 +1346,7 @@ contract VaultControllerTest is Test { assertEq(uint256(IMultiRewardStaking(staking).rewardInfos(iRewardToken).rewardsEndTimestamp), callTimestamp + 110); } - function testFail__fundStakingRewards_missmatching_arrays() public { + function testFail__fundStakingRewards_mismatching_arrays() public { address[] memory targets = new address[](2); IERC20[] memory rewardTokens = new IERC20[](1); uint256[] memory amounts = new uint256[](1); @@ -1368,7 +1368,7 @@ contract VaultControllerTest is Test { assertEq(escrow.fees(iRewardToken).feePerc, 1e14); } - function testFail__setEscrowTokenFees_missmatching_arrays() public { + function testFail__setEscrowTokenFees_mismatching_arrays() public { IERC20[] memory targets = new IERC20[](2); uint256[] memory fees = new uint256[](1); @@ -1440,7 +1440,7 @@ contract VaultControllerTest is Test { assertTrue(template.endorsed); } - function testFail__toggleTemplateEndorsements_missmatching_arrays() public { + function testFail__toggleTemplateEndorsements_mismatching_arrays() public { bytes32[] memory templateCategories = new bytes32[](1); templateCategories[0] = templateCategory; bytes32[] memory templateIds = new bytes32[](1); From c34007fae84341728a5384a8216acff9c6d74c8f Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 12 Apr 2023 12:59:41 +0200 Subject: [PATCH 16/18] renamed missmatch to mismatch 2 --- src/vault/VaultController.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vault/VaultController.sol b/src/vault/VaultController.sol index 2974712..2d3ae54 100644 --- a/src/vault/VaultController.sol +++ b/src/vault/VaultController.sol @@ -836,7 +836,7 @@ contract VaultController is Owned { error NotSubmitterNorOwner(address caller); error NotSubmitter(address caller); error NotAllowed(address subject); - error ArrayLengthMissmatch(); + error ArrayLengthMismatch(); /// @notice Verify that the caller is the creator of the vault or owner of `VaultController` (admin rights). function _verifyCreatorOrOwner( @@ -873,7 +873,7 @@ contract VaultController is Owned { uint256 length1, uint256 length2 ) internal pure { - if (length1 != length2) revert ArrayLengthMissmatch(); + if (length1 != length2) revert ArrayLengthMismatch(); } modifier canCreate() { From 061df2d0f1b3a604f6ee3f4fc86c62a50c255552 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 12 Apr 2023 12:59:56 +0200 Subject: [PATCH 17/18] wip - wrap claims in try-catch --- .../adapter/aave/aaveV2/AaveV2Adapter.sol | 209 ++++++----- .../adapter/aave/aaveV3/AaveV3Adapter.sol | 18 +- src/vault/adapter/beefy/BeefyAdapter.sol | 328 +++++++++++------- .../compound/compoundV2/CompoundV2Adapter.sol | 17 +- src/vault/adapter/convex/ConvexAdapter.sol | 7 +- .../masterChefV1/MasterChefV1Adapter.sol | 200 ++++++----- .../masterChefV2/MasterChefV2Adapter.sol | 7 +- 7 files changed, 443 insertions(+), 343 deletions(-) diff --git a/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol b/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol index cddbd20..cb87a5b 100644 --- a/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol +++ b/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../../abstracts/WithRewards.sol"; -import { ILendingPool, IAaveMining, IAToken, IProtocolDataProvider } from "./IAaveV2.sol"; -import { DataTypes } from "./lib.sol"; +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../../abstracts/WithRewards.sol"; +import {ILendingPool, IAaveMining, IAToken, IProtocolDataProvider} from "./IAaveV2.sol"; +import {DataTypes} from "./lib.sol"; /** * @title AaveV2 Adapter @@ -19,124 +19,149 @@ import { DataTypes } from "./lib.sol"; */ contract AaveV2Adapter is AdapterBase, WithRewards { - using SafeERC20 for IERC20; - using Math for uint256; + using SafeERC20 for IERC20; + using Math for uint256; - string internal _name; - string internal _symbol; + string internal _name; + string internal _symbol; - // @notice The Aave aToken contract - IAToken public aToken; + // @notice The Aave aToken contract + IAToken public aToken; - // @notice The Aave liquidity mining contract - IAaveMining public aaveMining; + // @notice The Aave liquidity mining contract + IAaveMining public aaveMining; - // @notice Check to see if Aave liquidity mining is active - bool public isActiveMining; + // @notice Check to see if Aave liquidity mining is active + bool public isActiveMining; - // @notice The Aave LendingPool contract - ILendingPool public lendingPool; + // @notice The Aave LendingPool contract + ILendingPool public lendingPool; - uint256 internal constant RAY = 1e27; - uint256 internal constant halfRAY = RAY / 2; + uint256 internal constant RAY = 1e27; + uint256 internal constant halfRAY = RAY / 2; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - error DifferentAssets(address asset, address underlying); - - /** - * @notice Initialize a new AaveV2 Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @param aaveDataProvider Encoded data for the base adapter initialization. - * @dev This function is called by the factory contract when deploying a new vault. - */ - function initialize( - bytes memory adapterInitData, - address aaveDataProvider, - bytes memory - ) external initializer { - __AdapterBase_init(adapterInitData); - - _name = string.concat("Popcorn AaveV2", IERC20Metadata(asset()).name(), " Adapter"); - _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); - - (address _aToken, , ) = IProtocolDataProvider(aaveDataProvider).getReserveTokensAddresses(asset()); - aToken = IAToken(_aToken); - if (aToken.UNDERLYING_ASSET_ADDRESS() != asset()) - revert DifferentAssets(aToken.UNDERLYING_ASSET_ADDRESS(), asset()); - - lendingPool = ILendingPool(aToken.POOL()); - aaveMining = IAaveMining(aToken.getIncentivesController()); - - IERC20(asset()).approve(address(lendingPool), type(uint256).max); - - uint128 emission; - if (address(aaveMining) != address(0)) { - (, emission, ) = aaveMining.assets(asset()); + error DifferentAssets(address asset, address underlying); + + /** + * @notice Initialize a new AaveV2 Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @param aaveDataProvider Encoded data for the base adapter initialization. + * @dev This function is called by the factory contract when deploying a new vault. + */ + function initialize( + bytes memory adapterInitData, + address aaveDataProvider, + bytes memory + ) external initializer { + __AdapterBase_init(adapterInitData); + + _name = string.concat( + "Popcorn AaveV2", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + (address _aToken, , ) = IProtocolDataProvider(aaveDataProvider) + .getReserveTokensAddresses(asset()); + aToken = IAToken(_aToken); + if (aToken.UNDERLYING_ASSET_ADDRESS() != asset()) + revert DifferentAssets(aToken.UNDERLYING_ASSET_ADDRESS(), asset()); + + lendingPool = ILendingPool(aToken.POOL()); + aaveMining = IAaveMining(aToken.getIncentivesController()); + + IERC20(asset()).approve(address(lendingPool), type(uint256).max); } - isActiveMining = emission > 0 ? true : false; - } - - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - function _totalAssets() internal view override returns (uint256) { - return aToken.balanceOf(address(this)); - } + function _totalAssets() internal view override returns (uint256) { + return aToken.balanceOf(address(this)); + } - /// @notice The token rewarded if the aave liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - address[] memory _rewardTokens = new address[](1); - if (isActiveMining == false) return _rewardTokens; - _rewardTokens[0] = aaveMining.REWARD_TOKEN(); - return _rewardTokens; - } + /// @notice The token rewarded if the aave liquidity mining is active + function rewardTokens() + external + view + override + returns (address[] memory _rewardTokens) + { + _rewardTokens = new address[](1); + if (address(aaveMining) != address(0)) { + _rewardTokens[0] = aaveMining.REWARD_TOKEN(); + } + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Deposit into aave lending pool - function _protocolDeposit(uint256 assets, uint256) internal virtual override { - lendingPool.deposit(asset(), assets, address(this), 0); - } + /// @notice Deposit into aave lending pool + function _protocolDeposit( + uint256 assets, + uint256 + ) internal virtual override { + lendingPool.deposit(asset(), assets, address(this), 0); + } - /// @notice Withdraw from lending pool - function _protocolWithdraw(uint256 assets, uint256) internal virtual override { - lendingPool.withdraw(asset(), assets, address(this)); - } + /// @notice Withdraw from lending pool + function _protocolWithdraw( + uint256 assets, + uint256 + ) internal virtual override { + lendingPool.withdraw(asset(), assets, address(this)); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - error IncentivesNotActive(); + /// @notice Claim liquidity mining rewards given that it's active + function claim() public override onlyStrategy { + if (address(aaveMining) == address(0)) return; - /// @notice Claim liquidity mining rewards given that it's active - function claim() public override onlyStrategy { - address[] memory assets = new address[](1); - assets[0] = address(aToken); - if (isActiveMining == false) revert IncentivesNotActive(); - aaveMining.claimRewards(assets, type(uint256).max, address(this)); - } + address[] memory assets = new address[](1); + assets[0] = address(aToken); - /*////////////////////////////////////////////////////////////// + try + aaveMining.claimRewards(assets, type(uint256).max, address(this)) + {} catch {} + } + + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol b/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol index 52cec79..895e884 100644 --- a/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol +++ b/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol @@ -31,9 +31,6 @@ contract AaveV3Adapter is AdapterBase, WithRewards { /// @notice The Aave liquidity mining contract IAaveIncentives public aaveIncentives; - /// @notice Array of reward tokens available for aToken. - address[] availableRewards; - /// @notice Check to see if Aave liquidity mining is active bool public isActiveIncentives; @@ -72,12 +69,6 @@ contract AaveV3Adapter is AdapterBase, WithRewards { aaveIncentives = IAaveIncentives(aToken.getIncentivesController()); IERC20(asset()).approve(address(lendingPool), type(uint256).max); - - if (address(aaveIncentives) != address(0)) { - availableRewards = aaveIncentives.getRewardsByAsset(asset()); - } - - isActiveIncentives = availableRewards.length > 0 ? true : false; } function name() public view override(IERC20Metadata, ERC20) returns (string memory) { @@ -98,7 +89,7 @@ contract AaveV3Adapter is AdapterBase, WithRewards { /// @notice The token rewarded if the aave liquidity mining is active function rewardTokens() external view override returns (address[] memory) { - return aaveIncentives.getRewardsList(); + return aaveIncentives.getRewardsByAsset(asset()); } /*////////////////////////////////////////////////////////////// @@ -119,14 +110,15 @@ contract AaveV3Adapter is AdapterBase, WithRewards { STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - error IncentivesNotActive(); /// @notice Claim additional rewards given that it's active. function claim() public override onlyStrategy { - if (isActiveIncentives == false) revert IncentivesNotActive(); + if (address(aaveIncentives) == address(0)) return; + address[] memory _assets = new address[](1); _assets[0] = address(aToken); - aaveIncentives.claimAllRewardsOnBehalf(_assets, address(this), address(this)); + + try aaveIncentives.claimAllRewardsOnBehalf(_assets, address(this), address(this)) {} catch {}; } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/adapter/beefy/BeefyAdapter.sol b/src/vault/adapter/beefy/BeefyAdapter.sol index 9c4c957..5453fbe 100644 --- a/src/vault/adapter/beefy/BeefyAdapter.sol +++ b/src/vault/adapter/beefy/BeefyAdapter.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../abstracts/WithRewards.sol"; -import { IBeefyVault, IBeefyBooster, IBeefyBalanceCheck, IBeefyStrat } from "./IBeefy.sol"; -import { IPermissionRegistry } from "../../../interfaces/vault/IPermissionRegistry.sol"; +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../abstracts/WithRewards.sol"; +import {IBeefyVault, IBeefyBooster, IBeefyBalanceCheck, IBeefyStrat} from "./IBeefy.sol"; +import {IPermissionRegistry} from "../../../interfaces/vault/IPermissionRegistry.sol"; /** * @title Beefy Adapter @@ -18,159 +18,221 @@ import { IPermissionRegistry } from "../../../interfaces/vault/IPermissionRegist * Allows for additional strategies to use rewardsToken in case of an active Booster. */ contract BeefyAdapter is AdapterBase, WithRewards { - using SafeERC20 for IERC20; - using Math for uint256; - - string internal _name; - string internal _symbol; - - IBeefyVault public beefyVault; - IBeefyBooster public beefyBooster; - IBeefyBalanceCheck public beefyBalanceCheck; - - uint256 public constant BPS_DENOMINATOR = 10_000; - - error NotEndorsed(address beefyVault); - error InvalidBeefyVault(address beefyVault); - error InvalidBeefyBooster(address beefyBooster); - - /** - * @notice Initialize a new Beefy Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @param registry Endorsement Registry to check if the beefy adapter is endorsed. - * @param beefyInitData Encoded data for the beefy adapter initialization. - * @dev `_beefyVault` - The underlying beefy vault. - * @dev `_beefyBooster` - An optional beefy booster. - * @dev This function is called by the factory contract when deploying a new vault. - */ - function initialize(bytes memory adapterInitData, address registry, bytes memory beefyInitData) external initializer { - (address _beefyVault, address _beefyBooster) = abi.decode(beefyInitData, (address, address)); - __AdapterBase_init(adapterInitData); - - if (!IPermissionRegistry(registry).endorsed(_beefyVault)) revert NotEndorsed(_beefyVault); - if (_beefyBooster != address(0) && !IPermissionRegistry(registry).endorsed(_beefyBooster)) - revert NotEndorsed(_beefyBooster); - if (IBeefyVault(_beefyVault).want() != asset()) revert InvalidBeefyVault(_beefyVault); - if (_beefyBooster != address(0) && IBeefyBooster(_beefyBooster).stakedToken() != _beefyVault) - revert InvalidBeefyBooster(_beefyBooster); - - _name = string.concat("Popcorn Beefy", IERC20Metadata(asset()).name(), " Adapter"); - _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); - - beefyVault = IBeefyVault(_beefyVault); - beefyBooster = IBeefyBooster(_beefyBooster); - - beefyBalanceCheck = IBeefyBalanceCheck(_beefyBooster == address(0) ? _beefyVault : _beefyBooster); - - IERC20(asset()).approve(_beefyVault, type(uint256).max); - - if (_beefyBooster != address(0)) IERC20(_beefyVault).approve(_beefyBooster, type(uint256).max); - } + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + IBeefyVault public beefyVault; + IBeefyBooster public beefyBooster; + IBeefyBalanceCheck public beefyBalanceCheck; + + uint256 public constant BPS_DENOMINATOR = 10_000; + + error NotEndorsed(address beefyVault); + error InvalidBeefyVault(address beefyVault); + error InvalidBeefyBooster(address beefyBooster); + + /** + * @notice Initialize a new Beefy Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @param registry Endorsement Registry to check if the beefy adapter is endorsed. + * @param beefyInitData Encoded data for the beefy adapter initialization. + * @dev `_beefyVault` - The underlying beefy vault. + * @dev `_beefyBooster` - An optional beefy booster. + * @dev This function is called by the factory contract when deploying a new vault. + */ + function initialize( + bytes memory adapterInitData, + address registry, + bytes memory beefyInitData + ) external initializer { + (address _beefyVault, address _beefyBooster) = abi.decode( + beefyInitData, + (address, address) + ); + __AdapterBase_init(adapterInitData); + + if (!IPermissionRegistry(registry).endorsed(_beefyVault)) + revert NotEndorsed(_beefyVault); + if ( + _beefyBooster != address(0) && + !IPermissionRegistry(registry).endorsed(_beefyBooster) + ) revert NotEndorsed(_beefyBooster); + if (IBeefyVault(_beefyVault).want() != asset()) + revert InvalidBeefyVault(_beefyVault); + if ( + _beefyBooster != address(0) && + IBeefyBooster(_beefyBooster).stakedToken() != _beefyVault + ) revert InvalidBeefyBooster(_beefyBooster); + + _name = string.concat( + "Popcorn Beefy", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + beefyVault = IBeefyVault(_beefyVault); + beefyBooster = IBeefyBooster(_beefyBooster); + + beefyBalanceCheck = IBeefyBalanceCheck( + _beefyBooster == address(0) ? _beefyVault : _beefyBooster + ); + + IERC20(asset()).approve(_beefyVault, type(uint256).max); + + if (_beefyBooster != address(0)) + IERC20(_beefyVault).approve(_beefyBooster, type(uint256).max); + } - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - function _totalAssets() internal view override returns (uint256) { - return - beefyBalanceCheck.balanceOf(address(this)).mulDiv( - beefyVault.balance(), - beefyVault.totalSupply(), - Math.Rounding.Down - ); - } - - /// @notice The amount of beefy shares to withdraw given an amount of adapter shares - function convertToUnderlyingShares(uint256 assets, uint256 shares) public view override returns (uint256) { - uint256 supply = totalSupply(); - return supply == 0 ? shares : shares.mulDiv(beefyBalanceCheck.balanceOf(address(this)), supply, Math.Rounding.Up); - } - - /// @notice The token rewarded if a beefy booster is configured - function rewardTokens() external view override returns (address[] memory) { - address[] memory _rewardTokens = new address[](1); - if (address(beefyBooster) == address(0)) return _rewardTokens; - _rewardTokens[0] = beefyBooster.rewardToken(); - return _rewardTokens; - } - - /// @notice `previewWithdraw` that takes beefy withdrawal fees into account - function previewWithdraw(uint256 assets) public view override returns (uint256) { - IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); - - uint256 beefyFee; - try strat.withdrawalFee() returns (uint256 _beefyFee) { - beefyFee = _beefyFee; - } catch { - beefyFee = strat.withdrawFee(); + function _totalAssets() internal view override returns (uint256) { + return + beefyBalanceCheck.balanceOf(address(this)).mulDiv( + beefyVault.balance(), + beefyVault.totalSupply(), + Math.Rounding.Down + ); } - if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR, BPS_DENOMINATOR - beefyFee, Math.Rounding.Down); - - return _convertToShares(assets, Math.Rounding.Up); - } - - /// @notice `previewRedeem` that takes beefy withdrawal fees into account - function previewRedeem(uint256 shares) public view override returns (uint256) { - uint256 assets = _convertToAssets(shares, Math.Rounding.Down); - - IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); + /// @notice The amount of beefy shares to withdraw given an amount of adapter shares + function convertToUnderlyingShares( + uint256 assets, + uint256 shares + ) public view override returns (uint256) { + uint256 supply = totalSupply(); + return + supply == 0 + ? shares + : shares.mulDiv( + beefyBalanceCheck.balanceOf(address(this)), + supply, + Math.Rounding.Up + ); + } - uint256 beefyFee; - try strat.withdrawalFee() returns (uint256 _beefyFee) { - beefyFee = _beefyFee; - } catch { - beefyFee = strat.withdrawFee(); + /// @notice The token rewarded if a beefy booster is configured + function rewardTokens() external view override returns (address[] memory _rewardTokens) { + _rewardTokens = new address[](1); + if (address(beefyBooster) != address(0)) return _rewardTokens[0] = beefyBooster.rewardToken(); } - if (beefyFee > 0) assets = assets.mulDiv(BPS_DENOMINATOR - beefyFee, BPS_DENOMINATOR, Math.Rounding.Down); + /// @notice `previewWithdraw` that takes beefy withdrawal fees into account + function previewWithdraw( + uint256 assets + ) public view override returns (uint256) { + IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); + + uint256 beefyFee; + try strat.withdrawalFee() returns (uint256 _beefyFee) { + beefyFee = _beefyFee; + } catch { + beefyFee = strat.withdrawFee(); + } + + if (beefyFee > 0) + assets = assets.mulDiv( + BPS_DENOMINATOR, + BPS_DENOMINATOR - beefyFee, + Math.Rounding.Down + ); + + return _convertToShares(assets, Math.Rounding.Up); + } - return assets; - } + /// @notice `previewRedeem` that takes beefy withdrawal fees into account + function previewRedeem( + uint256 shares + ) public view override returns (uint256) { + uint256 assets = _convertToAssets(shares, Math.Rounding.Down); + + IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); + + uint256 beefyFee; + try strat.withdrawalFee() returns (uint256 _beefyFee) { + beefyFee = _beefyFee; + } catch { + beefyFee = strat.withdrawFee(); + } + + if (beefyFee > 0) + assets = assets.mulDiv( + BPS_DENOMINATOR - beefyFee, + BPS_DENOMINATOR, + Math.Rounding.Down + ); + + return assets; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Deposit into beefy vault and optionally into the booster given its configured - function _protocolDeposit(uint256 amount, uint256) internal virtual override { - beefyVault.deposit(amount); - if (address(beefyBooster) != address(0)) beefyBooster.stake(beefyVault.balanceOf(address(this))); - } + /// @notice Deposit into beefy vault and optionally into the booster given its configured + function _protocolDeposit( + uint256 amount, + uint256 + ) internal virtual override { + beefyVault.deposit(amount); + if (address(beefyBooster) != address(0)) + beefyBooster.stake(beefyVault.balanceOf(address(this))); + } - /// @notice Withdraw from the beefy vault and optionally from the booster given its configured - function _protocolWithdraw(uint256, uint256 shares) internal virtual override { - uint256 beefyShares = convertToUnderlyingShares(0, shares); + /// @notice Withdraw from the beefy vault and optionally from the booster given its configured + function _protocolWithdraw( + uint256, + uint256 shares + ) internal virtual override { + uint256 beefyShares = convertToUnderlyingShares(0, shares); - if (address(beefyBooster) != address(0)) beefyBooster.withdraw(beefyShares); - beefyVault.withdraw(beefyShares); - } + if (address(beefyBooster) != address(0)) + beefyBooster.withdraw(beefyShares); + beefyVault.withdraw(beefyShares); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - error NoBeefyBooster(); - - /// @notice Claim rewards from the beefy booster given its configured - function claim() public override onlyStrategy { - if (address(beefyBooster) == address(0)) revert NoBeefyBooster(); - beefyBooster.getReward(); - } + /// @notice Claim rewards from the beefy booster given its configured + function claim() public override onlyStrategy { + if (address(beefyBooster) == address(0)) return; + try beefyBooster.getReward() {} catch {}; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol b/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol index 8926815..525f7fe 100644 --- a/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol +++ b/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol @@ -80,8 +80,6 @@ contract CompoundV2Adapter is AdapterBase, WithRewards { if (isListed == false) revert InvalidAsset(address(cToken)); IERC20(asset()).approve(address(cToken), type(uint256).max); - - isActiveCompRewards = comptroller.compSupplySpeeds(address(cToken)) > 0; } function name() @@ -135,9 +133,13 @@ contract CompoundV2Adapter is AdapterBase, WithRewards { } /// @notice The token rewarded if compound liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - address[] memory _rewardTokens = new address[](1); - if (isActiveCompRewards == false) return _rewardTokens; + function rewardTokens() + external + view + override + returns (address[] memory _rewardTokens) + { + _rewardTokens = new address[](1); _rewardTokens[0] = comptroller.getCompAddress(); } @@ -166,12 +168,9 @@ contract CompoundV2Adapter is AdapterBase, WithRewards { STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - error IncentivesNotActive(); - /// @notice Claim additional rewards given that it's active. function claim() public override onlyStrategy { - if (isActiveCompRewards == false) revert IncentivesNotActive(); - comptroller.claimComp(address(this)); + try comptroller.claimComp(address(this)) {} catch {} } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/adapter/convex/ConvexAdapter.sol b/src/vault/adapter/convex/ConvexAdapter.sol index 4e8f36a..b14f1b7 100644 --- a/src/vault/adapter/convex/ConvexAdapter.sol +++ b/src/vault/adapter/convex/ConvexAdapter.sol @@ -85,7 +85,7 @@ contract ConvexAdapter is AdapterBase, WithRewards { } /// @notice The token rewarded from the convex reward contract - function rewardTokens() external view override returns (address[] memory) { + function rewardTokens() external view override returns (address[] memory tokens) { uint256 len = convexRewards.extraRewardsLength(); address[] memory tokens = new address[](len + 1); @@ -94,7 +94,6 @@ contract ConvexAdapter is AdapterBase, WithRewards { for (uint256 i; i < len; i++) { tokens[i + 1] = convexRewards.extraRewards(i).rewardToken(); } - return tokens; } /*////////////////////////////////////////////////////////////// @@ -120,11 +119,9 @@ contract ConvexAdapter is AdapterBase, WithRewards { STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - error MiningNotActive(); - /// @notice Claim liquidity mining rewards given that it's active function claim() public override onlyStrategy { - convexRewards.getReward(address(this), true); + try convexRewards.getReward(address(this), true) {} catch {}; } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol b/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol index 0dc092e..3fcba59 100644 --- a/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol +++ b/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../../abstracts/WithRewards.sol"; -import { IMasterChefV1 } from "./IMasterChefV1.sol"; +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../../abstracts/WithRewards.sol"; +import {IMasterChefV1} from "./IMasterChefV1.sol"; /** * @title MasterChefV1 Adapter @@ -15,110 +15,138 @@ import { IMasterChefV1 } from "./IMasterChefV1.sol"; * Allows wrapping MasterChefV1 Vaults. */ contract MasterChefV1Adapter is AdapterBase, WithRewards { - using SafeERC20 for IERC20; - using Math for uint256; + using SafeERC20 for IERC20; + using Math for uint256; - string internal _name; - string internal _symbol; + string internal _name; + string internal _symbol; - // @notice The MasterChef contract - IMasterChefV1 public masterChef; + // @notice The MasterChef contract + IMasterChefV1 public masterChef; - // @notice The address of the reward token - address public rewardsToken; + // @notice The address of the reward token + address public rewardsToken; - // @notice The pool ID - uint256 public pid; + // @notice The pool ID + uint256 public pid; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ - error InvalidAsset(); - - /** - * @notice Initialize a new MasterChef Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @dev `_pid` - The poolId for lpToken. - * @dev `_rewardsToken` - The token rewarded by the MasterChef contract (Sushi, Cake...) - * @dev This function is called by the factory contract when deploying a new vault. - */ - - function initialize( - bytes memory adapterInitData, - address registry, - bytes memory masterchefInitData - ) external initializer { - __AdapterBase_init(adapterInitData); - - (uint256 _pid, address _rewardsToken) = abi.decode(masterchefInitData, (uint256, address)); - - masterChef = IMasterChefV1(registry); - IMasterChefV1.PoolInfo memory pool = masterChef.poolInfo(_pid); - - if (pool.lpToken != asset()) revert InvalidAsset(); - - pid = _pid; - rewardsToken = _rewardsToken; - - _name = string.concat("Popcorn MasterChef", IERC20Metadata(asset()).name(), " Adapter"); - _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); - - IERC20(pool.lpToken).approve(address(masterChef), type(uint256).max); - } - - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } - - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } - - /*////////////////////////////////////////////////////////////// + error InvalidAsset(); + + /** + * @notice Initialize a new MasterChef Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @dev `_pid` - The poolId for lpToken. + * @dev `_rewardsToken` - The token rewarded by the MasterChef contract (Sushi, Cake...) + * @dev This function is called by the factory contract when deploying a new vault. + */ + + function initialize( + bytes memory adapterInitData, + address registry, + bytes memory masterchefInitData + ) external initializer { + __AdapterBase_init(adapterInitData); + + (uint256 _pid, address _rewardsToken) = abi.decode( + masterchefInitData, + (uint256, address) + ); + + masterChef = IMasterChefV1(registry); + IMasterChefV1.PoolInfo memory pool = masterChef.poolInfo(_pid); + + if (pool.lpToken != asset()) revert InvalidAsset(); + + pid = _pid; + rewardsToken = _rewardsToken; + + _name = string.concat( + "Popcorn MasterChef", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + IERC20(pool.lpToken).approve(address(masterChef), type(uint256).max); + } + + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } + + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Calculates the total amount of underlying tokens the Vault holds. - /// @return The total amount of underlying tokens the Vault holds. + /// @notice Calculates the total amount of underlying tokens the Vault holds. + /// @return The total amount of underlying tokens the Vault holds. - function _totalAssets() internal view override returns (uint256) { - IMasterChefV1.UserInfo memory user = masterChef.userInfo(pid, address(this)); - return user.amount; - } + function _totalAssets() internal view override returns (uint256) { + IMasterChefV1.UserInfo memory user = masterChef.userInfo( + pid, + address(this) + ); + return user.amount; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - function _protocolDeposit(uint256 amount, uint256) internal override { - masterChef.deposit(pid, amount); - } + function _protocolDeposit(uint256 amount, uint256) internal override { + masterChef.deposit(pid, amount); + } - function _protocolWithdraw(uint256 amount, uint256) internal override { - masterChef.withdraw(pid, amount); - } + function _protocolWithdraw(uint256 amount, uint256) internal override { + masterChef.withdraw(pid, amount); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Claim rewards from the masterChef - function claim() public override onlyStrategy { - masterChef.deposit(pid, 0); - } - - /// @notice The token rewarded - function rewardTokens() external view override returns (address[] memory) { - address[] memory _rewardTokens = new address[](1); - _rewardTokens[0] = rewardsToken; - return _rewardTokens; - } - - /*////////////////////////////////////////////////////////////// + /// @notice Claim rewards from the masterChef + function claim() public override onlyStrategy { + try masterChef.deposit(pid, 0) {} catch {} + } + + /// @notice The token rewarded + function rewardTokens() + external + view + override + returns (address[] memory _rewardTokens) + { + _rewardTokens = new address[](1); + _rewardTokens[0] = rewardsToken; + } + + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol index f5c7669..2fbf2e4 100644 --- a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol +++ b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol @@ -124,14 +124,13 @@ contract MasterChefV2Adapter is AdapterBase, WithRewards { //////////////////////////////////////////////////////////////*/ /// @notice Claim rewards from the masterChef function claim() public override onlyStrategy { - masterChef.harvest(pid, address(this)); + try masterChef.harvest(pid, address(this)) {} catch {} } /// @notice The token rewarded - function rewardTokens() external view override returns (address[] memory) { + function rewardTokens() external view override returns (address[] memory _rewardTokens) { address rewarder = masterChef.rewarder(pid); - address[] memory _rewardTokens; if (rewarder == address(0)) { _rewardTokens = new address[](1); } else { @@ -139,8 +138,6 @@ contract MasterChefV2Adapter is AdapterBase, WithRewards { _rewardTokens[1] = IRewarder(rewarder).rewardToken(); } _rewardTokens[0] = rewardsToken; - - return _rewardTokens; } /*////////////////////////////////////////////////////////////// From 89c38c3f164646c528d3312168029ce0fd4db6d8 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Wed, 12 Apr 2023 14:27:08 +0200 Subject: [PATCH 18/18] claim now returns success --- src/interfaces/vault/IWithRewards.sol | 4 +- .../adapter/aave/aaveV2/AaveV2Adapter.sol | 10 +- .../adapter/aave/aaveV3/AaveV3Adapter.sol | 214 ++++++++++-------- src/vault/adapter/abstracts/WithRewards.sol | 24 +- src/vault/adapter/beefy/BeefyAdapter.sol | 18 +- .../compound/compoundV2/CompoundV2Adapter.sol | 6 +- src/vault/adapter/convex/ConvexAdapter.sol | 190 +++++++++------- .../masterChefV1/MasterChefV1Adapter.sol | 6 +- .../masterChefV2/MasterChefV2Adapter.sol | 13 +- 9 files changed, 285 insertions(+), 200 deletions(-) diff --git a/src/interfaces/vault/IWithRewards.sol b/src/interfaces/vault/IWithRewards.sol index c6ccdc2..7c7d127 100644 --- a/src/interfaces/vault/IWithRewards.sol +++ b/src/interfaces/vault/IWithRewards.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.15; interface IWithRewards { - function claim() external; + function claim() external returns (bool); - function rewardTokens() external view returns (address[] memory); + function rewardTokens() external view returns (address[] memory); } diff --git a/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol b/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol index cb87a5b..3d6e25f 100644 --- a/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol +++ b/src/vault/adapter/aave/aaveV2/AaveV2Adapter.sol @@ -142,15 +142,15 @@ contract AaveV2Adapter is AdapterBase, WithRewards { //////////////////////////////////////////////////////////////*/ /// @notice Claim liquidity mining rewards given that it's active - function claim() public override onlyStrategy { - if (address(aaveMining) == address(0)) return; + function claim() public override onlyStrategy returns (bool success) { + if (address(aaveMining) == address(0)) return false; address[] memory assets = new address[](1); assets[0] = address(aToken); - try - aaveMining.claimRewards(assets, type(uint256).max, address(this)) - {} catch {} + try aaveMining.claimRewards(assets, type(uint256).max, address(this)) { + success = true; + } catch {} } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol b/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol index 895e884..7c0583d 100644 --- a/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol +++ b/src/vault/adapter/aave/aaveV3/AaveV3Adapter.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../../abstracts/WithRewards.sol"; -import { ILendingPool, IAaveIncentives, IAToken, IProtocolDataProvider } from "./IAaveV3.sol"; -import { DataTypes } from "./lib.sol"; +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../../abstracts/WithRewards.sol"; +import {ILendingPool, IAaveIncentives, IAToken, IProtocolDataProvider} from "./IAaveV3.sol"; +import {DataTypes} from "./lib.sol"; /** * @title AaveV2 Adapter @@ -19,113 +19,145 @@ import { DataTypes } from "./lib.sol"; */ contract AaveV3Adapter is AdapterBase, WithRewards { - using SafeERC20 for IERC20; - using Math for uint256; + using SafeERC20 for IERC20; + using Math for uint256; - string internal _name; - string internal _symbol; + string internal _name; + string internal _symbol; - /// @notice The Aave aToken contract - IAToken public aToken; + /// @notice The Aave aToken contract + IAToken public aToken; - /// @notice The Aave liquidity mining contract - IAaveIncentives public aaveIncentives; + /// @notice The Aave liquidity mining contract + IAaveIncentives public aaveIncentives; - /// @notice Check to see if Aave liquidity mining is active - bool public isActiveIncentives; + /// @notice Check to see if Aave liquidity mining is active + bool public isActiveIncentives; - /// @notice The Aave LendingPool contract - ILendingPool public lendingPool; - - /*////////////////////////////////////////////////////////////// + /// @notice The Aave LendingPool contract + ILendingPool public lendingPool; + + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - error DifferentAssets(address asset, address underlying); - - /** - * @notice Initialize a new AaveV2 Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @param aaveDataProvider Encoded data for the base adapter initialization. - * @dev This function is called by the factory contract when deploying a new vault. - */ - - function initialize( - bytes memory adapterInitData, - address aaveDataProvider, - bytes memory - ) public initializer { - __AdapterBase_init(adapterInitData); - - _name = string.concat("Popcorn AaveV2", IERC20Metadata(asset()).name(), " Adapter"); - _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); - - (address _aToken, , ) = IProtocolDataProvider(aaveDataProvider).getReserveTokensAddresses(asset()); - aToken = IAToken(_aToken); - if (aToken.UNDERLYING_ASSET_ADDRESS() != asset()) - revert DifferentAssets(aToken.UNDERLYING_ASSET_ADDRESS(), asset()); - - lendingPool = ILendingPool(aToken.POOL()); - aaveIncentives = IAaveIncentives(aToken.getIncentivesController()); - - IERC20(asset()).approve(address(lendingPool), type(uint256).max); - } - - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } - - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } - - /*////////////////////////////////////////////////////////////// + error DifferentAssets(address asset, address underlying); + + /** + * @notice Initialize a new AaveV2 Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @param aaveDataProvider Encoded data for the base adapter initialization. + * @dev This function is called by the factory contract when deploying a new vault. + */ + + function initialize( + bytes memory adapterInitData, + address aaveDataProvider, + bytes memory + ) public initializer { + __AdapterBase_init(adapterInitData); + + _name = string.concat( + "Popcorn AaveV2", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + (address _aToken, , ) = IProtocolDataProvider(aaveDataProvider) + .getReserveTokensAddresses(asset()); + aToken = IAToken(_aToken); + if (aToken.UNDERLYING_ASSET_ADDRESS() != asset()) + revert DifferentAssets(aToken.UNDERLYING_ASSET_ADDRESS(), asset()); + + lendingPool = ILendingPool(aToken.POOL()); + aaveIncentives = IAaveIncentives(aToken.getIncentivesController()); + + IERC20(asset()).approve(address(lendingPool), type(uint256).max); + } + + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } + + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - function _totalAssets() internal view override returns (uint256) { - return aToken.balanceOf(address(this)); - } + function _totalAssets() internal view override returns (uint256) { + return aToken.balanceOf(address(this)); + } - /// @notice The token rewarded if the aave liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - return aaveIncentives.getRewardsByAsset(asset()); - } + /// @notice The token rewarded if the aave liquidity mining is active + function rewardTokens() external view override returns (address[] memory) { + return aaveIncentives.getRewardsByAsset(asset()); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Deposit into aave lending pool - function _protocolDeposit(uint256 assets, uint256) internal virtual override { - lendingPool.supply(asset(), assets, address(this), 0); - } - - /// @notice Withdraw from lending pool - function _protocolWithdraw(uint256 assets, uint256) internal virtual override { - lendingPool.withdraw(asset(), assets, address(this)); - } - - /*////////////////////////////////////////////////////////////// + /// @notice Deposit into aave lending pool + function _protocolDeposit( + uint256 assets, + uint256 + ) internal virtual override { + lendingPool.supply(asset(), assets, address(this), 0); + } + + /// @notice Withdraw from lending pool + function _protocolWithdraw( + uint256 assets, + uint256 + ) internal virtual override { + lendingPool.withdraw(asset(), assets, address(this)); + } + + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - - /// @notice Claim additional rewards given that it's active. - function claim() public override onlyStrategy { - if (address(aaveIncentives) == address(0)) return; - - address[] memory _assets = new address[](1); - _assets[0] = address(aToken); - - try aaveIncentives.claimAllRewardsOnBehalf(_assets, address(this), address(this)) {} catch {}; - } - - /*////////////////////////////////////////////////////////////// + /// @notice Claim additional rewards given that it's active. + function claim() public override onlyStrategy returns (bool success) { + if (address(aaveIncentives) == address(0)) return false; + + address[] memory _assets = new address[](1); + _assets[0] = address(aToken); + + try + aaveIncentives.claimAllRewardsOnBehalf( + _assets, + address(this), + address(this) + ) + { + success = true; + } catch {} + } + + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/abstracts/WithRewards.sol b/src/vault/adapter/abstracts/WithRewards.sol index e3b8b30..35aa304 100644 --- a/src/vault/adapter/abstracts/WithRewards.sol +++ b/src/vault/adapter/abstracts/WithRewards.sol @@ -3,22 +3,26 @@ pragma solidity ^0.8.15; -import { EIP165 } from "../../../utils/EIP165.sol"; -import { OnlyStrategy } from "./OnlyStrategy.sol"; -import { IWithRewards } from "../../../interfaces/vault/IWithRewards.sol"; -import { IAdapter } from "../../../interfaces/vault/IAdapter.sol"; +import {EIP165} from "../../../utils/EIP165.sol"; +import {OnlyStrategy} from "./OnlyStrategy.sol"; +import {IWithRewards} from "../../../interfaces/vault/IWithRewards.sol"; +import {IAdapter} from "../../../interfaces/vault/IAdapter.sol"; /// @notice Abstract base for adapters that have rewards contract WithRewards is EIP165, OnlyStrategy { - function rewardTokens() external view virtual returns (address[] memory) {} + function rewardTokens() external view virtual returns (address[] memory) {} - function claim() public virtual onlyStrategy {} + function claim() public virtual onlyStrategy returns (bool) {} - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/beefy/BeefyAdapter.sol b/src/vault/adapter/beefy/BeefyAdapter.sol index 5453fbe..3e4a32e 100644 --- a/src/vault/adapter/beefy/BeefyAdapter.sol +++ b/src/vault/adapter/beefy/BeefyAdapter.sol @@ -135,9 +135,15 @@ contract BeefyAdapter is AdapterBase, WithRewards { } /// @notice The token rewarded if a beefy booster is configured - function rewardTokens() external view override returns (address[] memory _rewardTokens) { + function rewardTokens() + external + view + override + returns (address[] memory _rewardTokens) + { _rewardTokens = new address[](1); - if (address(beefyBooster) != address(0)) return _rewardTokens[0] = beefyBooster.rewardToken(); + if (address(beefyBooster) != address(0)) + _rewardTokens[0] = beefyBooster.rewardToken(); } /// @notice `previewWithdraw` that takes beefy withdrawal fees into account @@ -219,9 +225,11 @@ contract BeefyAdapter is AdapterBase, WithRewards { //////////////////////////////////////////////////////////////*/ /// @notice Claim rewards from the beefy booster given its configured - function claim() public override onlyStrategy { - if (address(beefyBooster) == address(0)) return; - try beefyBooster.getReward() {} catch {}; + function claim() public override onlyStrategy returns (bool success) { + if (address(beefyBooster) == address(0)) return false; + try beefyBooster.getReward() { + success = true; + } catch {} } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol b/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol index 525f7fe..825d94e 100644 --- a/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol +++ b/src/vault/adapter/compound/compoundV2/CompoundV2Adapter.sol @@ -169,8 +169,10 @@ contract CompoundV2Adapter is AdapterBase, WithRewards { //////////////////////////////////////////////////////////////*/ /// @notice Claim additional rewards given that it's active. - function claim() public override onlyStrategy { - try comptroller.claimComp(address(this)) {} catch {} + function claim() public override onlyStrategy returns (bool success) { + try comptroller.claimComp(address(this)) { + success = true; + } catch {} } /*////////////////////////////////////////////////////////////// diff --git a/src/vault/adapter/convex/ConvexAdapter.sol b/src/vault/adapter/convex/ConvexAdapter.sol index b14f1b7..e671a84 100644 --- a/src/vault/adapter/convex/ConvexAdapter.sol +++ b/src/vault/adapter/convex/ConvexAdapter.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.15; -import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../abstracts/AdapterBase.sol"; -import { WithRewards, IWithRewards } from "../abstracts/WithRewards.sol"; -import { IConvexBooster, IConvexRewards, IRewards } from "./IConvex.sol"; +import {AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter} from "../abstracts/AdapterBase.sol"; +import {WithRewards, IWithRewards} from "../abstracts/WithRewards.sol"; +import {IConvexBooster, IConvexRewards, IRewards} from "./IConvex.sol"; /** * @title Convex Adapter @@ -17,118 +17,148 @@ import { IConvexBooster, IConvexRewards, IRewards } from "./IConvex.sol"; * Allows for additional strategies to use rewardsToken in case of an active convexBooster. */ contract ConvexAdapter is AdapterBase, WithRewards { - using SafeERC20 for IERC20; - using Math for uint256; + using SafeERC20 for IERC20; + using Math for uint256; - string internal _name; - string internal _symbol; + string internal _name; + string internal _symbol; - /// @notice The poolId inside Convex booster for relevant Curve lpToken. - uint256 public pid; + /// @notice The poolId inside Convex booster for relevant Curve lpToken. + uint256 public pid; - /// @notice The booster address for Convex - IConvexBooster public convexBooster; + /// @notice The booster address for Convex + IConvexBooster public convexBooster; - /// @notice The Convex convexRewards. - IConvexRewards public convexRewards; + /// @notice The Convex convexRewards. + IConvexRewards public convexRewards; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ - error AssetMismatch(); + error AssetMismatch(); - /** - * @notice Initialize a new Convex Adapter. - * @param adapterInitData Encoded data for the base adapter initialization. - * @param registry The Convex Booster contract - * @param convexInitData Encoded data for the convex adapter initialization. - * @dev `_pid` - The poolId for lpToken. - * @dev This function is called by the factory contract when deploying a new vault. - */ - function initialize(bytes memory adapterInitData, address registry, bytes memory convexInitData) public initializer { - __AdapterBase_init(adapterInitData); + /** + * @notice Initialize a new Convex Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @param registry The Convex Booster contract + * @param convexInitData Encoded data for the convex adapter initialization. + * @dev `_pid` - The poolId for lpToken. + * @dev This function is called by the factory contract when deploying a new vault. + */ + function initialize( + bytes memory adapterInitData, + address registry, + bytes memory convexInitData + ) public initializer { + __AdapterBase_init(adapterInitData); - uint256 _pid = abi.decode(convexInitData, (uint256)); + uint256 _pid = abi.decode(convexInitData, (uint256)); - convexBooster = IConvexBooster(registry); - pid = _pid; + convexBooster = IConvexBooster(registry); + pid = _pid; - (address _asset, , , address _convexRewards, , ) = convexBooster.poolInfo(pid); + (address _asset, , , address _convexRewards, , ) = convexBooster + .poolInfo(pid); - if (_asset != asset()) revert AssetMismatch(); + if (_asset != asset()) revert AssetMismatch(); - convexRewards = IConvexRewards(_convexRewards); + convexRewards = IConvexRewards(_convexRewards); - _name = string.concat("Popcorn Convex", IERC20Metadata(_asset).name(), " Adapter"); - _symbol = string.concat("popB-", IERC20Metadata(_asset).symbol()); + _name = string.concat( + "Popcorn Convex", + IERC20Metadata(_asset).name(), + " Adapter" + ); + _symbol = string.concat("popB-", IERC20Metadata(_asset).symbol()); - IERC20(_asset).approve(address(convexBooster), type(uint256).max); - } + IERC20(_asset).approve(address(convexBooster), type(uint256).max); + } - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _name; - } + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { - return _symbol; - } + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Calculates the total amount of underlying tokens the Vault holds. - /// @return The total amount of underlying tokens the Vault holds. - function _totalAssets() internal view override returns (uint256) { - return convexRewards.balanceOf(address(this)); - } - - /// @notice The token rewarded from the convex reward contract - function rewardTokens() external view override returns (address[] memory tokens) { - uint256 len = convexRewards.extraRewardsLength(); - - address[] memory tokens = new address[](len + 1); - tokens[0] = 0xD533a949740bb3306d119CC777fa900bA034cd52; // CRV + /// @notice Calculates the total amount of underlying tokens the Vault holds. + /// @return The total amount of underlying tokens the Vault holds. + function _totalAssets() internal view override returns (uint256) { + return convexRewards.balanceOf(address(this)); + } - for (uint256 i; i < len; i++) { - tokens[i + 1] = convexRewards.extraRewards(i).rewardToken(); + /// @notice The token rewarded from the convex reward contract + function rewardTokens() + external + view + override + returns (address[] memory tokens) + { + uint256 len = convexRewards.extraRewardsLength(); + + tokens = new address[](len + 1); + tokens[0] = 0xD533a949740bb3306d119CC777fa900bA034cd52; // CRV + + for (uint256 i; i < len; i++) { + tokens[i + 1] = convexRewards.extraRewards(i).rewardToken(); + } } - } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Deposit into Convex convexBooster contract. - function _protocolDeposit(uint256 amount, uint256) internal override { - convexBooster.deposit(pid, amount, true); - } + /// @notice Deposit into Convex convexBooster contract. + function _protocolDeposit(uint256 amount, uint256) internal override { + convexBooster.deposit(pid, amount, true); + } - /// @notice Withdraw from Convex convexRewards contract. - function _protocolWithdraw(uint256 amount, uint256) internal override { - /** - * @dev No need to convert as Convex shares are 1:1 with Curve deposits. - * @param amount Amount of shares to withdraw. - * @param claim Claim rewards on withdraw? - */ - convexRewards.withdrawAndUnwrap(amount, false); - } + /// @notice Withdraw from Convex convexRewards contract. + function _protocolWithdraw(uint256 amount, uint256) internal override { + /** + * @dev No need to convert as Convex shares are 1:1 with Curve deposits. + * @param amount Amount of shares to withdraw. + * @param claim Claim rewards on withdraw? + */ + convexRewards.withdrawAndUnwrap(amount, false); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Claim liquidity mining rewards given that it's active - function claim() public override onlyStrategy { - try convexRewards.getReward(address(this), true) {} catch {}; - } + /// @notice Claim liquidity mining rewards given that it's active + function claim() public override onlyStrategy returns (bool success) { + try convexRewards.getReward(address(this), true) { + success = true; + } catch {} + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EIP-165 LOGIC //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { - return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; - } + function supportsInterface( + bytes4 interfaceId + ) public pure override(WithRewards, AdapterBase) returns (bool) { + return + interfaceId == type(IWithRewards).interfaceId || + interfaceId == type(IAdapter).interfaceId; + } } diff --git a/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol b/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol index 3fcba59..db99fc5 100644 --- a/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol +++ b/src/vault/adapter/sushi/masterChefV1/MasterChefV1Adapter.sol @@ -123,8 +123,10 @@ contract MasterChefV1Adapter is AdapterBase, WithRewards { STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ /// @notice Claim rewards from the masterChef - function claim() public override onlyStrategy { - try masterChef.deposit(pid, 0) {} catch {} + function claim() public override onlyStrategy returns (bool success) { + try masterChef.deposit(pid, 0) { + success = true; + } catch {} } /// @notice The token rewarded diff --git a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol index 2fbf2e4..0d2d7dc 100644 --- a/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol +++ b/src/vault/adapter/sushi/masterChefV2/MasterChefV2Adapter.sol @@ -123,12 +123,19 @@ contract MasterChefV2Adapter is AdapterBase, WithRewards { STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ /// @notice Claim rewards from the masterChef - function claim() public override onlyStrategy { - try masterChef.harvest(pid, address(this)) {} catch {} + function claim() public override onlyStrategy returns (bool success) { + try masterChef.harvest(pid, address(this)) { + success = true; + } catch {} } /// @notice The token rewarded - function rewardTokens() external view override returns (address[] memory _rewardTokens) { + function rewardTokens() + external + view + override + returns (address[] memory _rewardTokens) + { address rewarder = masterChef.rewarder(pid); if (rewarder == address(0)) {