From 298cf48f0369401bf21a43d796f9b2e27591297b Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 28 Mar 2023 14:17:07 +0200 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 ea69a03a87cf1b73f0d2edbfe4ec2edd5f8e47ad Mon Sep 17 00:00:00 2001 From: RedVeil Date: Thu, 6 Apr 2023 13:23:49 +0200 Subject: [PATCH 6/7] added usdt test for yearn --- src/vault/adapter/yearn/YearnAdapter.sol | 272 ++++++++----- .../integration/yearn/YearnAdapter.t.sol | 381 +++++++++++------- 2 files changed, 394 insertions(+), 259 deletions(-) diff --git a/src/vault/adapter/yearn/YearnAdapter.sol b/src/vault/adapter/yearn/YearnAdapter.sol index 48c1335..4356ac9 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,185 @@ 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 constant DEGRADATION_COEFFICIENT = 10 ** 18; + + /** + * @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); + + yVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); + + _name = string.concat( + "Popcorn Yearn", + IERC20Metadata(asset()).name(), + " Adapter" + ); + _symbol = string.concat("popY-", IERC20Metadata(asset()).symbol()); + + IERC20(_asset).safeIncreaseAllowance( + 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)); + } } diff --git a/test/vault/integration/yearn/YearnAdapter.t.sol b/test/vault/integration/yearn/YearnAdapter.t.sol index 2292ac4..12c696e 100644 --- a/test/vault/integration/yearn/YearnAdapter.t.sol +++ b/test/vault/integration/yearn/YearnAdapter.t.sol @@ -3,175 +3,254 @@ 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); + using Math for uint256; + using SafeERC20 for IERC20; + + 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; + } + + /*////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////*/ - testConfigStorage = ITestConfigStorage(address(new YearnTestConfigStorage())); + 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 + //////////////////////////////////////////////////////////////*/ - _setUpTest(testConfigStorage.getTestConfig(0)); - } + 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" + ); + } + + /*////////////////////////////////////////////////////////////// + USDT TESTS + //////////////////////////////////////////////////////////////*/ - function overrideSetup(bytes memory testConfig) public override { - _setUpTest(testConfig); - } + function test__usdt_deposit_bob() public { + overrideSetup(abi.encode(0xdAC17F958D2ee523a2206206994597C13D831ec7)); - function _setUpTest(bytes memory testConfig) internal { - address _asset = abi.decode(testConfig, (address)); + deal(address(asset), address(bob), 1e6); - setUpBaseTest(IERC20(_asset), address(new YearnAdapter()), 0x50c1a2eA0a861A967D9d0FFE2AE4012c2E053804, 10, "Yearn ", false); + vm.startPrank(bob); + IERC20(asset).safeIncreaseAllowance(address(adapter), 1e6); - yearnVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); - - vm.label(address(yearnVault), "yearnVault"); - vm.label(address(asset), "asset"); - vm.label(address(this), "test"); + adapter.deposit(1e6, address(bob)); + vm.stopPrank(); + emit log_uint(IERC20(asset).balanceOf(address(adapter))); + } - adapter.initialize(abi.encode(asset, address(this), address(0), 0, sigs, ""), externalRegistry, ""); + function test__usdt_deposit_this() public { + overrideSetup(abi.encode(0xdAC17F958D2ee523a2206206994597C13D831ec7)); - defaultAmount = 10**IERC20Metadata(address(asset)).decimals(); - minFuzz = defaultAmount * 10_000; - raise = defaultAmount * 100_000_000; - maxAssets = defaultAmount * 1_000_000; - maxShares = maxAssets / 2; - } + deal(address(asset), address(this), 1e6); - /*////////////////////////////////////////////////////////////// - HELPER - //////////////////////////////////////////////////////////////*/ + IERC20(asset).safeIncreaseAllowance(address(adapter), 1e6); - 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 - //////////////////////////////////////////////////////////////*/ + adapter.deposit(1e6, address(this)); + emit log_uint(IERC20(asset).balanceOf(address(adapter))); + } - 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"); - } - - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// 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); + } } From 70646a1972a0c75aecb9959eff883478dccab3cb Mon Sep 17 00:00:00 2001 From: RedVeil Date: Mon, 10 Apr 2023 10:09:55 +0200 Subject: [PATCH 7/7] changed bob and alice adresses --- test/vault/integration/abstract/AbstractAdapterTest.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/vault/integration/abstract/AbstractAdapterTest.sol b/test/vault/integration/abstract/AbstractAdapterTest.sol index 7719187..b6a9a5a 100644 --- a/test/vault/integration/abstract/AbstractAdapterTest.sol +++ b/test/vault/integration/abstract/AbstractAdapterTest.sol @@ -25,8 +25,8 @@ contract AbstractAdapterTest is PropertyTest { bytes32 constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - address bob = address(1); - address alice = address(2); + address bob = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + address alice = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); address feeRecipient = address(0x4444); uint256 defaultAmount;