diff --git a/contracts/ERC4626/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol similarity index 80% rename from contracts/ERC4626/VenusERC4626.sol rename to contracts/ERC4626/Base/VenusERC4626.sol index d599b885..0a9b313c 100644 --- a/contracts/ERC4626/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -5,23 +5,25 @@ import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ER import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import { MaxLoopsLimitHelper } from "@venusprotocol/isolated-pools/contracts/MaxLoopsLimitHelper.sol"; import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; -import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; -import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewards/RewardsDistributor.sol"; -import { MaxLoopsLimitHelper } from "@venusprotocol/isolated-pools/contracts/MaxLoopsLimitHelper.sol"; -import { IComptroller } from "./Interfaces/IComptroller.sol"; -import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; +import { IComptroller, Action } from "../Interfaces/IComptroller.sol"; +import { VTokenInterface } from "../Interfaces/VTokenInterface.sol"; -import { Action } from "@venusprotocol/isolated-pools/contracts/ComptrollerInterface.sol"; -import { EXP_SCALE } from "@venusprotocol/isolated-pools/contracts/lib/constants.sol"; -import { VToken } from "@venusprotocol/isolated-pools/contracts/VToken.sol"; +uint256 constant EXP_SCALE = 1e18; /// @title VenusERC4626 -/// @notice ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. -contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHelper, ReentrancyGuardUpgradeable { +/// @notice Abstract ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. +abstract contract VenusERC4626 is + ERC4626Upgradeable, + AccessControlledV8, + MaxLoopsLimitHelper, + ReentrancyGuardUpgradeable +{ using MathUpgradeable for uint256; using SafeERC20Upgradeable for ERC20Upgradeable; @@ -29,7 +31,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe uint256 internal constant NO_ERROR = 0; /// @notice The Venus vToken associated with this ERC4626 vault. - VToken public vToken; + VTokenInterface public vToken; /// @notice The Venus Comptroller contract, responsible for market operations. IComptroller public comptroller; @@ -37,6 +39,10 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @notice The recipient of rewards distributed by the Venus Protocol. address public rewardRecipient; + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + uint256[47] private __gap; + /// @notice Emitted when rewards are claimed. /// @param amount The amount of reward tokens claimed. /// @param rewardToken The address of the reward token claimed. @@ -76,43 +82,10 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @param operation The name of the operation that failed (e.g., "deposit", "withdraw", "mint", "redeem"). error ERC4626__ZeroAmount(string operation); - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Note that the contract is upgradeable. Use initialize() or reinitializers - // to set the state variables. - _disableInitializers(); - } - - /// @notice Initializes the VenusERC4626 vault, only with the VToken address associated to the vault - /// @dev `initialize2` should be invoked to complete the configuration of the vault - /// @param vToken_ The VToken associated with the vault, representing the yield-bearing asset. - function initialize(address vToken_) public initializer { - ensureNonzeroAddress(vToken_); - - vToken = VToken(vToken_); - comptroller = IComptroller(address(vToken.comptroller())); - ERC20Upgradeable asset = ERC20Upgradeable(vToken.underlying()); - - __ERC20_init(_generateVaultName(asset), _generateVaultSymbol(asset)); - __ERC4626_init(asset); - __ReentrancyGuard_init(); - } - - /** - * @notice Set the limit for the loops can iterate to avoid the DOS - * @param loopsLimit Number of loops limit - * @custom:event Emits MaxLoopsLimitUpdated event on success - * @custom:access Controlled by ACM - */ - function setMaxLoopsLimit(uint256 loopsLimit) external { - _checkAccessAllowed("setMaxLoopsLimit(uint256)"); - _setMaxLoopsLimit(loopsLimit); - } - /// @notice Sets a new reward recipient address /// @param newRecipient The address of the new reward recipient /// @custom:access Controlled by ACM - function setRewardRecipient(address newRecipient) external { + function setRewardRecipient(address newRecipient) external virtual { _checkAccessAllowed("setRewardRecipient(address)"); _setRewardRecipient(newRecipient); } @@ -121,7 +94,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @param token Address of the token /// @custom:event SweepToken emits on success /// @custom:access Only owner - function sweepToken(IERC20Upgradeable token) external onlyOwner { + function sweepToken(IERC20Upgradeable token) external virtual onlyOwner { uint256 balance = token.balanceOf(address(this)); if (balance > 0) { @@ -131,64 +104,12 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe } } - /// @notice Claims rewards from all reward distributors associated with the VToken and transfers them to the reward recipient. - /// @dev Iterates through all reward distributors fetched from the comptroller, claims rewards, and transfers them if available. - function claimRewards() external { - IComptroller _comptroller = comptroller; - VToken _vToken = vToken; - address _rewardRecipient = rewardRecipient; - - RewardsDistributor[] memory rewardDistributors = _comptroller.getRewardDistributors(); - - _ensureMaxLoops(rewardDistributors.length); - - for (uint256 i = 0; i < rewardDistributors.length; i++) { - RewardsDistributor rewardDistributor = rewardDistributors[i]; - IERC20Upgradeable rewardToken = IERC20Upgradeable(address(rewardDistributor.rewardToken())); - - VToken[] memory vTokens = new VToken[](1); - vTokens[0] = _vToken; - RewardsDistributor(rewardDistributor).claimRewardToken(address(this), vTokens); - uint256 rewardBalance = rewardToken.balanceOf(address(this)); - - if (rewardBalance > 0) { - SafeERC20Upgradeable.safeTransfer(rewardToken, _rewardRecipient, rewardBalance); - - // Try to update the asset state on the recipient if reward recipient is a protocol share reserve - // reward recipient cannot be an EOA - try - IProtocolShareReserve(_rewardRecipient).updateAssetsState( - address(_comptroller), - address(rewardToken), - IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS - ) - {} catch {} - } - emit ClaimRewards(rewardBalance, address(rewardToken)); - } - } - - /// @notice Second function to invoke to complete the configuration of the vault, setting the rest of the attributes - /// @param accessControlManager_ Address of the ACM contract - /// @param rewardRecipient_ The address that will receive rewards generated by the vault. - /// @param loopsLimit_ The maximum number of loops allowed for reward distribution. - /// @param vaultOwner_ The owner that will be set for the created vault - function initialize2( - address accessControlManager_, - address rewardRecipient_, - uint256 loopsLimit_, - address vaultOwner_ - ) public reinitializer(2) { - ensureNonzeroAddress(vaultOwner_); - - __AccessControlled_init(accessControlManager_); - _setMaxLoopsLimit(loopsLimit_); - _setRewardRecipient(rewardRecipient_); - _transferOwnership(vaultOwner_); - } + /// @notice Claims rewards from the Venus protocol + /// @dev Must be implemented by child contracts + function claimRewards() external virtual; /// @inheritdoc ERC4626Upgradeable - function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256) { + function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256) { ensureNonzeroAddress(receiver); vToken.accrueInterest(); @@ -214,7 +135,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @dev The minted shares are calculated considering the minted VTokens /// @dev It can mint slightly fewer shares than requested, because VToken.mint rounds down /// @inheritdoc ERC4626Upgradeable - function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256) { + function mint(uint256 shares, address receiver) public virtual override nonReentrant returns (uint256) { ensureNonzeroAddress(receiver); vToken.accrueInterest(); @@ -224,6 +145,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe if (shares > maxMint(receiver)) { revert ERC4626__MintMoreThanMax(); } + uint256 assets = previewMint(shares); if (assets == 0) { revert ERC4626__ZeroAmount("mint"); @@ -235,7 +157,11 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @dev Receiver can receive slightly more assets than requested, because VToken.redeemUnderlying rounds up /// @dev The shares to burn are calculated considering the actual transferred assets, not the requested ones /// @inheritdoc ERC4626Upgradeable - function withdraw(uint256 assets, address receiver, address owner) public override nonReentrant returns (uint256) { + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual override nonReentrant returns (uint256) { ensureNonzeroAddress(receiver); ensureNonzeroAddress(owner); @@ -254,7 +180,11 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe } /// @inheritdoc ERC4626Upgradeable - function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual override nonReentrant returns (uint256) { ensureNonzeroAddress(receiver); ensureNonzeroAddress(owner); @@ -345,11 +275,42 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe } } + /// @notice Initializes the VenusERC4626 vault, only with the VToken address associated to the vault + /// @dev `initialize2` should be invoked to complete the configuration of the vault + /// @param vToken_ The VToken associated with the vault, representing the yield-bearing asset. + function __VenusERC4626_init(address vToken_) internal onlyInitializing { + ensureNonzeroAddress(vToken_); + + vToken = VTokenInterface(vToken_); + comptroller = IComptroller(address(vToken.comptroller())); + ERC20Upgradeable asset = ERC20Upgradeable(vToken.underlying()); + + __ERC20_init(_generateVaultName(asset), _generateVaultSymbol(asset)); + __ERC4626_init(asset); + __ReentrancyGuard_init(); + } + + /// @notice Second function to invoke to complete the configuration of the vault, setting the rest of the attributes + /// @param accessControlManager_ Address of the ACM contract + /// @param rewardRecipient_ The address that will receive rewards generated by the vault. + /// @param vaultOwner_ The owner that will be set for the created vault + function __VenusERC4626_init2( + address accessControlManager_, + address rewardRecipient_, + address vaultOwner_ + ) internal onlyInitializing { + ensureNonzeroAddress(vaultOwner_); + + __AccessControlled_init(accessControlManager_); + _setRewardRecipient(rewardRecipient_); + _transferOwnership(vaultOwner_); + } + /// @notice Redeems the amount of vTokens equivalent to the provided shares. /// @dev Calls `redeem` on the vToken contract. Reverts on error. /// @param shares The amount of shares to redeem. /// @return The amount of assets transferred in - function _beforeRedeem(uint256 shares) internal returns (uint256) { + function _beforeRedeem(uint256 shares) internal virtual returns (uint256) { IERC20Upgradeable token = IERC20Upgradeable(asset()); uint256 balanceBefore = token.balanceOf(address(this)); @@ -377,7 +338,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @return actualAssets The amount of assets transferred in /// @return actualShares The shares equivalent to `actualAssets`, to be burned, rounded up /// @custom:error ERC4626__ZeroAmount is thrown when the redeemed VTokens are zero - function _beforeWithdraw(uint256 assets) internal returns (uint256 actualAssets, uint256 actualShares) { + function _beforeWithdraw(uint256 assets) internal virtual returns (uint256 actualAssets, uint256 actualShares) { IERC20Upgradeable token = IERC20Upgradeable(asset()); uint256 balanceBefore = token.balanceOf(address(this)); uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); @@ -394,6 +355,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe if (actualVTokens == 0) { revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); } + // Return the shares equivalent to the burned vTokens actualShares = actualVTokens.mulDiv( totalSupply() + 10 ** _decimalsOffset(), @@ -405,7 +367,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @notice Mints vTokens after depositing assets. /// @dev Calls `mint` on the vToken contract. Reverts on error. /// @param assets The amount of underlying assets to deposit. - function _mintVTokens(uint256 assets) internal { + function _mintVTokens(uint256 assets) internal virtual { ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); uint256 errorCode = vToken.mint(assets); if (errorCode != NO_ERROR) { @@ -417,7 +379,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @param newRecipient The address of the new reward recipient /// @custom:error ZeroAddressNotAllowed is thrown when the new recipient address is zero /// @custom:event RewardRecipientUpdated is emitted when the reward recipient address is updated - function _setRewardRecipient(address newRecipient) internal { + function _setRewardRecipient(address newRecipient) internal virtual { ensureNonzeroAddress(newRecipient); emit RewardRecipientUpdated(rewardRecipient, newRecipient); @@ -428,7 +390,7 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// underlying assets equivalent to the new VTokens minted /// @custom:error ERC4626__ZeroAmount is thrown when the minted VTokens are zero /// @inheritdoc ERC4626Upgradeable - function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override { + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { // 1. Track pre-transfer balances uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); @@ -472,14 +434,14 @@ contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHe /// @notice Generates and returns the derived name of the vault considering the asset name /// @param asset_ Asset to be accepted in the vault whose name this function will return /// @return Name of the vault considering the asset name - function _generateVaultName(ERC20Upgradeable asset_) internal view returns (string memory) { + function _generateVaultName(ERC20Upgradeable asset_) internal view virtual returns (string memory) { return string(abi.encodePacked("ERC4626-Wrapped Venus ", asset_.name())); } /// @notice Generates and returns the derived symbol of the vault considering the asset symbol /// @param asset_ Asset to be accepted in the vault whose symbol this function will return /// @return Symbol of the vault considering the asset name - function _generateVaultSymbol(ERC20Upgradeable asset_) internal view returns (string memory) { + function _generateVaultSymbol(ERC20Upgradeable asset_) internal view virtual returns (string memory) { return string(abi.encodePacked("v4626", asset_.symbol())); } } diff --git a/contracts/ERC4626/Interfaces/IComptroller.sol b/contracts/ERC4626/Interfaces/IComptroller.sol index a906440a..bade1961 100644 --- a/contracts/ERC4626/Interfaces/IComptroller.sol +++ b/contracts/ERC4626/Interfaces/IComptroller.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.25; import { Action } from "@venusprotocol/isolated-pools/contracts/ComptrollerInterface.sol"; import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewards/RewardsDistributor.sol"; +import { VTokenInterface } from "./VTokenInterface.sol"; /** * @title IComptroller @@ -10,9 +11,20 @@ import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewa * @notice Combined interface for the `Comptroller` contract, including both core and view functions. */ interface IComptroller { + function claimVenus( + address[] calldata holders, + VTokenInterface[] calldata vTokens, + bool borrowers, + bool suppliers + ) external; + function actionPaused(address market, Action action) external view returns (bool); function getRewardDistributors() external view returns (RewardsDistributor[] memory); function supplyCaps(address) external view returns (uint256); + + function markets(address) external view returns (bool, uint256); + + function getXVSAddress() external view returns (address); } diff --git a/contracts/ERC4626/Interfaces/IProtocolShareReserve.sol b/contracts/ERC4626/Interfaces/IProtocolShareReserve.sol index d18451d0..6c650378 100644 --- a/contracts/ERC4626/Interfaces/IProtocolShareReserve.sol +++ b/contracts/ERC4626/Interfaces/IProtocolShareReserve.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; interface IProtocolShareReserve { /// @notice it represents the type of vToken income diff --git a/contracts/ERC4626/Interfaces/VTokenInterface.sol b/contracts/ERC4626/Interfaces/VTokenInterface.sol new file mode 100644 index 00000000..70ba7338 --- /dev/null +++ b/contracts/ERC4626/Interfaces/VTokenInterface.sol @@ -0,0 +1,27 @@ +pragma solidity 0.8.25; + +import { IComptroller } from "./IComptroller.sol"; + +interface VTokenInterface { + function mint(uint256 mintAmount) external returns (uint256); + + function redeem(uint256 redeemTokens) external returns (uint256); + + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + + function accrueInterest() external returns (uint256); + + function balanceOf(address owner) external view returns (uint256); + + function comptroller() external view returns (IComptroller); + + function totalSupply() external view returns (uint256); + + function underlying() external view returns (address); + + function getCash() external view returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function totalReserves() external view returns (uint256); +} diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol new file mode 100644 index 00000000..0ad6dc24 --- /dev/null +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { VenusERC4626 } from "./Base/VenusERC4626.sol"; +import { IERC20Upgradeable, SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; +import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; +import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; + +/// @title VenusERC4626Core +/// @notice ERC4626 wrapper for Venus Core Pool vTokens +contract VenusERC4626Core is VenusERC4626 { + /// @notice Immutable XVS token address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address public immutable XVS_ADDRESS; + + /// @notice Constructor + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address xvsAddress_) { + ensureNonzeroAddress(xvsAddress_); + XVS_ADDRESS = xvsAddress_; + _disableInitializers(); + } + + /// @notice Initializes the VenusERC4626Core contract + /// @dev `initialize2` should be invoked to complete the configuration of the vault + /// @param vToken_ The address of the vToken to be wrapped + function initialize(address vToken_) public virtual initializer { + __VenusERC4626_init(vToken_); + } + + /// @inheritdoc VenusERC4626 + function claimRewards() external override { + address[] memory holders = new address[](1); + holders[0] = address(this); + VTokenInterface[] memory vTokens = new VTokenInterface[](1); + vTokens[0] = vToken; + comptroller.claimVenus(holders, vTokens, false, true); + + IERC20Upgradeable xvs = IERC20Upgradeable(XVS_ADDRESS); + uint256 rewardAmount = xvs.balanceOf(address(this)); + + if (rewardAmount > 0) { + SafeERC20Upgradeable.safeTransfer(xvs, rewardRecipient, rewardAmount); + + try + IProtocolShareReserve(rewardRecipient).updateAssetsState( + address(comptroller), + XVS_ADDRESS, + IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS + ) + {} catch {} + + emit ClaimRewards(rewardAmount, XVS_ADDRESS); + } + } + + /// @notice second function to invoke to complete the initialization + /// @param accessControlManager_ The address of the access control manager + /// @param rewardRecipient_ The address that will receive rewards + /// @param vaultOwner_ The owner of the vault + function initialize2( + address accessControlManager_, + address rewardRecipient_, + address vaultOwner_ + ) public virtual reinitializer(2) { + __VenusERC4626_init2(accessControlManager_, rewardRecipient_, vaultOwner_); + } +} diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 79a3a4e5..8fdcaf21 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -4,40 +4,65 @@ pragma solidity 0.8.25; import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; - import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; +import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; + +import { IComptroller } from "./Interfaces/IComptroller.sol"; +import { VenusERC4626Core } from "./VenusERC4626Core.sol"; +import { VenusERC4626Isolated } from "./VenusERC4626Isolated.sol"; + import { PoolRegistryInterface } from "@venusprotocol/isolated-pools/contracts/Pool/PoolRegistryInterface.sol"; import { MaxLoopsLimitHelper } from "@venusprotocol/isolated-pools/contracts/MaxLoopsLimitHelper.sol"; -import { VTokenInterface } from "@venusprotocol/isolated-pools/contracts/VTokenInterfaces.sol"; -import { VenusERC4626 } from "./VenusERC4626.sol"; -import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; +import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; -/// @title VenusERC4626Factory -/// @notice Factory for creating VenusERC4626 contracts +/// @title ERC4626Factory +/// @notice Factory contract for deploying ERC4626 vaults (core and isolated) with beacon proxies. contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { - /// @notice A constant salt value used for deterministic contract deployment - bytes32 public constant SALT = keccak256("Venus-ERC4626 Vault"); + /// @notice Salt used to deterministically deploy isolated pool vaults + /// @dev Previously named `salt` + bytes32 public constant ISOLATED_SALT = keccak256("Venus-ERC4626 Vault"); + + /// @notice Salt used to deterministically deploy core pool vaults + bytes32 public constant CORE_SALT = keccak256("Venus-Core-ERC4626"); - /// @notice The beacon contract for VenusERC4626 proxies - UpgradeableBeacon public beacon; + /// @notice Comptroller for core pool validation + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IComptroller public immutable CORE_COMPTROLLER; - /// @notice The Pool Registry contract + /// @notice Address of the VBNB token + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address public immutable VBNB; + + /// @notice Beacon for isolated vaults + /// @dev Previously named `beacon` + UpgradeableBeacon public isolatedBeacon; + + /// @notice PoolRegistry contract to validate isolated pool vTokens PoolRegistryInterface public poolRegistry; - /// @notice The address that will receive the liquidity mining rewards + /// @notice Address to which rewards will be distributed address public rewardRecipient; - // @notice Map of vaults created by this factory + /// @notice Mapping from vToken to deployed ERC4626 vaults mapping(address vToken => ERC4626Upgradeable vault) public createdVaults; - /// @notice Emitted when a new ERC4626 vault has been created - /// @param vToken The vToken used by the vault - /// @param vault The vault that was created - event CreateERC4626(VTokenInterface indexed vToken, ERC4626Upgradeable indexed vault); + /// @notice Beacon for core vaults + /// @dev Will be address(0) for non-BSC chains as core functionality is only required on BSC + UpgradeableBeacon public coreBeacon; + + /// @notice Mapping indicating whether a vault belongs to core pool + /// @dev Will be false for all vaults on non-BSC chains as core functionality is only required on BSC + mapping(address => bool) public isCoreVault; - /// @notice Emitted when the reward recipient address is updated. - /// @param oldRecipient The previous reward recipient address. - /// @param newRecipient The new reward recipient address. + /// @notice Emitted when a new vault is created + /// @param vToken The address of the vToken for which the vault is created + /// @param vault The address of the newly created ERC4626 vault + /// @param isCore Indicates whether the vault is a core pool vault + event CreateERC4626(address indexed vToken, address indexed vault, bool isCore); + + /// @notice Emitted when the reward recipient address is updated + /// @param oldRecipient The previous reward recipient address + /// @param newRecipient The new reward recipient address event RewardRecipientUpdated(address indexed oldRecipient, address indexed newRecipient); /// @notice Thrown when the provided vToken is not registered in PoolRegistry @@ -46,42 +71,63 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Thrown when a VenusERC4626 already exists for the provided vToken error VenusERC4626Factory__ERC4626AlreadyExists(); + /// @notice Constructor + /// @param coreComptroller_ Address of the core comptroller + /// @param vBNB_ Address of the VBNB token /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - // Note that the contract is upgradeable. Use initialize() or reinitializers - // to set the state variables. + constructor(address coreComptroller_, address vBNB_) { + if (coreComptroller_ != address(0)) { + CORE_COMPTROLLER = IComptroller(coreComptroller_); + } + + if (vBNB_ != address(0)) { + VBNB = vBNB_; + } + _disableInitializers(); } - /// @notice Initializes the contract - /// @param accessControlManager Address of the ACM contract - /// @param poolRegistryAddress Address of the Pool Registry contract - /// @param rewardRecipientAddress Reward recipient address - /// @param venusERC4626Implementation Address of the VenusERC4626 implementation contract - /// @param loopsLimitNumber The loops limit for the MaxLoopsLimit helper + /// @notice Initializes the factory contract + /// @dev `initialize2` should be invoked to complete the configuration of the factory + /// @param accessControlManager_ Access control manager address + /// @param isolatedImplementation_ Implementation address for isolated vaults + /// @param poolRegistry_ Pool registry address + /// @param rewardRecipient_ Initial reward recipient address + /// @param loopsLimitNumber_ Maximum number of loops function initialize( - address accessControlManager, - address poolRegistryAddress, - address rewardRecipientAddress, - address venusERC4626Implementation, - uint256 loopsLimitNumber + address accessControlManager_, + address isolatedImplementation_, + address poolRegistry_, + address rewardRecipient_, + uint256 loopsLimitNumber_ ) external initializer { - ensureNonzeroAddress(accessControlManager); - ensureNonzeroAddress(poolRegistryAddress); - ensureNonzeroAddress(rewardRecipientAddress); - ensureNonzeroAddress(venusERC4626Implementation); + ensureNonzeroAddress(isolatedImplementation_); + ensureNonzeroAddress(poolRegistry_); + ensureNonzeroAddress(rewardRecipient_); - __AccessControlled_init(accessControlManager); + __AccessControlled_init(accessControlManager_); + _setMaxLoopsLimit(loopsLimitNumber_); - poolRegistry = PoolRegistryInterface(poolRegistryAddress); - rewardRecipient = rewardRecipientAddress; - _setMaxLoopsLimit(loopsLimitNumber); + isolatedBeacon = new UpgradeableBeacon(isolatedImplementation_); - // Deploy the upgradeable beacon with the initial implementation - beacon = new UpgradeableBeacon(venusERC4626Implementation); + poolRegistry = PoolRegistryInterface(poolRegistry_); + rewardRecipient = rewardRecipient_; // The owner of the beacon will initially be the owner of the factory - beacon.transferOwnership(owner()); + isolatedBeacon.transferOwnership(owner()); + } + + /// @notice Initializes the core beacon attribute + /// @dev It has to be called after `initialize` + /// @param coreImplementation_ Implementation address for core vaults. It must be zero if the chain does + /// not have a "legacy" Core pool + function initialize2(address coreImplementation_) external reinitializer(2) { + if (coreImplementation_ != address(0)) { + coreBeacon = new UpgradeableBeacon(coreImplementation_); + + // The owner of the beacon will initially be the owner of the factory + coreBeacon.transferOwnership(owner()); + } } /// @notice Sets a new reward recipient address @@ -97,61 +143,61 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { rewardRecipient = newRecipient; } - /** - * @notice Set the limit for the loops can iterate to avoid the DOS - * @param loopsLimit Number of loops limit - * @custom:event Emits MaxLoopsLimitUpdated event on success - * @custom:access Controlled by ACM - */ + /// @notice Sets the max loops limit to protect from DoS due to unbounded iterations + /// @param loopsLimit New maximum loop count + /// @custom:event Emits MaxLoopsLimitUpdated event on success + /// @custom:access Controlled by ACM function setMaxLoopsLimit(uint256 loopsLimit) external { _checkAccessAllowed("setMaxLoopsLimit(uint256)"); _setMaxLoopsLimit(loopsLimit); } - /// @notice Creates a VenusERC4626 vault for a given asset and comptroller - /// @param vToken The vToken address to create the vault - /// @return vault The deployed VenusERC4626 vault - /// @custom:error ZeroAddressNotAllowed is thrown when the vToken address is zero - /// @custom:error VenusERC4626Factory__InvalidVToken is thrown when the provided vToken is not supported by the poolRegistry - /// @custom:error VenusERC4626Factory__ERC4626AlreadyExists is thrown when this factory already created a VenusERC4626 for the provided vToken + /// @notice Creates an ERC4626 vault for the given vToken + /// @param vToken Address of the vToken + /// @return vault The deployed ERC4626 vault + /// @custom:error ERC4626AlreadyExists if a vault already exists for the vToken + /// @custom:error InvalidVToken if the vToken is invalid /// @custom:event CreateERC4626 is emitted when the ERC4626 wrapper is created function createERC4626(address vToken) external returns (ERC4626Upgradeable vault) { ensureNonzeroAddress(vToken); - if (address(createdVaults[vToken]) != address(0)) { - revert VenusERC4626Factory__ERC4626AlreadyExists(); - } - - VTokenInterface vToken_ = VTokenInterface(vToken); - - address comptroller = address(vToken_.comptroller()); - - if (vToken != poolRegistry.getVTokenForAsset(comptroller, vToken_.underlying())) { + if (VBNB != address(0) && vToken == VBNB) { revert VenusERC4626Factory__InvalidVToken(); } - VenusERC4626 venusERC4626 = VenusERC4626( - address( - new BeaconProxy{ salt: SALT }( - address(beacon), - abi.encodeWithSelector(VenusERC4626.initialize.selector, vToken) - ) - ) - ); + if (address(createdVaults[vToken]) != address(0)) revert VenusERC4626Factory__ERC4626AlreadyExists(); - venusERC4626.initialize2(address(_accessControlManager), rewardRecipient, maxLoopsLimit, owner()); + bool isCore = _isCoreVToken(vToken); - vault = ERC4626Upgradeable(address(venusERC4626)); + if (isCore) { + isCoreVault[vToken] = isCore; + vault = _deployCoreVault(vToken); + } else { + address underlying = VTokenInterface(vToken).underlying(); + address comptroller = address(VTokenInterface(vToken).comptroller()); + if (vToken != poolRegistry.getVTokenForAsset(comptroller, underlying)) { + revert VenusERC4626Factory__InvalidVToken(); + } - createdVaults[vToken] = vault; + vault = _deployIsolatedVault(vToken); + } - emit CreateERC4626(vToken_, vault); + createdVaults[vToken] = vault; + emit CreateERC4626(vToken, address(vault), isCore); } - /// @notice Predicts the vault address for a given vToken - /// @param vToken The vToken address - /// @return The precomputed vault address + /// @notice Computes the deterministic vault address for a given vToken + /// @param vToken Address of the vToken + /// @return The computed vault address function computeVaultAddress(address vToken) public view returns (address) { + bool isCore = _isCoreVToken(vToken); + + bytes32 salt = isCore ? CORE_SALT : ISOLATED_SALT; + address beacon = isCore ? address(coreBeacon) : address(isolatedBeacon); + bytes memory initData = isCore + ? abi.encodeWithSelector(VenusERC4626Core.initialize.selector, vToken) + : abi.encodeWithSelector(VenusERC4626Isolated.initialize.selector, vToken); + return address( uint160( @@ -160,15 +206,9 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { abi.encodePacked( bytes1(0xff), address(this), - SALT, + salt, keccak256( - abi.encodePacked( - type(BeaconProxy).creationCode, - abi.encode( - address(beacon), - abi.encodeWithSelector(VenusERC4626.initialize.selector, vToken) - ) - ) + abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(beacon, initData)) ) ) ) @@ -176,4 +216,49 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { ) ); } + + /// @notice Checks if the provided vToken is a core pool vToken + /// @dev This function uses the coreComptroller to verify if the vToken is listed. + /// @param vToken Address of the vToken to check + /// @return True if the vToken is a core pool vToken, false otherwise + function _isCoreVToken(address vToken) internal view returns (bool) { + if (CORE_COMPTROLLER == IComptroller(address(0))) { + return false; + } + (bool listed, ) = CORE_COMPTROLLER.markets(vToken); + return listed; + } + + /// @dev Deploys a new isolated pool vault + /// @param vToken Address of the isolated pool vToken + /// @return The deployed vault as ERC4626Upgradeable + function _deployIsolatedVault(address vToken) private returns (ERC4626Upgradeable) { + VenusERC4626Isolated vault = VenusERC4626Isolated( + address( + new BeaconProxy{ salt: ISOLATED_SALT }( + address(isolatedBeacon), + abi.encodeWithSelector(VenusERC4626Isolated.initialize.selector, vToken) + ) + ) + ); + vault.initialize2(address(_accessControlManager), rewardRecipient, owner(), maxLoopsLimit); + return ERC4626Upgradeable(address(vault)); + } + + /// @dev Deploys a new core pool vault + /// @param vToken Address of the core pool vToken + /// @return The deployed vault as ERC4626Upgradeable + function _deployCoreVault(address vToken) private returns (ERC4626Upgradeable) { + VenusERC4626Core vault = VenusERC4626Core( + address( + new BeaconProxy{ salt: CORE_SALT }( + address(coreBeacon), + abi.encodeWithSelector(VenusERC4626Core.initialize.selector, vToken) + ) + ) + ); + + vault.initialize2(address(_accessControlManager), rewardRecipient, owner()); + return ERC4626Upgradeable(address(vault)); + } } diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol new file mode 100644 index 00000000..90b08c6f --- /dev/null +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { VenusERC4626 } from "./Base/VenusERC4626.sol"; +import { IComptroller } from "./Interfaces/IComptroller.sol"; +import { VToken } from "@venusprotocol/isolated-pools/contracts/VToken.sol"; +import { IERC20Upgradeable, SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewards/RewardsDistributor.sol"; +import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; + +/// @title VenusERC4626Isolated +/// @notice ERC4626 wrapper for Venus Isolated Pool vTokens +contract VenusERC4626Isolated is VenusERC4626 { + /// @notice The maximum number of iterations allowed in certain loop operations. + /// @dev This constant is used to prevent excessive gas consumption by limiting the number of loop iterations. + uint256 public constant LOOPS_LIMIT = 10; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + // Note that the contract is upgradeable. Use initialize() or reinitializers + // to set the state variables. + _disableInitializers(); + } + + /// @notice Initializes the VenusERC4626Isolated contract + /// @dev `initialize2` should be invoked to complete the configuration of the vault + /// @param vToken_ The address of the vToken to be wrapped + function initialize(address vToken_) public virtual initializer { + __VenusERC4626_init(vToken_); + } + + /// @notice Sets the maximum loops limit + /// @param loopsLimit Number of loops limit + function setMaxLoopsLimit(uint256 loopsLimit) external { + _checkAccessAllowed("setMaxLoopsLimit(uint256)"); + _setMaxLoopsLimit(loopsLimit); + } + + /// @inheritdoc VenusERC4626 + function claimRewards() external override { + IComptroller _comptroller = comptroller; + VToken _vToken = VToken(address(vToken)); + address _rewardRecipient = rewardRecipient; + + RewardsDistributor[] memory rewardDistributors = _comptroller.getRewardDistributors(); + + _ensureMaxLoops(rewardDistributors.length); + + for (uint256 i; i < rewardDistributors.length; i++) { + RewardsDistributor rewardDistributor = rewardDistributors[i]; + IERC20Upgradeable rewardToken = IERC20Upgradeable(address(rewardDistributor.rewardToken())); + + VToken[] memory vTokens = new VToken[](1); + vTokens[0] = _vToken; + rewardDistributor.claimRewardToken(address(this), vTokens); + uint256 rewardBalance = rewardToken.balanceOf(address(this)); + + if (rewardBalance > 0) { + SafeERC20Upgradeable.safeTransfer(rewardToken, _rewardRecipient, rewardBalance); + + try + IProtocolShareReserve(_rewardRecipient).updateAssetsState( + address(_comptroller), + address(rewardToken), + IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS + ) + {} catch {} + + emit ClaimRewards(rewardBalance, address(rewardToken)); + } + } + } + + /// @notice Initializes the isolated pool vault with additional parameters + /// @param accessControlManager_ Address of the ACM contract + /// @param rewardRecipient_ Address that will receive rewards + /// @param vaultOwner_ Owner of the vault + /// @param maxLoopsLimit_ Maximum number of loops allowed in certain operations + function initialize2( + address accessControlManager_, + address rewardRecipient_, + address vaultOwner_, + uint256 maxLoopsLimit_ + ) public virtual reinitializer(2) { + __VenusERC4626_init2(accessControlManager_, rewardRecipient_, vaultOwner_); + _setMaxLoopsLimit(maxLoopsLimit_); + } +} diff --git a/contracts/test/Mocks/MockVenusERC4626Core.sol b/contracts/test/Mocks/MockVenusERC4626Core.sol new file mode 100644 index 00000000..346b1348 --- /dev/null +++ b/contracts/test/Mocks/MockVenusERC4626Core.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { VenusERC4626Core } from "../../ERC4626/VenusERC4626Core.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +contract MockVenusERC4626Core is VenusERC4626Core { + mapping(address => uint256) private _balances; + uint256 private mockTotalAssets; + uint256 private mockMaxDeposit; + uint256 private mockMaxWithdraw; + uint256 private mockMaxMint; + uint256 private mockMaxRedeem; + uint256 private mockTotalSupply; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address xvsAddress_) VenusERC4626Core(xvsAddress_) { + _disableInitializers(); + } + + // Mock functions for testing + function setTotalAssets(uint256 _totalAssets) external { + mockTotalAssets = _totalAssets; + } + + function setMaxWithdraw(uint256 _maxWithdraw) external { + mockMaxWithdraw = _maxWithdraw; + } + + function setMaxDeposit(uint256 _maxDeposit) external { + mockMaxDeposit = _maxDeposit; + } + + function setMaxRedeem(uint256 _maxRedeem) external { + mockMaxRedeem = _maxRedeem; + } + + function setMaxMint(uint256 _maxMint) external { + mockMaxMint = _maxMint; + } + + function setTotalSupply(uint256 _totalSupply) external { + mockTotalSupply = _totalSupply; + } + + function setAccountBalance(address account, uint256 balance) public { + _balances[account] = balance; + } + + // Override totalAssets to return the mocked value + function totalAssets() public view override returns (uint256) { + return mockTotalAssets; + } + + // Override maxDeposit to return the mocked value + function maxDeposit(address) public view override returns (uint256) { + return mockMaxDeposit; + } + + // Override maxWithdraw to return the mocked value + function maxWithdraw(address) public view override returns (uint256) { + return mockMaxWithdraw; + } + + function maxMint(address) public view override returns (uint256) { + return mockMaxMint; + } + + function maxRedeem(address) public view override returns (uint256) { + return mockMaxRedeem; + } + + function totalSupply() public view override(ERC20Upgradeable, IERC20Upgradeable) returns (uint256) { + return mockTotalSupply; + } + + function _mint(address account, uint256 amount) internal override { + mockTotalSupply += amount; + _balances[account] += amount; + super._mint(account, amount); + } + + function _burn(address account, uint256 amount) internal override { + mockTotalSupply -= amount; + _balances[account] -= amount; + super._burn(account, amount); + } +} diff --git a/contracts/test/Mocks/MockVenusERC4626.sol b/contracts/test/Mocks/MockVenusERC4626Isolated.sol similarity index 94% rename from contracts/test/Mocks/MockVenusERC4626.sol rename to contracts/test/Mocks/MockVenusERC4626Isolated.sol index 8a9f8357..8c4e808d 100644 --- a/contracts/test/Mocks/MockVenusERC4626.sol +++ b/contracts/test/Mocks/MockVenusERC4626Isolated.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity 0.8.25; -import { VenusERC4626 } from "../../ERC4626/VenusERC4626.sol"; +import { VenusERC4626Isolated } from "../../ERC4626/VenusERC4626Isolated.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -contract MockVenusERC4626 is VenusERC4626 { +contract MockVenusERC4626Isolated is VenusERC4626Isolated { mapping(address => uint256) private _balances; uint256 private mockTotalAssets; uint256 private mockMaxDeposit; diff --git a/contracts/test/VBep20Immutable.sol b/contracts/test/VBep20Immutable.sol new file mode 100644 index 00000000..8a526f4d --- /dev/null +++ b/contracts/test/VBep20Immutable.sol @@ -0,0 +1,51 @@ +pragma solidity ^0.5.16; + +import { VBep20 } from "@venusprotocol/venus-protocol/contracts/Tokens/VTokens/VBep20.sol"; +import { ComptrollerInterface } from "@venusprotocol/venus-protocol/contracts/Comptroller/ComptrollerInterface.sol"; +import { InterestRateModel } from "@venusprotocol/venus-protocol/contracts/InterestRateModels/InterestRateModel.sol"; + +/** + * @title Venus's VBep20Immutable Contract + * @notice VTokens which wrap an EIP-20 underlying and are immutable + * @author Venus + */ +contract VBep20Immutable is VBep20 { + /** + * @notice Construct a new money market + * @param underlying_ The address of the underlying asset + * @param comptroller_ The address of the comptroller + * @param interestRateModel_ The address of the interest rate model + * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 + * @param name_ BEP-20 name of this token + * @param symbol_ BEP-20 symbol of this token + * @param decimals_ BEP-20 decimal precision of this token + * @param admin_ Address of the administrator of this token + */ + constructor( + address underlying_, + ComptrollerInterface comptroller_, + InterestRateModel interestRateModel_, + uint initialExchangeRateMantissa_, + string memory name_, + string memory symbol_, + uint8 decimals_, + address payable admin_ + ) public { + // Creator of the contract is admin during initialization + admin = msg.sender; + + // Initialize the market + initialize( + underlying_, + comptroller_, + interestRateModel_, + initialExchangeRateMantissa_, + name_, + symbol_, + decimals_ + ); + + // Set the proper admin now that initialization is done + admin = admin_; + } +} diff --git a/deploy/020-deploy-VenusERC4626Factory.ts b/deploy/020-deploy-VenusERC4626Factory.ts index e28c2f9c..6e4e3faa 100644 --- a/deploy/020-deploy-VenusERC4626Factory.ts +++ b/deploy/020-deploy-VenusERC4626Factory.ts @@ -16,8 +16,11 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { preconfiguredAddresses.AccessControlManager || "AccessControlManager", ); const poolRegistryAddress = await toAddress(preconfiguredAddresses.PoolRegistry || "PoolRegistry"); + const coreComptroller = await toAddress(preconfiguredAddresses.CoreComptroller || "CoreComptroller"); const proxyOwnerAddress = await toAddress(preconfiguredAddresses.NormalTimelock || "account:deployer"); const rewardRecipientAddress = await toAddress(preconfiguredAddresses.RewardRecipient || "account:deployer"); + const xvsAddress = await toAddress(preconfiguredAddresses.XVS || "XVS"); + const vBNB = await toAddress(preconfiguredAddresses.VBNB || "VBNB"); // Fetch the zk-compatible ProxyAdmin artifact const defaultProxyAdmin = await artifacts.readArtifact( @@ -25,8 +28,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { ); // ERC4626 Beacon - const venusERC4626Implementation: DeployResult = await deploy("VenusERC4626Implementation", { - contract: "VenusERC4626", + const IsolatedImplementation: DeployResult = await deploy("IsolatedImplementation", { + contract: "VenusERC4626Isolated", from: deployer, args: [], log: true, @@ -34,6 +37,15 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { skipIfAlreadyDeployed: true, }); + const CoreImplementation: DeployResult = await deploy("CoreImplementation", { + contract: "VenusERC4626Core", + from: deployer, + args: [xvsAddress], + log: true, + autoMine: true, + skipIfAlreadyDeployed: true, + }); + const loopsLimit = 10; await deploy("VenusERC4626Factory", { @@ -46,9 +58,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { methodName: "initialize", args: [ accessControlManagerAddress, + IsolatedImplementation.address, poolRegistryAddress, rewardRecipientAddress, - venusERC4626Implementation.address, loopsLimit, ], }, @@ -58,6 +70,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }, upgradeIndex: 0, }, + args: [coreComptroller, vBNB], autoMine: true, log: true, skipIfAlreadyDeployed: true, @@ -72,8 +85,14 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { (await erc4626FactoryProxy.owner()) === deployer && (await erc4626FactoryProxy.pendingOwner()) === ethers.constants.AddressZero ) { + console.log( + `Setting the VenusERC4626 implementation for the VTokens on the Core pool, on the erc4626FactoryProxy to ${CoreImplementation.address}`, + ); + let tx = await erc4626FactoryProxy.initialize2(CoreImplementation.address); + await tx.wait(); + console.log(`Transferring ownership of erc4626FactoryProxy to ${targetOwner}`); - const tx = await erc4626FactoryProxy.transferOwnership(targetOwner); + tx = await erc4626FactoryProxy.transferOwnership(targetOwner); await tx.wait(); } }; diff --git a/deployments/berachainbartio.json b/deployments/berachainbartio.json new file mode 100644 index 00000000..0a5aada0 --- /dev/null +++ b/deployments/berachainbartio.json @@ -0,0 +1,5 @@ +{ + "name": "berachainbartio", + "chainId": "80084", + "contracts": {} +} diff --git a/deployments/berachainbartio/.chainId b/deployments/berachainbartio/.chainId new file mode 100644 index 00000000..39513277 --- /dev/null +++ b/deployments/berachainbartio/.chainId @@ -0,0 +1 @@ +80084 \ No newline at end of file diff --git a/deployments/berachainbartio_addresses.json b/deployments/berachainbartio_addresses.json new file mode 100644 index 00000000..737e8adf --- /dev/null +++ b/deployments/berachainbartio_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "berachainbartio", + "chainId": "80084", + "addresses": {} +} diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index 577053b3..79e21957 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -58,6 +58,9 @@ export const preconfiguredAddresses = { VTreasury: "account:deployer", AccessControlManager: Wallet.createRandom().address, PoolRegistry: Wallet.createRandom().address, + CoreComptroller: Wallet.createRandom().address, + XVS: Wallet.createRandom().address, + VBNB: Wallet.createRandom().address, }, bsctestnet: { NormalTimelock: governanceBscTestnet.NormalTimelock.address, diff --git a/tests/hardhat/ERC4626/VenusERC4626Core.ts b/tests/hardhat/ERC4626/VenusERC4626Core.ts new file mode 100644 index 00000000..81960976 --- /dev/null +++ b/tests/hardhat/ERC4626/VenusERC4626Core.ts @@ -0,0 +1,403 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai from "chai"; +import { ethers, upgrades } from "hardhat"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; + +import { + AccessControlManagerMock, + ERC20, + IComptroller, + IProtocolShareReserve, + MockVenusERC4626Core, + VBep20Immutable, +} from "../../../typechain"; + +const { expect } = chai; +chai.use(smock.matchers); + +describe("VenusERC4626Core", () => { + let deployer: SignerWithAddress; + let user: SignerWithAddress; + let vaultOwner: SignerWithAddress; + let venusERC4626Core: MockVenusERC4626Core; + let asset: FakeContract; + let xvs: FakeContract; + let vToken: FakeContract; + let comptroller: FakeContract; + let accessControlManager: FakeContract; + let rewardRecipient: string; + let rewardRecipientPSR: FakeContract; + + beforeEach(async () => { + [deployer, user, vaultOwner] = await ethers.getSigners(); + + // Create Smock Fake Contracts + asset = await smock.fake("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20"); + xvs = await smock.fake("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20"); + vToken = await smock.fake("contracts/test/VBep20Immutable.sol:VBep20Immutable"); + comptroller = await smock.fake("contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller"); + accessControlManager = await smock.fake("AccessControlManagerMock"); + rewardRecipient = deployer.address; + rewardRecipientPSR = await smock.fake( + "contracts/ERC4626/Interfaces/IProtocolShareReserve.sol:IProtocolShareReserve", + ); + + // Configure mock behaviors + accessControlManager.isAllowedToCall.returns(true); + vToken.underlying.returns(asset.address); + vToken.comptroller.returns(comptroller.address); + + // Deploy and initialize MockVenusERC4626 + const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Core"); + + venusERC4626Core = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + initializer: "initialize", + constructorArgs: [xvs.address], + }); + + await venusERC4626Core.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address); + }); + + describe("Initialization", () => { + it("should deploy with correct parameters", async () => { + expect(venusERC4626Core.address).to.not.equal(ethers.constants.AddressZero); + expect(await venusERC4626Core.asset()).to.equal(asset.address); + expect(await venusERC4626Core.vToken()).to.equal(vToken.address); + expect(await venusERC4626Core.comptroller()).to.equal(comptroller.address); + expect(await venusERC4626Core.rewardRecipient()).to.equal(rewardRecipient); + expect(await venusERC4626Core.accessControlManager()).to.equal(accessControlManager.address); + expect(await venusERC4626Core.owner()).to.equal(vaultOwner.address); + }); + }); + + describe("Access Control", () => { + it("should allow authorized accounts to update reward recipient", async () => { + const newRecipient = ethers.Wallet.createRandom().address; + await expect(venusERC4626Core.setRewardRecipient(newRecipient)) + .to.emit(venusERC4626Core, "RewardRecipientUpdated") + .withArgs(rewardRecipient, newRecipient); + }); + }); + + describe("Mint Operations", () => { + const mintShares = ethers.utils.parseEther("10"); + let expectedAssets: ethers.BigNumber; + + beforeEach(async () => { + asset.transferFrom.returns(true); + asset.approve.returns(true); + vToken.mint.returns(0); // NO_ERROR + vToken.exchangeRateStored.returns(ethers.utils.parseUnits("1.0001", 18)); + + await venusERC4626Core.setMaxDeposit(ethers.utils.parseUnits("100", 18)); // Sets max assets + await venusERC4626Core.setMaxMint(ethers.utils.parseUnits("100", 18)); // Sets max shares + await venusERC4626Core.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Core.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + + expectedAssets = await venusERC4626Core.previewMint(mintShares); + + asset.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + asset.balanceOf.returnsAtCall(1, expectedAssets); + + vToken.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + vToken.balanceOf.returnsAtCall(1, expectedAssets); + }); + + it("should mint shares successfully", async () => { + const tx = await venusERC4626Core.connect(user).mint(mintShares, user.address); + + const receipt = await tx.wait(); + const depositEvent = receipt.events?.find(e => e.event === "Deposit"); + const [actualCaller, actualReceiver, actualAssets, actualShares] = depositEvent?.args || []; + + expect(actualCaller).to.equal(user.address); + expect(actualReceiver).to.equal(user.address); + expect(actualAssets).to.be.gte(expectedAssets); + expect(actualShares).to.be.gte(mintShares); + + expect(vToken.mint).to.have.been.calledWith(actualAssets); + + expect(await venusERC4626Core.balanceOf(user.address)).to.equal(actualShares); + }); + + it("should return correct assets amount", async () => { + const returnedAssets = await venusERC4626Core.connect(user).callStatic.mint(mintShares, user.address); + expect(returnedAssets).to.equal(expectedAssets); + }); + + it("should revert if vToken mint fails", async () => { + vToken.mint.returns(1); // Error code 1 + await expect(venusERC4626Core.connect(user).mint(mintShares, user.address)).to.be.revertedWithCustomError( + venusERC4626Core, + "VenusERC4626__VenusError", + ); + }); + + it("should fail mint with no approval", async () => { + asset.transferFrom.returns(false); + await expect(venusERC4626Core.connect(user).mint(mintShares, user.address)).to.be.reverted; + }); + + it("should fail mint zero shares", async () => { + await expect(venusERC4626Core.connect(user).mint(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") + .withArgs("mint"); + }); + }); + + describe("Deposit Operations", () => { + const depositAmount = ethers.utils.parseUnits("10", 18); + let expectedShares: ethers.BigNumber; + + beforeEach(async () => { + asset.transferFrom.returns(true); + asset.approve.returns(true); + + asset.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + asset.balanceOf.returnsAtCall(1, depositAmount); + + vToken.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + vToken.balanceOf.returnsAtCall(1, depositAmount); + + vToken.mint.returns(0); // NO_ERROR + vToken.exchangeRateStored.returns(ethers.utils.parseUnits("1.0001", 18)); + + await venusERC4626Core.setMaxDeposit(ethers.utils.parseEther("100")); // sets max deposit allowed + await venusERC4626Core.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Core.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + }); + + it("should deposit assets successfully", async () => { + // Calculate shares using previewDeposit + expectedShares = await venusERC4626Core.previewDeposit(depositAmount); + + const tx = await venusERC4626Core.connect(user).deposit(depositAmount, user.address); + + const receipt = await tx.wait(); + const depositEvent = receipt.events?.find(e => e.event === "Deposit"); + const [actualCaller, actualReceiver, actualAssets, actualShares] = depositEvent?.args || []; + + expect(actualCaller).to.equal(user.address); + expect(actualReceiver).to.equal(user.address); + expect(actualAssets).to.equal(depositAmount); + expect(actualShares).to.be.gte(expectedShares); + + expect(vToken.mint).to.have.been.calledWith(depositAmount); + expect(await venusERC4626Core.balanceOf(user.address)).to.be.gte(expectedShares); + }); + + it("should revert if vToken mint fails", async () => { + vToken.mint.returns(1); // Error code 1 + await expect( + venusERC4626Core.connect(user).deposit(ethers.utils.parseEther("50"), user.address), + ).to.be.revertedWithCustomError(venusERC4626Core, "VenusERC4626__VenusError"); + }); + + it("should fail deposit with no approval", async () => { + asset.transferFrom.returns(false); + await expect(venusERC4626Core.connect(user).deposit(ethers.utils.parseEther("1"), user.address)).to.be.reverted; + }); + + it("should fail deposit zero amount", async () => { + await expect(venusERC4626Core.connect(user).deposit(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") + .withArgs("deposit"); + }); + }); + + describe("Withdraw Operations", () => { + const depositAmount = ethers.utils.parseEther("10"); + const withdrawAmount = ethers.utils.parseEther("5"); + let expectedShares: ethers.BigNumber; + + beforeEach(async () => { + asset.transferFrom.returns(true); + asset.approve.returns(true); + asset.transfer.returns(true); + + vToken.mint.returns(0); // NO_ERROR + vToken.redeemUnderlying.returns(0); + vToken.exchangeRateStored.returns(ethers.utils.parseUnits("1.0001", 18)); + + asset.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + asset.balanceOf.returnsAtCall(1, depositAmount); + asset.balanceOf.returnsAtCall(2, ethers.utils.parseUnits("110", 18)); + asset.balanceOf.returnsAtCall(3, ethers.utils.parseUnits("110", 18).add(withdrawAmount)); + + vToken.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + vToken.balanceOf.returnsAtCall(1, depositAmount); + vToken.balanceOf.returnsAtCall(2, ethers.utils.parseUnits("110", 18)); + vToken.balanceOf.returnsAtCall(3, ethers.utils.parseUnits("105", 18)); + + await venusERC4626Core.setMaxDeposit(ethers.utils.parseEther("50")); + await venusERC4626Core.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Core.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + await venusERC4626Core.connect(user).deposit(depositAmount, user.address); + await venusERC4626Core.setMaxWithdraw(ethers.utils.parseEther("15")); + await venusERC4626Core.setTotalAssets(ethers.utils.parseUnits("110", 18)); // sets total assets + }); + + it("should withdraw assets successfully", async () => { + expectedShares = await venusERC4626Core.previewWithdraw(withdrawAmount); + + const tx = await venusERC4626Core.connect(user).withdraw(withdrawAmount, user.address, user.address); + + const receipt = await tx.wait(); + const withdrawEvent = receipt.events?.find(e => e.event === "Withdraw"); + const [actualCaller, actualReceiver, actualOwner, actualAssets, actualShares] = withdrawEvent?.args || []; + + expect(actualCaller).to.equal(user.address); + expect(actualReceiver).to.equal(user.address); + expect(actualOwner).to.equal(user.address); + expect(actualAssets).to.gte(withdrawAmount); + expect(expectedShares).to.be.lte(actualShares); + + expect(vToken.redeemUnderlying).to.have.been.calledWith(withdrawAmount); + }); + + it("should revert if vToken redeemUnderlying fails", async () => { + vToken.redeemUnderlying.returns(1); // Error code 1 + await expect( + venusERC4626Core.connect(user).withdraw(withdrawAmount, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Core, "VenusERC4626__VenusError"); + }); + + it("should fail withdraw with no balance", async () => { + await venusERC4626Core.setTotalAssets(0); + await venusERC4626Core.setTotalSupply(0); + await expect(venusERC4626Core.connect(user).withdraw(ethers.utils.parseEther("1"), user.address, user.address)).to + .be.reverted; + }); + + it("should fail withdraw zero amount", async () => { + await expect(venusERC4626Core.connect(user).withdraw(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") + .withArgs("withdraw"); + }); + }); + + describe("Redeem Operations", () => { + const depositAmount = ethers.utils.parseEther("10"); + const redeemShares = ethers.utils.parseEther("5"); + let expectedRedeemAssets: ethers.BigNumber; + + beforeEach(async () => { + asset.transferFrom.returns(true); + asset.approve.returns(true); + asset.transfer.returns(true); + + vToken.mint.returns(0); // NO_ERROR + vToken.redeem.returns(0); + vToken.exchangeRateStored.returns(ethers.utils.parseUnits("1.0001", 18)); + + asset.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + asset.balanceOf.returnsAtCall(1, depositAmount); + asset.balanceOf.returnsAtCall(2, ethers.utils.parseUnits("110", 18)); + asset.balanceOf.returnsAtCall(3, ethers.utils.parseUnits("110", 18).add(redeemShares)); + + vToken.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); + vToken.balanceOf.returnsAtCall(1, depositAmount); + vToken.balanceOf.returnsAtCall(3, ethers.utils.parseUnits("110", 18)); + + await venusERC4626Core.setMaxDeposit(ethers.utils.parseEther("50")); + await venusERC4626Core.setMaxRedeem(ethers.utils.parseEther("50")); + await venusERC4626Core.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Core.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + await venusERC4626Core.connect(user).deposit(depositAmount, user.address); + await venusERC4626Core.setTotalAssets(ethers.utils.parseUnits("110", 18)); // sets total assets + + expectedRedeemAssets = await venusERC4626Core.previewRedeem(redeemShares); + }); + + it("should redeem shares successfully", async () => { + const tx = await venusERC4626Core.connect(user).redeem(redeemShares, user.address, user.address); + + const receipt = await tx.wait(); + const event = receipt.events?.find(e => e.event === "Withdraw"); + const [actualCaller, actualReceiver, actualOwner, actualAssets, actualShares] = event?.args || []; + + expect(actualCaller).to.equal(user.address); + expect(actualReceiver).to.equal(user.address); + expect(actualOwner).to.equal(user.address); + + expect(actualAssets).to.be.gte(expectedRedeemAssets); + expect(actualShares).to.be.gte(redeemShares); + }); + + it("should return correct assets amount", async () => { + const returnedAssets = await venusERC4626Core + .connect(user) + .callStatic.redeem(redeemShares, user.address, user.address); + expect(returnedAssets).to.be.gte(expectedRedeemAssets); + }); + + it("should revert if vToken redeem fails", async () => { + vToken.redeem.returns(1); // Error code 1 + await expect( + venusERC4626Core.connect(user).redeem(redeemShares, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Core, "VenusERC4626__VenusError"); + }); + + it("should fail redeem zero shares", async () => { + await expect(venusERC4626Core.connect(user).redeem(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") + .withArgs("redeem"); + }); + }); + + describe("Reward Distribution", () => { + const rewardAmount = ethers.utils.parseEther("10"); + + describe("When rewardRecipient is EOA", () => { + it("should revert the transaction", async () => { + xvs.balanceOf.whenCalledWith(venusERC4626Core.address).returns(rewardAmount); + xvs.transfer.returns(true); + + await expect(venusERC4626Core.claimRewards()).to.be.reverted; + }); + }); + + describe("When rewardRecipient is ProtocolShareReserve", () => { + let venusERC4626WithPSR: MockVenusERC4626Core; + + beforeEach(async () => { + // Deploy new instance with PSR as reward recipient + const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Core"); + venusERC4626WithPSR = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + initializer: "initialize", + constructorArgs: [xvs.address], + }); + + await venusERC4626WithPSR.initialize2( + accessControlManager.address, + rewardRecipientPSR.address, + vaultOwner.address, + ); + + xvs.balanceOf.whenCalledWith(venusERC4626WithPSR.address).returns(rewardAmount); + xvs.transfer.returns(true); + }); + + it("should claim rewards and update PSR state", async () => { + await expect(venusERC4626WithPSR.claimRewards()) + .to.emit(venusERC4626WithPSR, "ClaimRewards") + .withArgs(rewardAmount, xvs.address); + + expect(comptroller.claimVenus).to.have.been.calledWith( + [venusERC4626WithPSR.address], + [vToken.address], + false, + true, + ); + expect(xvs.transfer).to.have.been.calledWith(rewardRecipientPSR.address, rewardAmount); + + // Verify PSR state update + expect(rewardRecipientPSR.updateAssetsState).to.have.been.calledWith( + comptroller.address, + xvs.address, + 2, // ERC4626_WRAPPER_REWARDS + ); + }); + }); + }); +}); diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 3267de57..1d360c5b 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -5,15 +5,17 @@ import { ethers, upgrades } from "hardhat"; import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; import { - AccessControlManager, + VToken as CoreVToken, ERC20, + IAccessControlManagerV8, IComptroller, + VToken as IsolatedVToken, PoolRegistryInterface, UpgradeableBeacon, - VToken, - VenusERC4626, + VenusERC4626Core, VenusERC4626Factory, -} from "../../../typechain"; + VenusERC4626Isolated, +} from "../../typechain"; const { expect } = chai; chai.use(smock.matchers); @@ -22,179 +24,164 @@ describe("VenusERC4626Factory", () => { let deployer: SignerWithAddress; let user: SignerWithAddress; let factory: VenusERC4626Factory; - let beacon: UpgradeableBeacon; - let listedAsset: FakeContract; - let vTokenA: FakeContract; - let vTokenB: FakeContract; - let fakeVToken: FakeContract; - let unlistedVToken: FakeContract; - let comptroller: FakeContract; + let isolatedBeacon: UpgradeableBeacon; + let coreBeacon: UpgradeableBeacon; + let asset1: FakeContract; + let asset2: FakeContract; + let coreVToken: FakeContract; + let isolatedVToken: FakeContract; + let invalidVToken: FakeContract; + let vBNB: FakeContract; + let coreComptroller: FakeContract; let poolRegistry: FakeContract; - let accessControlManager: FakeContract; + let accessControl: FakeContract; let rewardRecipient: string; - let venusERC4626Impl: VenusERC4626; + let venusERC4626CoreImpl: VenusERC4626Core; + let venusERC4626IsolatedImpl: VenusERC4626Isolated; beforeEach(async () => { [deployer, user] = await ethers.getSigners(); - listedAsset = await smock.fake("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20"); - vTokenA = await smock.fake("@venusprotocol/isolated-pools/contracts/VToken.sol:VToken"); - vTokenB = await smock.fake("@venusprotocol/isolated-pools/contracts/VToken.sol:VToken"); - fakeVToken = await smock.fake("@venusprotocol/isolated-pools/contracts/VToken.sol:VToken"); - unlistedVToken = await smock.fake("@venusprotocol/isolated-pools/contracts/VToken.sol:VToken"); - comptroller = await smock.fake("@venusprotocol/isolated-pools/contracts/Comptroller.sol:Comptroller"); - poolRegistry = await smock.fake( - "@venusprotocol/isolated-pools/contracts/Pool/PoolRegistryInterface.sol:PoolRegistryInterface", - ); - accessControlManager = await smock.fake("AccessControlManager"); + // Setup fake contracts + asset1 = await smock.fake("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20"); + asset2 = await smock.fake("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20"); + coreVToken = await smock.fake("@venusprotocol/venus-protocol/contracts/Tokens/VTokens/VToken.sol:VToken"); + vBNB = await smock.fake("@venusprotocol/venus-protocol/contracts/Tokens/VTokens/VToken.sol:VToken"); + isolatedVToken = await smock.fake("@venusprotocol/isolated-pools/contracts/VToken.sol:VToken"); + invalidVToken = await smock.fake("@venusprotocol/venus-protocol/contracts/Tokens/VTokens/VToken.sol:VToken"); + accessControl = await smock.fake("IAccessControlManagerV8"); rewardRecipient = deployer.address; - accessControlManager.isAllowedToCall.returns(true); - comptroller.poolRegistry.returns(poolRegistry.address); - - vTokenA.comptroller.returns(comptroller.address); - vTokenA.underlying.returns(listedAsset.address); - - const otherAsset = await smock.fake("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20"); - vTokenB.comptroller.returns(comptroller.address); - vTokenB.underlying.returns(otherAsset.address); - - fakeVToken.comptroller.returns(constants.AddressZero); - unlistedVToken.comptroller.returns(comptroller.address); - unlistedVToken.underlying.returns(ethers.Wallet.createRandom().address); // Random underlying + // Setup core pool + const xvsAddress = ethers.Wallet.createRandom().address; + coreComptroller = await smock.fake("contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller"); + coreVToken.comptroller.returns(coreComptroller.address); + coreVToken.underlying.returns(asset1.address); + coreComptroller.markets.whenCalledWith(coreVToken.address).returns([true, 0]); - vTokenB.comptroller.returns(comptroller.address); + // Setup isolated pool + poolRegistry = await smock.fake("PoolRegistryInterface"); + isolatedVToken.comptroller.returns(ethers.Wallet.createRandom().address); + isolatedVToken.underlying.returns(asset2.address); + poolRegistry.getVTokenForAsset.returns(isolatedVToken.address); - poolRegistry.getPoolByComptroller.whenCalledWith(comptroller.address).returns({ - name: "Test Pool", - creator: deployer.address, - comptroller: comptroller.address, - blockPosted: 123456, - timestampPosted: Math.floor(Date.now() / 1000), - }); - - poolRegistry.getPoolByComptroller.whenCalledWith(constants.AddressZero).returns({ - name: "", - creator: constants.AddressZero, - comptroller: constants.AddressZero, - blockPosted: 0, - timestampPosted: 0, - }); + // Setup invalid vToken + invalidVToken.comptroller.returns(constants.AddressZero); - poolRegistry.getVTokenForAsset.whenCalledWith(comptroller.address, listedAsset.address).returns(vTokenA.address); + // Deploy implementations + const VenusERC4626Core = await ethers.getContractFactory("VenusERC4626Core"); + venusERC4626CoreImpl = await VenusERC4626Core.deploy(xvsAddress); - poolRegistry.getVTokenForAsset.whenCalledWith(comptroller.address, otherAsset.address).returns(vTokenB.address); - - const VenusERC4626 = await ethers.getContractFactory("VenusERC4626"); - venusERC4626Impl = await VenusERC4626.deploy(); - await venusERC4626Impl.deployed(); + const VenusERC4626Isolated = await ethers.getContractFactory("VenusERC4626Isolated"); + venusERC4626IsolatedImpl = await VenusERC4626Isolated.deploy(); + // Deploy factory const Factory = await ethers.getContractFactory("VenusERC4626Factory"); + factory = await upgrades.deployProxy( Factory, - [accessControlManager.address, poolRegistry.address, rewardRecipient, venusERC4626Impl.address, 10], - { initializer: "initialize" }, + [accessControl.address, venusERC4626IsolatedImpl.address, poolRegistry.address, rewardRecipient, 10], + { + initializer: "initialize", + constructorArgs: [coreComptroller.address, vBNB.address], + }, ); - beacon = await ethers.getContractAt("UpgradeableBeacon", await factory.beacon()); + await factory.initialize2(venusERC4626CoreImpl.address); + + isolatedBeacon = await ethers.getContractAt("UpgradeableBeacon", await factory.isolatedBeacon()); + coreBeacon = await ethers.getContractAt("UpgradeableBeacon", await factory.coreBeacon()); }); describe("Initialization", () => { it("should set correct initial values", async () => { + expect(await factory.accessControlManager()).to.equal(accessControl.address); expect(await factory.poolRegistry()).to.equal(poolRegistry.address); + expect(await factory.CORE_COMPTROLLER()).to.equal(coreComptroller.address); expect(await factory.rewardRecipient()).to.equal(rewardRecipient); - expect(await factory.maxLoopsLimit()).to.equal(10); }); - it("should setup beacon proxy correctly", async () => { - expect(await beacon.implementation()).to.equal(venusERC4626Impl.address); + it("should setup beacons correctly", async () => { + expect(await isolatedBeacon.implementation()).to.equal(venusERC4626IsolatedImpl.address); + expect(await coreBeacon.implementation()).to.equal(venusERC4626CoreImpl.address); }); - it("should set the owner of the beacon to the owner of the factory", async () => { - expect(await beacon.owner()).to.equal(await factory.owner()); + it("should set beacon owners to factory owner", async () => { + expect(await isolatedBeacon.owner()).to.equal(await factory.owner()); + expect(await coreBeacon.owner()).to.equal(await factory.owner()); }); }); describe("Vault Creation", () => { - it("should create vault and emit event", async () => { - const tx = await factory.createERC4626(vTokenA.address); + it("should create core vault and emit event", async () => { + const tx = await factory.createERC4626(coreVToken.address); const receipt = await tx.wait(); const event = receipt.events?.find(e => e.event === "CreateERC4626"); - expect(event?.args?.vToken).to.equal(vTokenA.address); - expect(event?.args?.vault).to.not.equal(constants.AddressZero); + expect(event?.args?.vToken).to.equal(coreVToken.address); + expect(event?.args?.isCore).to.be.true; }); - it("should set the owner of the vault", async () => { - const tx = await factory.createERC4626(vTokenA.address); + it("should create isolated vault and emit event", async () => { + const tx = await factory.createERC4626(isolatedVToken.address); const receipt = await tx.wait(); - const deployed = receipt.events?.find(e => e.event === "CreateERC4626")?.args?.vault; - - const venusERC4626 = await ethers.getContractAt("VenusERC4626", deployed); + const event = receipt.events?.find(e => e.event === "CreateERC4626"); - expect(await venusERC4626.owner()).to.equal(await factory.owner()); + expect(event?.args?.vToken).to.equal(isolatedVToken.address); + expect(event?.args?.isCore).to.be.false; }); - it("should revert for zero vToken address", async () => { - await expect(factory.createERC4626(constants.AddressZero)).to.be.revertedWithCustomError( + it("should revert for vBNB vToken", async () => { + await expect(factory.createERC4626(vBNB.address)).to.be.revertedWithCustomError( factory, - "ZeroAddressNotAllowed", + "VenusERC4626Factory__InvalidVToken", ); }); - it("should revert for unlisted vToken", async () => { - await expect(factory.createERC4626(unlistedVToken.address)).to.be.revertedWithCustomError( + it("should revert for invalid core vToken", async () => { + coreComptroller.markets.whenCalledWith(invalidVToken.address).returns([false, 0]); + await expect(factory.createERC4626(invalidVToken.address)).to.be.revertedWithCustomError( factory, "VenusERC4626Factory__InvalidVToken", ); }); - }); - - describe("CREATE2 Functionality", () => { - it("should deploy to predicted address", async () => { - const predicted = await factory.computeVaultAddress(vTokenA.address); - const tx = await factory.createERC4626(vTokenA.address); - const receipt = await tx.wait(); - const deployed = receipt.events?.find(e => e.event === "CreateERC4626")?.args?.vault; - expect(deployed).to.equal(predicted); + it("should revert for invalid isolated vToken", async () => { + poolRegistry.getVTokenForAsset.returns(constants.AddressZero); + await expect(factory.createERC4626(invalidVToken.address)).to.be.revertedWithCustomError( + factory, + "VenusERC4626Factory__InvalidVToken", + ); }); - it("should revert for deployment of same vToken", async () => { - await factory.createERC4626(vTokenA.address); - await expect(factory.createERC4626(vTokenA.address)).to.be.revertedWithCustomError( + it("should revert for duplicate vToken", async () => { + await factory.createERC4626(coreVToken.address); + await expect(factory.createERC4626(coreVToken.address)).to.be.revertedWithCustomError( factory, "VenusERC4626Factory__ERC4626AlreadyExists", ); }); + }); - it("should revert for deployment of same vToken after updating reward recipient", async () => { - const newRecipient = ethers.Wallet.createRandom().address; - - await factory.createERC4626(vTokenA.address); - await factory.setRewardRecipient(newRecipient); - - await expect(factory.createERC4626(vTokenA.address)).to.be.reverted; - }); - - it("should revert for deployment of same vToken after updating max loop limit", async () => { - const maxLoopsLimit = await factory.maxLoopsLimit(); - const newMaxLoopLimit = maxLoopsLimit.add(10); - - await factory.createERC4626(vTokenA.address); - await factory.setMaxLoopsLimit(newMaxLoopLimit); - - await expect(factory.createERC4626(vTokenA.address)).to.be.reverted; + describe("CREATE2 Functionality", () => { + it("should deploy core vault to predicted address", async () => { + const predicted = await factory.computeVaultAddress(coreVToken.address); + const tx = await factory.createERC4626(coreVToken.address); + const deployed = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; + expect(deployed).to.equal(predicted); }); - it("Should not revert for deployment of different vTokens", async () => { - await factory.createERC4626(vTokenA.address); - await expect(factory.createERC4626(vTokenB.address)); + it("should deploy isolated vault to predicted address", async () => { + const predicted = await factory.computeVaultAddress(isolatedVToken.address); + const tx = await factory.createERC4626(isolatedVToken.address); + const deployed = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; + expect(deployed).to.equal(predicted); }); }); describe("Access Control", () => { - it("should allow authorized accounts to update reward recipient", async () => { + it("should allow ACM-authorized calls to setRewardRecipient", async () => { + accessControl.isAllowedToCall.returns(true); const newRecipient = ethers.Wallet.createRandom().address; await expect(factory.setRewardRecipient(newRecipient)) .to.emit(factory, "RewardRecipientUpdated") @@ -202,6 +189,7 @@ describe("VenusERC4626Factory", () => { }); it("should allow authorized accounts to update maxLoopsLimit", async () => { + accessControl.isAllowedToCall.returns(true); const maxLoopsLimit = await factory.maxLoopsLimit(); const newMaxLoopLimit = maxLoopsLimit.add(10); await expect(factory.setMaxLoopsLimit(newMaxLoopLimit)) @@ -209,8 +197,8 @@ describe("VenusERC4626Factory", () => { .withArgs(maxLoopsLimit, newMaxLoopLimit); }); - it("should revert when unauthorized user tries to update", async () => { - accessControlManager.isAllowedToCall.returns(false); + it("should revert unauthorized setRewardRecipient calls", async () => { + accessControl.isAllowedToCall.returns(false); await expect(factory.connect(user).setRewardRecipient(user.address)).to.be.revertedWithCustomError( factory, "Unauthorized", @@ -218,21 +206,50 @@ describe("VenusERC4626Factory", () => { }); }); - describe("Beacon Proxy Verification", () => { - it("should deploy valid BeaconProxy", async () => { - // Deploy the vault - const tx = await factory.createERC4626(vTokenA.address); - const receipt = await tx.wait(); - const vaultAddress = receipt.events?.find(e => e.event === "CreateERC4626")?.args?.vault; + describe("Beacon Verification", () => { + it("should use correct beacon for core vault", async () => { + const tx = await factory.createERC4626(coreVToken.address); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; + + const beaconSlot = ethers.utils.hexlify( + ethers.BigNumber.from(ethers.utils.keccak256(ethers.utils.toUtf8Bytes("eip1967.proxy.beacon"))).sub(1), + ); - // Verify proxy storage slot (EIP-1967) - const beaconSlot = ethers.BigNumber.from( - ethers.utils.keccak256(ethers.utils.toUtf8Bytes("eip1967.proxy.beacon")), - ).sub(1); const beaconAddress = await ethers.provider.getStorageAt(vaultAddress, beaconSlot); + expect(ethers.utils.getAddress("0x" + beaconAddress.slice(-40))).to.equal(coreBeacon.address); + }); + + it("should use correct beacon for isolated vault", async () => { + const tx = await factory.createERC4626(isolatedVToken.address); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; + + const beaconSlot = ethers.utils.hexlify( + ethers.BigNumber.from(ethers.utils.keccak256(ethers.utils.toUtf8Bytes("eip1967.proxy.beacon"))).sub(1), + ); + + const beaconAddress = await ethers.provider.getStorageAt(vaultAddress, beaconSlot); + expect(ethers.utils.getAddress("0x" + beaconAddress.slice(-40))).to.equal(isolatedBeacon.address); + }); + }); + + describe("Vault Initialization", () => { + it("should initialize core vault with correct parameters", async () => { + const tx = await factory.createERC4626(coreVToken.address); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; + const vault = await ethers.getContractAt("VenusERC4626Core", vaultAddress); + + expect(await vault.owner()).to.equal(await factory.owner()); + expect(await vault.rewardRecipient()).to.equal(rewardRecipient); + }); + + it("should initialize isolated vault with correct parameters", async () => { + const tx = await factory.createERC4626(isolatedVToken.address); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; + const vault = await ethers.getContractAt("VenusERC4626Isolated", vaultAddress); - // Storage returns 32 bytes, last 20 bytes are the address - expect(ethers.utils.getAddress("0x" + beaconAddress.slice(-40))).to.equal(await factory.beacon()); + expect(await vault.owner()).to.equal(await factory.owner()); + expect(await vault.rewardRecipient()).to.equal(rewardRecipient); + expect(await vault.maxLoopsLimit()).to.equal(await factory.maxLoopsLimit()); }); }); }); diff --git a/tests/hardhat/ERC4626/VenusERC4626.ts b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts similarity index 62% rename from tests/hardhat/ERC4626/VenusERC4626.ts rename to tests/hardhat/ERC4626/VenusERC4626Isolated.ts index 0a8a9f51..e432a076 100644 --- a/tests/hardhat/ERC4626/VenusERC4626.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts @@ -10,18 +10,18 @@ import { IComptroller, IProtocolShareReserve, IRewardsDistributor, - MockVenusERC4626, + MockVenusERC4626Isolated, VToken, } from "../../../typechain"; const { expect } = chai; chai.use(smock.matchers); -describe("VenusERC4626", () => { +describe("VenusERC4626Isolated", () => { let deployer: SignerWithAddress; let user: SignerWithAddress; let vaultOwner: SignerWithAddress; - let venusERC4626: MockVenusERC4626; + let venusERC4626Isolated: MockVenusERC4626Isolated; let asset: FakeContract; let xvs: FakeContract; let vToken: FakeContract; @@ -52,39 +52,39 @@ describe("VenusERC4626", () => { vToken.comptroller.returns(comptroller.address); // Deploy and initialize MockVenusERC4626 - const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626"); + const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Isolated"); - venusERC4626 = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + venusERC4626Isolated = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", }); - await venusERC4626.initialize2(accessControlManager.address, rewardRecipient, 100, vaultOwner.address); + await venusERC4626Isolated.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address, 100); }); describe("Initialization", () => { it("should deploy with correct parameters", async () => { - expect(venusERC4626.address).to.not.equal(ethers.constants.AddressZero); - expect(await venusERC4626.asset()).to.equal(asset.address); - expect(await venusERC4626.vToken()).to.equal(vToken.address); - expect(await venusERC4626.comptroller()).to.equal(comptroller.address); - expect(await venusERC4626.rewardRecipient()).to.equal(rewardRecipient); - expect(await venusERC4626.accessControlManager()).to.equal(accessControlManager.address); - expect(await venusERC4626.owner()).to.equal(vaultOwner.address); + expect(venusERC4626Isolated.address).to.not.equal(ethers.constants.AddressZero); + expect(await venusERC4626Isolated.asset()).to.equal(asset.address); + expect(await venusERC4626Isolated.vToken()).to.equal(vToken.address); + expect(await venusERC4626Isolated.comptroller()).to.equal(comptroller.address); + expect(await venusERC4626Isolated.rewardRecipient()).to.equal(rewardRecipient); + expect(await venusERC4626Isolated.accessControlManager()).to.equal(accessControlManager.address); + expect(await venusERC4626Isolated.owner()).to.equal(vaultOwner.address); }); }); describe("Access Control", () => { it("should allow authorized accounts to update reward recipient", async () => { const newRecipient = ethers.Wallet.createRandom().address; - await expect(venusERC4626.setRewardRecipient(newRecipient)) - .to.emit(venusERC4626, "RewardRecipientUpdated") + await expect(venusERC4626Isolated.setRewardRecipient(newRecipient)) + .to.emit(venusERC4626Isolated, "RewardRecipientUpdated") .withArgs(rewardRecipient, newRecipient); }); it("should allow authorized accounts to update maxLoopsLimit", async () => { - const maxLoopsLimit = await venusERC4626.maxLoopsLimit(); + const maxLoopsLimit = await venusERC4626Isolated.maxLoopsLimit(); const newMaxLoopLimit = maxLoopsLimit.add(10); - await expect(venusERC4626.setMaxLoopsLimit(newMaxLoopLimit)) - .to.emit(venusERC4626, "MaxLoopsLimitUpdated") + await expect(venusERC4626Isolated.setMaxLoopsLimit(newMaxLoopLimit)) + .to.emit(venusERC4626Isolated, "MaxLoopsLimitUpdated") .withArgs(maxLoopsLimit, newMaxLoopLimit); }); }); @@ -99,12 +99,12 @@ describe("VenusERC4626", () => { vToken.mint.returns(0); // NO_ERROR vToken.exchangeRateStored.returns(ethers.utils.parseUnits("1.0001", 18)); - await venusERC4626.setMaxDeposit(ethers.utils.parseUnits("100", 18)); // Sets max assets - await venusERC4626.setMaxMint(ethers.utils.parseUnits("100", 18)); // Sets max shares - await venusERC4626.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply - await venusERC4626.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + await venusERC4626Isolated.setMaxDeposit(ethers.utils.parseUnits("100", 18)); // Sets max assets + await venusERC4626Isolated.setMaxMint(ethers.utils.parseUnits("100", 18)); // Sets max shares + await venusERC4626Isolated.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Isolated.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets - expectedAssets = await venusERC4626.previewMint(mintShares); + expectedAssets = await venusERC4626Isolated.previewMint(mintShares); asset.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); asset.balanceOf.returnsAtCall(1, expectedAssets); @@ -114,7 +114,7 @@ describe("VenusERC4626", () => { }); it("should mint shares successfully with proper vToken accounting", async () => { - const tx = await venusERC4626.connect(user).mint(mintShares, user.address); + const tx = await venusERC4626Isolated.connect(user).mint(mintShares, user.address); const receipt = await tx.wait(); const depositEvent = receipt.events?.find(e => e.event === "Deposit"); @@ -127,30 +127,30 @@ describe("VenusERC4626", () => { expect(vToken.mint).to.have.been.calledWith(actualAssets); - expect(await venusERC4626.balanceOf(user.address)).to.equal(actualShares); + expect(await venusERC4626Isolated.balanceOf(user.address)).to.equal(actualShares); }); it("should return correct assets amount", async () => { - const returnedAssets = await venusERC4626.connect(user).callStatic.mint(mintShares, user.address); + const returnedAssets = await venusERC4626Isolated.connect(user).callStatic.mint(mintShares, user.address); expect(returnedAssets).to.equal(expectedAssets); }); it("should revert if vToken mint fails", async () => { vToken.mint.returns(1); // Error code 1 - await expect(venusERC4626.connect(user).mint(mintShares, user.address)).to.be.revertedWithCustomError( - venusERC4626, + await expect(venusERC4626Isolated.connect(user).mint(mintShares, user.address)).to.be.revertedWithCustomError( + venusERC4626Isolated, "VenusERC4626__VenusError", ); }); it("should fail mint with no approval", async () => { asset.transferFrom.returns(false); - await expect(venusERC4626.connect(user).mint(mintShares, user.address)).to.be.reverted; + await expect(venusERC4626Isolated.connect(user).mint(mintShares, user.address)).to.be.reverted; }); it("should fail mint zero shares", async () => { - await expect(venusERC4626.connect(user).mint(0, user.address)) - .to.be.revertedWithCustomError(venusERC4626, "ERC4626__ZeroAmount") + await expect(venusERC4626Isolated.connect(user).mint(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "ERC4626__ZeroAmount") .withArgs("mint"); }); }); @@ -172,16 +172,16 @@ describe("VenusERC4626", () => { vToken.mint.returns(0); // NO_ERROR vToken.exchangeRateStored.returns(ethers.utils.parseUnits("1.0001", 18)); - await venusERC4626.setMaxDeposit(ethers.utils.parseEther("100")); // sets max deposit allowed - await venusERC4626.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply - await venusERC4626.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + await venusERC4626Isolated.setMaxDeposit(ethers.utils.parseEther("100")); // sets max deposit allowed + await venusERC4626Isolated.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Isolated.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets }); it("should deposit assets successfully", async () => { // Calculate shares using previewDeposit - expectedShares = await venusERC4626.previewDeposit(depositAmount); + expectedShares = await venusERC4626Isolated.previewDeposit(depositAmount); - const tx = await venusERC4626.connect(user).deposit(depositAmount, user.address); + const tx = await venusERC4626Isolated.connect(user).deposit(depositAmount, user.address); const receipt = await tx.wait(); const depositEvent = receipt.events?.find(e => e.event === "Deposit"); @@ -193,24 +193,25 @@ describe("VenusERC4626", () => { expect(actualShares).to.be.gte(expectedShares); expect(vToken.mint).to.have.been.calledWith(depositAmount); - expect(await venusERC4626.balanceOf(user.address)).to.be.gte(expectedShares); + expect(await venusERC4626Isolated.balanceOf(user.address)).to.be.gte(expectedShares); }); it("should revert if vToken mint fails", async () => { vToken.mint.returns(1); // Error code 1 await expect( - venusERC4626.connect(user).deposit(ethers.utils.parseEther("50"), user.address), - ).to.be.revertedWithCustomError(venusERC4626, "VenusERC4626__VenusError"); + venusERC4626Isolated.connect(user).deposit(ethers.utils.parseEther("50"), user.address), + ).to.be.revertedWithCustomError(venusERC4626Isolated, "VenusERC4626__VenusError"); }); it("should fail deposit with no approval", async () => { asset.transferFrom.returns(false); - await expect(venusERC4626.connect(user).deposit(ethers.utils.parseEther("1"), user.address)).to.be.reverted; + await expect(venusERC4626Isolated.connect(user).deposit(ethers.utils.parseEther("1"), user.address)).to.be + .reverted; }); it("should fail deposit zero amount", async () => { - await expect(venusERC4626.connect(user).deposit(0, user.address)) - .to.be.revertedWithCustomError(venusERC4626, "ERC4626__ZeroAmount") + await expect(venusERC4626Isolated.connect(user).deposit(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "ERC4626__ZeroAmount") .withArgs("deposit"); }); }); @@ -239,18 +240,18 @@ describe("VenusERC4626", () => { vToken.balanceOf.returnsAtCall(2, ethers.utils.parseUnits("110", 18)); vToken.balanceOf.returnsAtCall(3, ethers.utils.parseUnits("105", 18)); - await venusERC4626.setMaxDeposit(ethers.utils.parseEther("50")); - await venusERC4626.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply - await venusERC4626.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets - await venusERC4626.connect(user).deposit(depositAmount, user.address); - await venusERC4626.setMaxWithdraw(ethers.utils.parseEther("15")); - await venusERC4626.setTotalAssets(ethers.utils.parseUnits("110", 18)); // sets total assets + await venusERC4626Isolated.setMaxDeposit(ethers.utils.parseEther("50")); + await venusERC4626Isolated.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Isolated.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + await venusERC4626Isolated.connect(user).deposit(depositAmount, user.address); + await venusERC4626Isolated.setMaxWithdraw(ethers.utils.parseEther("15")); + await venusERC4626Isolated.setTotalAssets(ethers.utils.parseUnits("110", 18)); // sets total assets }); it("should withdraw assets successfully", async () => { - expectedShares = await venusERC4626.previewWithdraw(withdrawAmount); + expectedShares = await venusERC4626Isolated.previewWithdraw(withdrawAmount); - const tx = await venusERC4626.connect(user).withdraw(withdrawAmount, user.address, user.address); + const tx = await venusERC4626Isolated.connect(user).withdraw(withdrawAmount, user.address, user.address); const receipt = await tx.wait(); const withdrawEvent = receipt.events?.find(e => e.event === "Withdraw"); @@ -268,20 +269,21 @@ describe("VenusERC4626", () => { it("should revert if vToken redeemUnderlying fails", async () => { vToken.redeemUnderlying.returns(1); // Error code 1 await expect( - venusERC4626.connect(user).withdraw(withdrawAmount, user.address, user.address), - ).to.be.revertedWithCustomError(venusERC4626, "VenusERC4626__VenusError"); + venusERC4626Isolated.connect(user).withdraw(withdrawAmount, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Isolated, "VenusERC4626__VenusError"); }); it("should fail withdraw with no balance", async () => { - await venusERC4626.setTotalAssets(0); - await venusERC4626.setTotalSupply(0); - await expect(venusERC4626.connect(user).withdraw(ethers.utils.parseEther("1"), user.address, user.address)).to.be - .reverted; + await venusERC4626Isolated.setTotalAssets(0); + await venusERC4626Isolated.setTotalSupply(0); + await expect( + venusERC4626Isolated.connect(user).withdraw(ethers.utils.parseEther("1"), user.address, user.address), + ).to.be.reverted; }); it("should fail withdraw zero amount", async () => { - await expect(venusERC4626.connect(user).withdraw(0, user.address, user.address)) - .to.be.revertedWithCustomError(venusERC4626, "ERC4626__ZeroAmount") + await expect(venusERC4626Isolated.connect(user).withdraw(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "ERC4626__ZeroAmount") .withArgs("withdraw"); }); }); @@ -309,18 +311,18 @@ describe("VenusERC4626", () => { vToken.balanceOf.returnsAtCall(1, depositAmount); vToken.balanceOf.returnsAtCall(3, ethers.utils.parseUnits("110", 18)); - await venusERC4626.setMaxDeposit(ethers.utils.parseEther("50")); - await venusERC4626.setMaxRedeem(ethers.utils.parseEther("50")); - await venusERC4626.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply - await venusERC4626.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets - await venusERC4626.connect(user).deposit(depositAmount, user.address); - await venusERC4626.setTotalAssets(ethers.utils.parseUnits("110", 18)); // sets total assets + await venusERC4626Isolated.setMaxDeposit(ethers.utils.parseEther("50")); + await venusERC4626Isolated.setMaxRedeem(ethers.utils.parseEther("50")); + await venusERC4626Isolated.setTotalSupply(ethers.utils.parseUnits("100", 18)); // sets total supply + await venusERC4626Isolated.setTotalAssets(ethers.utils.parseUnits("100", 18)); // sets total assets + await venusERC4626Isolated.connect(user).deposit(depositAmount, user.address); + await venusERC4626Isolated.setTotalAssets(ethers.utils.parseUnits("110", 18)); // sets total assets - expectedRedeemAssets = await venusERC4626.previewRedeem(redeemShares); + expectedRedeemAssets = await venusERC4626Isolated.previewRedeem(redeemShares); }); it("should redeem shares successfully", async () => { - const tx = await venusERC4626.connect(user).redeem(redeemShares, user.address, user.address); + const tx = await venusERC4626Isolated.connect(user).redeem(redeemShares, user.address, user.address); const receipt = await tx.wait(); const event = receipt.events?.find(e => e.event === "Withdraw"); @@ -335,7 +337,7 @@ describe("VenusERC4626", () => { }); it("should return correct assets amount", async () => { - const returnedAssets = await venusERC4626 + const returnedAssets = await venusERC4626Isolated .connect(user) .callStatic.redeem(redeemShares, user.address, user.address); @@ -345,13 +347,13 @@ describe("VenusERC4626", () => { it("should revert if vToken redeem fails", async () => { vToken.redeem.returns(1); // Error code 1 await expect( - venusERC4626.connect(user).redeem(redeemShares, user.address, user.address), - ).to.be.revertedWithCustomError(venusERC4626, "VenusERC4626__VenusError"); + venusERC4626Isolated.connect(user).redeem(redeemShares, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Isolated, "VenusERC4626__VenusError"); }); it("should fail redeem zero shares", async () => { - await expect(venusERC4626.connect(user).redeem(0, user.address, user.address)) - .to.be.revertedWithCustomError(venusERC4626, "ERC4626__ZeroAmount") + await expect(venusERC4626Isolated.connect(user).redeem(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "ERC4626__ZeroAmount") .withArgs("redeem"); }); }); @@ -362,43 +364,45 @@ describe("VenusERC4626", () => { beforeEach(async () => { comptroller.getRewardDistributors.returns([rewardDistributor.address]); rewardDistributor.rewardToken.returns(xvs.address); - xvs.balanceOf.whenCalledWith(venusERC4626.address).returns(rewardAmount); + xvs.balanceOf.whenCalledWith(venusERC4626Isolated.address).returns(rewardAmount); }); describe("When rewardRecipient is EOA", () => { it("should revert the transaction", async () => { xvs.transfer.returns(true); - await expect(venusERC4626.connect(user).claimRewards()).to.be.reverted; + await expect(venusERC4626Isolated.connect(user).claimRewards()).to.be.reverted; }); }); describe("When rewardRecipient is ProtocolShareReserve", () => { beforeEach(async () => { // Redeploy with PSR as rewardRecipient - const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626"); - venusERC4626 = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Isolated"); + venusERC4626Isolated = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", }); - await venusERC4626.initialize2( + await venusERC4626Isolated.initialize2( accessControlManager.address, rewardRecipientPSR.address, - 100, vaultOwner.address, + 100, ); comptroller.getRewardDistributors.returns([rewardDistributor.address]); rewardDistributor.rewardToken.returns(xvs.address); - xvs.balanceOf.whenCalledWith(venusERC4626.address).returns(rewardAmount); + xvs.balanceOf.whenCalledWith(venusERC4626Isolated.address).returns(rewardAmount); }); it("should claim rewards and update PSR state", async () => { xvs.transfer.returns(true); - await expect(venusERC4626.connect(user).claimRewards()) - .to.emit(venusERC4626, "ClaimRewards") + await expect(venusERC4626Isolated.connect(user).claimRewards()) + .to.emit(venusERC4626Isolated, "ClaimRewards") .withArgs(rewardAmount, xvs.address); - expect(rewardDistributor.claimRewardToken).to.have.been.calledWith(venusERC4626.address, [vToken.address]); + expect(rewardDistributor.claimRewardToken).to.have.been.calledWith(venusERC4626Isolated.address, [ + vToken.address, + ]); expect(rewardRecipientPSR.updateAssetsState).to.have.been.calledWith(comptroller.address, xvs.address, 2); });