From 54d42a0fcb0d7ad8c3d2276fae70e5615d8f7fb1 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 10 Jun 2025 18:34:19 +0530 Subject: [PATCH 01/41] feat: single factory for bnb chain --- contracts/ERC4626/VenusERC4626Factory.sol | 250 ++++++++++------- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 277 ++++++++++--------- 2 files changed, 293 insertions(+), 234 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 79a3a4e5..7aacbc95 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -1,87 +1,112 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.25; +pragma solidity ^0.8.25; import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.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 as IsolatedVTokenInterface } from "@venusprotocol/isolated-pools/contracts/VTokenInterfaces.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"); + // --- Constants --- - /// @notice The beacon contract for VenusERC4626 proxies - UpgradeableBeacon public beacon; + /// @notice Salt used to deterministically deploy isolated pool vaults + bytes32 public constant ISOLATED_SALT = keccak256("Venus-Isolated-ERC4626"); - /// @notice The Pool Registry contract + /// @notice Salt used to deterministically deploy core pool vaults + bytes32 public constant CORE_SALT = keccak256("Venus-Core-ERC4626"); + + // --- State Variables --- + + /// @notice Beacon for isolated vaults + UpgradeableBeacon public isolatedBeacon; + + /// @notice Beacon for core vaults + UpgradeableBeacon public coreBeacon; + + /// @notice PoolRegistry contract to validate isolated pool vTokens PoolRegistryInterface public poolRegistry; - /// @notice The address that will receive the liquidity mining rewards + /// @notice Comptroller for core pool validation + IComptroller public coreComptroller; + + /// @notice Address to which rewards will be distributed address public rewardRecipient; - // @notice Map of vaults created by this factory - mapping(address vToken => ERC4626Upgradeable vault) public createdVaults; + /// @notice Mapping from vToken to deployed ERC4626 vault + mapping(address => ERC4626Upgradeable) public vaults; - /// @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 Mapping indicating whether a vault belongs to core pool + 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 was created + /// @param vault The deployed ERC4626 vault address + /// @param isCore Whether the vault is for a core pool + event VaultCreated(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 - error VenusERC4626Factory__InvalidVToken(); + /// @notice Thrown when a vault already exists for the given vToken + error VaultAlreadyExists(); - /// @notice Thrown when a VenusERC4626 already exists for the provided vToken - error VenusERC4626Factory__ERC4626AlreadyExists(); + /// @notice Thrown when the vToken provided is not valid (either unlisted or not part of the pool registry) + error InvalidVToken(); + /// @notice Constructor (disable initializer for upgradeable contract) /// @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 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 + /// @param accessControlManager Access control manager address + /// @param isolatedImplementation Implementation address for isolated vaults + /// @param coreImplementation Implementation address for core vaults + /// @param poolRegistry_ Pool registry address + /// @param coreComptroller_ Core pool comptroller address + /// @param rewardRecipient_ Initial reward recipient address function initialize( address accessControlManager, - address poolRegistryAddress, - address rewardRecipientAddress, - address venusERC4626Implementation, + address isolatedImplementation, + address coreImplementation, + address poolRegistry_, + address coreComptroller_, + address rewardRecipient_, uint256 loopsLimitNumber ) external initializer { - ensureNonzeroAddress(accessControlManager); - ensureNonzeroAddress(poolRegistryAddress); - ensureNonzeroAddress(rewardRecipientAddress); - ensureNonzeroAddress(venusERC4626Implementation); + ensureNonzeroAddress(isolatedImplementation); + ensureNonzeroAddress(coreImplementation); + ensureNonzeroAddress(poolRegistry_); + ensureNonzeroAddress(coreComptroller_); + ensureNonzeroAddress(rewardRecipient_); __AccessControlled_init(accessControlManager); - - poolRegistry = PoolRegistryInterface(poolRegistryAddress); - rewardRecipient = rewardRecipientAddress; _setMaxLoopsLimit(loopsLimitNumber); - // Deploy the upgradeable beacon with the initial implementation - beacon = new UpgradeableBeacon(venusERC4626Implementation); + isolatedBeacon = new UpgradeableBeacon(isolatedImplementation); + coreBeacon = new UpgradeableBeacon(coreImplementation); + + poolRegistry = PoolRegistryInterface(poolRegistry_); + coreComptroller = IComptroller(coreComptroller_); + rewardRecipient = rewardRecipient_; - // The owner of the beacon will initially be the owner of the factory - beacon.transferOwnership(owner()); + isolatedBeacon.transferOwnership(owner()); + coreBeacon.transferOwnership(owner()); } /// @notice Sets a new reward recipient address @@ -97,61 +122,55 @@ 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 - /// @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())) { - revert VenusERC4626Factory__InvalidVToken(); + /// @notice Creates an ERC4626 vault for the given vToken + /// @param vToken Address of the vToken + /// @param isCore Indicates if the vToken is part of the core pool + /// @return vault The deployed ERC4626 vault + /// @custom:error VaultAlreadyExists if a vault already exists for the vToken + /// @custom:error InvalidVToken if the vToken is invalid or unlisted + /// @custom:event Emits VaultCreated event on successful deployment + function createERC4626(address vToken, bool isCore) external returns (ERC4626Upgradeable vault) { + if (address(vaults[vToken]) != address(0)) revert VaultAlreadyExists(); + + if (isCore) { + (bool listed, ) = coreComptroller.markets(vToken); + if (!listed) revert InvalidVToken(); + vault = _deployCoreVault(vToken); + } else { + address underlying = IsolatedVTokenInterface(vToken).underlying(); + address comptroller = address(IsolatedVTokenInterface(vToken).comptroller()); + if (vToken != poolRegistry.getVTokenForAsset(comptroller, underlying)) { + revert InvalidVToken(); + } + + vault = _deployIsolatedVault(vToken); } - VenusERC4626 venusERC4626 = VenusERC4626( - address( - new BeaconProxy{ salt: SALT }( - address(beacon), - abi.encodeWithSelector(VenusERC4626.initialize.selector, vToken) - ) - ) - ); - - venusERC4626.initialize2(address(_accessControlManager), rewardRecipient, maxLoopsLimit, owner()); - - vault = ERC4626Upgradeable(address(venusERC4626)); - - createdVaults[vToken] = vault; - - emit CreateERC4626(vToken_, vault); + vaults[vToken] = vault; + isCoreVault[vToken] = isCore; + emit VaultCreated(vToken, address(vault), isCore); } - /// @notice Predicts the vault address for a given vToken - /// @param vToken The vToken address - /// @return The precomputed vault address - function computeVaultAddress(address vToken) public view returns (address) { + /// @notice Computes the deterministic vault address for a given vToken + /// @param vToken Address of the vToken + /// @param isCore Indicates if the vault is for core pool + /// @return The computed vault address + function computeVaultAddress(address vToken, bool isCore) public view returns (address) { + 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 +179,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 +189,37 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { ) ); } + + /// @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, 100, owner()); + 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/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 9c81d513..40035fcf 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, + ComptrollerInterface, + VToken as CoreVToken, ERC20, - IComptroller, + VenusERC4626Factory, + IAccessControlManagerV8, + VToken as IsolatedVToken, PoolRegistryInterface, UpgradeableBeacon, - VToken, - VenusERC4626, - VenusERC4626Factory, -} from "../../../typechain"; + VenusERC4626Core, + VenusERC4626Isolated, +} from "../../typechain"; const { expect } = chai; chai.use(smock.matchers); @@ -22,177 +24,158 @@ 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 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("VToken"); - vTokenB = await smock.fake("VToken"); - fakeVToken = await smock.fake("VToken"); - unlistedVToken = await smock.fake("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("VToken"); + isolatedVToken = await smock.fake("VToken"); + invalidVToken = await smock.fake("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 - - vTokenB.comptroller.returns(comptroller.address); - - poolRegistry.getPoolByComptroller.whenCalledWith(comptroller.address).returns({ - name: "Test Pool", - creator: deployer.address, - comptroller: comptroller.address, - blockPosted: 123456, - timestampPosted: Math.floor(Date.now() / 1000), - }); + // Setup core pool + coreComptroller = await smock.fake( + "contracts/interfaces/ComptrollerInterface.sol:ComptrollerInterface", + ); + coreVToken.comptroller.returns(coreComptroller.address); + coreVToken.underlying.returns(asset1.address); + coreComptroller.markets.whenCalledWith(coreVToken.address).returns([true, 0]); - poolRegistry.getPoolByComptroller.whenCalledWith(constants.AddressZero).returns({ - name: "", - creator: constants.AddressZero, - comptroller: constants.AddressZero, - blockPosted: 0, - timestampPosted: 0, - }); + // 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.getVTokenForAsset.whenCalledWith(comptroller.address, listedAsset.address).returns(vTokenA.address); + // Setup invalid vToken + invalidVToken.comptroller.returns(constants.AddressZero); - poolRegistry.getVTokenForAsset.whenCalledWith(comptroller.address, otherAsset.address).returns(vTokenB.address); + // Deploy implementations + const VenusERC4626Core = await ethers.getContractFactory("VenusERC4626Core"); + venusERC4626CoreImpl = await VenusERC4626Core.deploy(); - 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], + [ + accessControl.address, + venusERC4626IsolatedImpl.address, + venusERC4626CoreImpl.address, + poolRegistry.address, + coreComptroller.address, + rewardRecipient, + 100, + ], { initializer: "initialize" }, ); - beacon = await ethers.getContractAt("UpgradeableBeacon", await factory.beacon()); + 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.coreComptroller()).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, true); const receipt = await tx.wait(); - const event = receipt.events?.find(e => e.event === "CreateERC4626"); + const event = receipt.events?.find(e => e.event === "VaultCreated"); - 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, false); 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 === "VaultCreated"); - 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 invalid core vToken", async () => { + coreComptroller.markets.whenCalledWith(invalidVToken.address).returns([false, 0]); + await expect(factory.createERC4626(invalidVToken.address, true)).to.be.revertedWithCustomError( factory, - "ZeroAddressNotAllowed", + "InvalidVToken", ); }); - it("should revert for unlisted vToken", async () => { - await expect(factory.createERC4626(unlistedVToken.address)).to.be.revertedWithCustomError( + it("should revert for invalid isolated vToken", async () => { + poolRegistry.getVTokenForAsset.returns(constants.AddressZero); + await expect(factory.createERC4626(invalidVToken.address, false)).to.be.revertedWithCustomError( factory, - "VenusERC4626Factory__InvalidVToken", + "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 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, true); + await expect(factory.createERC4626(coreVToken.address, true)).to.be.revertedWithCustomError( factory, - "VenusERC4626Factory__ERC4626AlreadyExists", + "VaultAlreadyExists", ); }); + }); - 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, true); + const tx = await factory.createERC4626(coreVToken.address, true); + const deployed = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.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, false); + const tx = await factory.createERC4626(isolatedVToken.address, false); + const deployed = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.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") @@ -200,6 +183,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)) @@ -207,8 +191,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", @@ -216,21 +200,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, true); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.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, false); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.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, true); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.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, false); + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.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()); }); }); }); From a4eadc22373de056ffdfbee1b4da2639dafd35e3 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 10 Jun 2025 18:35:03 +0530 Subject: [PATCH 02/41] feat: separate wrappers for core and isolated pools --- contracts/ERC4626/VenusERC4626Core.sol | 428 +++++++++++++++++++ contracts/ERC4626/VenusERC4626Isolated.sol | 465 +++++++++++++++++++++ 2 files changed, 893 insertions(+) create mode 100644 contracts/ERC4626/VenusERC4626Core.sol create mode 100644 contracts/ERC4626/VenusERC4626Isolated.sol diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol new file mode 100644 index 00000000..e1363b59 --- /dev/null +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.25; + +import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +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 { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; + +import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; +import { IComptroller, Action } from "./Interfaces/IComptroller.sol"; +import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; + +/// @title VenusERC4626 +/// @notice ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. +contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable { + using SafeERC20Upgradeable for ERC20Upgradeable; + + enum Rounding { + Down, + Up + } + + /// @notice Error code representing no errors in Venus operations. + uint256 internal constant NO_ERROR = 0; + + /// @notice Base unit for computations, usually used in scaling (multiplications, divisions) + uint256 internal constant EXP_SCALE = 1e18; + + /// @notice The Venus vToken associated with this ERC4626 vault. + VTokenInterface public vToken; + + /// @notice The Venus Comptroller contract, responsible for market operations. + IComptroller public comptroller; + + /// @notice The recipient of rewards distributed by the Venus Protocol. + address public rewardRecipient; + + /// @notice Emitted when rewards are claimed. + /// @param amount The amount of reward tokens claimed. + /// @param rewardToken The address of the reward token claimed. + event ClaimRewards(uint256 amount, address indexed rewardToken); + + /// @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 Event emitted when tokens are swept + event SweepToken(address indexed token, address indexed receiver, uint256 amount); + + /// @notice Thrown when a Venus protocol call returns an error. + /// @dev This error is triggered if a Venus operation (such as minting or redeeming vTokens) fails. + /// @param errorCode The error code returned by the Venus protocol. + error VenusERC4626__VenusError(uint256 errorCode); + + /// @notice Thrown when a deposit exceeds the maximum allowed limit. + /// @dev This error is triggered if the deposit amount is greater than `maxDeposit(receiver)`. + error ERC4626__DepositMoreThanMax(); + + /// @notice Thrown when a mint operation exceeds the maximum allowed limit. + /// @dev This error is triggered if the mint amount is greater than `maxMint(receiver)`. + error ERC4626__MintMoreThanMax(); + + /// @notice Thrown when a withdrawal exceeds the maximum available assets. + /// @dev This error is triggered if the withdrawal amount is greater than `maxWithdraw(owner)`. + error ERC4626__WithdrawMoreThanMax(); + + /// @notice Thrown when a redemption exceeds the maximum redeemable shares. + /// @dev This error is triggered if the redemption amount is greater than `maxRedeem(owner)`. + error ERC4626__RedeemMoreThanMax(); + + /// @notice Thrown when attempting an operation with a zero amount. + /// @dev This error prevents unnecessary transactions with zero amounts in deposit, withdraw, mint, or redeem operations. + /// @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 = VTokenInterface(vToken_); + comptroller = IComptroller(address(vToken.comptroller())); + ERC20Upgradeable asset = ERC20Upgradeable(vToken.underlying()); + + __ERC20_init(_generateVaultName(asset), _generateVaultSymbol(asset)); + __ERC4626_init(asset); + __ReentrancyGuard_init(); + } + + /// @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 { + _checkAccessAllowed("setRewardRecipient(address)"); + _setRewardRecipient(newRecipient); + } + + /// @notice Sweeps the input token address tokens from the contract and sends them to the owner + /// @param token Address of the token + /// @custom:event SweepToken emits on success + /// @custom:access Only owner + function sweepToken(IERC20Upgradeable token) external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + + if (balance > 0) { + address owner_ = owner(); + SafeERC20Upgradeable.safeTransfer(token, owner_, balance); + emit SweepToken(address(token), owner_, balance); + } + } + + /// @notice Claims XVS rewards from Venus Core Pool and transfers them to rewardRecipient + function claimRewards() external { + comptroller.claimVenus(address(this)); + + address xvsAddress = comptroller.getXVSAddress(); + IERC20Upgradeable xvs = IERC20Upgradeable(xvsAddress); + uint256 rewardAmount = xvs.balanceOf(address(this)); + + if (rewardAmount > 0) { + SafeERC20Upgradeable.safeTransfer(xvs, rewardRecipient, rewardAmount); + + bytes memory data = abi.encodeCall( + IProtocolShareReserve.updateAssetsState, + (address(comptroller), xvsAddress, IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS) + ); + rewardRecipient.call(data); + + emit ClaimRewards(rewardAmount, xvsAddress); + } + } + + /// @notice Second function to invoked 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 initialize2( + address accessControlManager_, + address rewardRecipient_, + address vaultOwner_ + ) public reinitializer(2) { + ensureNonzeroAddress(vaultOwner_); + + __AccessControlled_init(accessControlManager_); + _setRewardRecipient(rewardRecipient_); + _transferOwnership(vaultOwner_); + } + + /// @inheritdoc ERC4626Upgradeable + function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + + vToken.accrueInterest(); + if (assets == 0) { + revert ERC4626__ZeroAmount("deposit"); + } + if (assets > maxDeposit(receiver)) { + revert ERC4626__DepositMoreThanMax(); + } + + uint256 shares = previewDeposit(assets); + if (shares == 0) { + revert ERC4626__ZeroAmount("deposit"); + } + _deposit(_msgSender(), receiver, assets, shares); + afterDeposit(assets); + return shares; + } + + /// @inheritdoc ERC4626Upgradeable + function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + + vToken.accrueInterest(); + if (shares == 0) { + revert ERC4626__ZeroAmount("mint"); + } + if (shares > maxMint(receiver)) { + revert ERC4626__MintMoreThanMax(); + } + + uint256 assets = previewMint(shares); + if (assets == 0) { + revert ERC4626__ZeroAmount("mint"); + } + _deposit(_msgSender(), receiver, assets, shares); + afterDeposit(assets); + return assets; + } + + /// @inheritdoc ERC4626Upgradeable + function withdraw(uint256 assets, address receiver, address owner) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + ensureNonzeroAddress(owner); + + vToken.accrueInterest(); + if (assets == 0) { + revert ERC4626__ZeroAmount("withdraw"); + } + if (assets > maxWithdraw(owner)) { + revert ERC4626__WithdrawMoreThanMax(); + } + + uint256 shares = previewWithdraw(assets); + if (shares == 0) { + revert ERC4626__ZeroAmount("withdraw"); + } + uint256 actualAssets = beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, shares); + return shares; + } + + /// @inheritdoc ERC4626Upgradeable + function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + ensureNonzeroAddress(owner); + + vToken.accrueInterest(); + if (shares == 0) { + revert ERC4626__ZeroAmount("redeem"); + } + if (shares > maxRedeem(owner)) { + revert ERC4626__RedeemMoreThanMax(); + } + + uint256 assets = previewRedeem(shares); + if (assets == 0) { + revert ERC4626__ZeroAmount("redeem"); + } + + // actualAssets should be equal to assets, because of the round performed in previewRedeem + // but let's use the returned value anyway + uint256 actualAssets = beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, shares); + return actualAssets; + } + + /// @inheritdoc ERC4626Upgradeable + function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + // round down the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals + uint256 adjustedAssets = _roundAssets(assets, Rounding.Down); + + return super.previewDeposit(adjustedAssets); + } + + /// @inheritdoc ERC4626Upgradeable + function previewMint(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewMint(shares); + + // round down the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals + return _roundAssets(assets, Rounding.Down); + } + + /// @inheritdoc ERC4626Upgradeable + function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + // round up the asset amount to redeem a natural number of vTokens, without decimals + uint256 adjustedAssets = _roundAssets(assets, Rounding.Up); + + return super.previewWithdraw(adjustedAssets); + } + + /// @inheritdoc ERC4626Upgradeable + function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewRedeem(shares); + + // round down the asset amount to redeem a natural number of vTokens, without decimals + return _roundAssets(assets, Rounding.Down); + } + + /// @notice Returns the total amount of assets deposited + /// @return Amount of assets deposited + function totalAssets() public view virtual override returns (uint256) { + return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; + } + + /// @notice Returns the maximum deposit allowed based on Venus supply caps. + /// @dev If minting is paused or the supply cap is reached, returns 0. + /// @param /*account*/ The address of the account. + /// @return The maximum amount of assets that can be deposited. + function maxDeposit(address /*account*/) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.MINT)) { + return 0; + } + + uint256 supplyCap = comptroller.supplyCaps(address(vToken)); + uint256 totalSupply_ = (vToken.totalSupply() * vToken.exchangeRateStored()) / EXP_SCALE; + return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; + } + + /// @notice Returns the maximum amount of shares that can be minted. + /// @dev This is derived from the maximum deposit amount converted to shares. + /// @param /*account*/ The address of the account. + /// @return The maximum number of shares that can be minted. + function maxMint(address /*account*/) public view virtual override returns (uint256) { + return convertToShares(maxDeposit(address(0))); + } + + /// @notice Returns the maximum amount that can be withdrawn. + /// @dev The withdrawable amount is limited by the available cash in the vault. + /// @param receiver The address of the account withdrawing. + /// @return The maximum amount of assets that can be withdrawn. + function maxWithdraw(address receiver) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { + return 0; + } + + uint256 cash = vToken.getCash(); + uint256 totalReserves = vToken.totalReserves(); + uint256 assetsBalance = convertToAssets(balanceOf(receiver)); + + if (cash < totalReserves) { + return 0; + } else { + uint256 availableCash = cash - totalReserves; + return availableCash < assetsBalance ? availableCash : assetsBalance; + } + } + + /// @notice Returns the maximum amount of shares that can be redeemed. + /// @dev Redemption is limited by the available cash in the vault. + /// @param receiver The address of the account redeeming. + /// @return The maximum number of shares that can be redeemed. + function maxRedeem(address receiver) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { + return 0; + } + + uint256 cash = vToken.getCash(); + uint256 totalReserves = vToken.totalReserves(); + if (cash < totalReserves) { + return 0; + } else { + uint256 availableCash = cash - totalReserves; + uint256 availableCashInShares = convertToShares(availableCash); + uint256 shareBalance = balanceOf(receiver); + return availableCashInShares < shareBalance ? availableCashInShares : shareBalance; + } + } + + /// @notice Redeems underlying assets before withdrawing from the vault. + /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. + /// @param assets The amount of underlying assets to redeem. + /// @return The amount of assets transferred in + function beforeWithdraw(uint256 assets) internal returns (uint256) { + IERC20Upgradeable token = IERC20Upgradeable(asset()); + uint256 balanceBefore = token.balanceOf(address(this)); + + uint256 errorCode = vToken.redeemUnderlying(assets); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + + uint256 balanceAfter = token.balanceOf(address(this)); + // Return the amount that was *actually* transferred + return balanceAfter - balanceBefore; + } + + /// @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 afterDeposit(uint256 assets) internal { + ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); + uint256 errorCode = vToken.mint(assets); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + } + + /// @notice Sets a new reward recipient address + /// @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 { + ensureNonzeroAddress(newRecipient); + + emit RewardRecipientUpdated(rewardRecipient, newRecipient); + rewardRecipient = newRecipient; + } + + /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. + /// @return Gap between 18 and the decimals of the asset token + function _decimalsOffset() internal view virtual override returns (uint8) { + return 18 - ERC20Upgradeable(asset()).decimals(); + } + + /// @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) { + 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) { + return string(abi.encodePacked("v4626", asset_.symbol())); + } + + /// @notice Round the amount of assets, up or down, to a multiple of the exchange rate. That way the + /// associated number of VTokens won't have any decimals + /// @param assets The amount of assets to round + /// @param rounding If the round should be up (Rounding.Up), or down (Rounding.Down) + /// @return The rounded amount of assets + function _roundAssets(uint256 assets, Rounding rounding) internal view returns (uint256) { + uint256 exchangeRate = vToken.exchangeRateStored(); + uint256 redeemTokens = (assets * EXP_SCALE) / exchangeRate; + + uint256 _redeemAmount = (redeemTokens * exchangeRate) / EXP_SCALE; + + if (_redeemAmount != 0 && _redeemAmount != assets && rounding == Rounding.Up) redeemTokens++; // round up + + // redeemAmount = exchangeRate * redeemTokens + return (exchangeRate * redeemTokens) / EXP_SCALE; + } +} diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol new file mode 100644 index 00000000..bf1c8cc1 --- /dev/null +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.25; + +import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +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 { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.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 { Action } from "./Interfaces/IComptroller.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 VenusERC4626Isolated is + ERC4626Upgradeable, + AccessControlledV8, + MaxLoopsLimitHelper, + ReentrancyGuardUpgradeable +{ + using SafeERC20Upgradeable for ERC20Upgradeable; + + enum Rounding { + Down, + Up + } + + /// @notice Error code representing no errors in Venus operations. + uint256 internal constant NO_ERROR = 0; + + /// @notice The Venus vToken associated with this ERC4626 vault. + VToken public vToken; + + /// @notice The Venus Comptroller contract, responsible for market operations. + IComptroller public comptroller; + + /// @notice The recipient of rewards distributed by the Venus Protocol. + address public rewardRecipient; + + /// @notice Emitted when rewards are claimed. + /// @param amount The amount of reward tokens claimed. + /// @param rewardToken The address of the reward token claimed. + event ClaimRewards(uint256 amount, address indexed rewardToken); + + /// @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 Event emitted when tokens are swept + event SweepToken(address indexed token, address indexed receiver, uint256 amount); + + /// @notice Thrown when a Venus protocol call returns an error. + /// @dev This error is triggered if a Venus operation (such as minting or redeeming vTokens) fails. + /// @param errorCode The error code returned by the Venus protocol. + error VenusERC4626__VenusError(uint256 errorCode); + + /// @notice Thrown when a deposit exceeds the maximum allowed limit. + /// @dev This error is triggered if the deposit amount is greater than `maxDeposit(receiver)`. + error ERC4626__DepositMoreThanMax(); + + /// @notice Thrown when a mint operation exceeds the maximum allowed limit. + /// @dev This error is triggered if the mint amount is greater than `maxMint(receiver)`. + error ERC4626__MintMoreThanMax(); + + /// @notice Thrown when a withdrawal exceeds the maximum available assets. + /// @dev This error is triggered if the withdrawal amount is greater than `maxWithdraw(owner)`. + error ERC4626__WithdrawMoreThanMax(); + + /// @notice Thrown when a redemption exceeds the maximum redeemable shares. + /// @dev This error is triggered if the redemption amount is greater than `maxRedeem(owner)`. + error ERC4626__RedeemMoreThanMax(); + + /// @notice Thrown when attempting an operation with a zero amount. + /// @dev This error prevents unnecessary transactions with zero amounts in deposit, withdraw, mint, or redeem operations. + /// @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 { + _checkAccessAllowed("setRewardRecipient(address)"); + _setRewardRecipient(newRecipient); + } + + /// @notice Sweeps the input token address tokens from the contract and sends them to the owner + /// @param token Address of the token + /// @custom:event SweepToken emits on success + /// @custom:access Only owner + function sweepToken(IERC20Upgradeable token) external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + + if (balance > 0) { + address owner_ = owner(); + SafeERC20Upgradeable.safeTransfer(token, owner_, balance); + emit SweepToken(address(token), owner_, balance); + } + } + + /// @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 invoked 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_); + } + + /// @inheritdoc ERC4626Upgradeable + function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + + vToken.accrueInterest(); + if (assets == 0) { + revert ERC4626__ZeroAmount("deposit"); + } + if (assets > maxDeposit(receiver)) { + revert ERC4626__DepositMoreThanMax(); + } + + uint256 shares = previewDeposit(assets); + if (shares == 0) { + revert ERC4626__ZeroAmount("deposit"); + } + _deposit(_msgSender(), receiver, assets, shares); + afterDeposit(assets); + return shares; + } + + /// @inheritdoc ERC4626Upgradeable + function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + + vToken.accrueInterest(); + if (shares == 0) { + revert ERC4626__ZeroAmount("mint"); + } + if (shares > maxMint(receiver)) { + revert ERC4626__MintMoreThanMax(); + } + uint256 assets = previewMint(shares); + if (assets == 0) { + revert ERC4626__ZeroAmount("mint"); + } + _deposit(_msgSender(), receiver, assets, shares); + afterDeposit(assets); + return assets; + } + + /// @inheritdoc ERC4626Upgradeable + function withdraw(uint256 assets, address receiver, address owner) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + ensureNonzeroAddress(owner); + + vToken.accrueInterest(); + if (assets == 0) { + revert ERC4626__ZeroAmount("withdraw"); + } + if (assets > maxWithdraw(owner)) { + revert ERC4626__WithdrawMoreThanMax(); + } + + uint256 shares = previewWithdraw(assets); + if (shares == 0) { + revert ERC4626__ZeroAmount("withdraw"); + } + uint256 actualAssets = beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, shares); + return shares; + } + + /// @inheritdoc ERC4626Upgradeable + function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + ensureNonzeroAddress(owner); + + vToken.accrueInterest(); + if (shares == 0) { + revert ERC4626__ZeroAmount("redeem"); + } + if (shares > maxRedeem(owner)) { + revert ERC4626__RedeemMoreThanMax(); + } + + uint256 assets = previewRedeem(shares); + if (assets == 0) { + revert ERC4626__ZeroAmount("redeem"); + } + + // actualAssets should be equal to assets, because of the round performed in previewRedeem + // but let's use the returned value anyway + uint256 actualAssets = beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, shares); + return actualAssets; + } + + /// @inheritdoc ERC4626Upgradeable + function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + // round down the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals + uint256 adjustedAssets = _roundAssets(assets, Rounding.Down); + + return super.previewDeposit(adjustedAssets); + } + + /// @inheritdoc ERC4626Upgradeable + function previewMint(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewMint(shares); + + // round up the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals + return _roundAssets(assets, Rounding.Up); + } + + /// @inheritdoc ERC4626Upgradeable + function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + // round up the asset amount to redeem a natural number of vTokens, without decimals + uint256 adjustedAssets = _roundAssets(assets, Rounding.Up); + + return super.previewWithdraw(adjustedAssets); + } + + /// @inheritdoc ERC4626Upgradeable + function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewRedeem(shares); + + // round down the asset amount to redeem a natural number of vTokens, without decimals + return _roundAssets(assets, Rounding.Down); + } + + /// @notice Returns the total amount of assets deposited + /// @return Amount of assets deposited + function totalAssets() public view virtual override returns (uint256) { + return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; + } + + /// @notice Returns the maximum deposit allowed based on Venus supply caps. + /// @dev If minting is paused or the supply cap is reached, returns 0. + /// @param /*account*/ The address of the account. + /// @return The maximum amount of assets that can be deposited. + function maxDeposit(address /*account*/) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.MINT)) { + return 0; + } + + uint256 supplyCap = comptroller.supplyCaps(address(vToken)); + uint256 totalSupply_ = (vToken.totalSupply() * vToken.exchangeRateStored()) / EXP_SCALE; + return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; + } + + /// @notice Returns the maximum amount of shares that can be minted. + /// @dev This is derived from the maximum deposit amount converted to shares. + /// @param /*account*/ The address of the account. + /// @return The maximum number of shares that can be minted. + function maxMint(address /*account*/) public view virtual override returns (uint256) { + return convertToShares(maxDeposit(address(0))); + } + + /// @notice Returns the maximum amount that can be withdrawn. + /// @dev The withdrawable amount is limited by the available cash in the vault. + /// @param receiver The address of the account withdrawing. + /// @return The maximum amount of assets that can be withdrawn. + function maxWithdraw(address receiver) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { + return 0; + } + + uint256 cash = vToken.getCash(); + uint256 totalReserves = vToken.totalReserves(); + uint256 assetsBalance = convertToAssets(balanceOf(receiver)); + + if (cash < totalReserves) { + return 0; + } else { + uint256 availableCash = cash - totalReserves; + return availableCash < assetsBalance ? availableCash : assetsBalance; + } + } + + /// @notice Returns the maximum amount of shares that can be redeemed. + /// @dev Redemption is limited by the available cash in the vault. + /// @param receiver The address of the account redeeming. + /// @return The maximum number of shares that can be redeemed. + function maxRedeem(address receiver) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { + return 0; + } + + uint256 cash = vToken.getCash(); + uint256 totalReserves = vToken.totalReserves(); + if (cash < totalReserves) { + return 0; + } else { + uint256 availableCash = cash - totalReserves; + uint256 availableCashInShares = convertToShares(availableCash); + uint256 shareBalance = balanceOf(receiver); + return availableCashInShares < shareBalance ? availableCashInShares : shareBalance; + } + } + + /// @notice Redeems underlying assets before withdrawing from the vault. + /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. + /// @param assets The amount of underlying assets to redeem. + /// @return The amount of assets transferred in + function beforeWithdraw(uint256 assets) internal returns (uint256) { + IERC20Upgradeable token = IERC20Upgradeable(asset()); + uint256 balanceBefore = token.balanceOf(address(this)); + + uint256 errorCode = vToken.redeemUnderlying(assets); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + + uint256 balanceAfter = token.balanceOf(address(this)); + // Return the amount that was *actually* transferred + return balanceAfter - balanceBefore; + } + + /// @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 afterDeposit(uint256 assets) internal { + ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); + uint256 errorCode = vToken.mint(assets); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + } + + /// @notice Sets a new reward recipient address + /// @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 { + ensureNonzeroAddress(newRecipient); + + emit RewardRecipientUpdated(rewardRecipient, newRecipient); + rewardRecipient = newRecipient; + } + + /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. + /// @return Gap between 18 and the decimals of the asset token + function _decimalsOffset() internal view virtual override returns (uint8) { + return 18 - ERC20Upgradeable(asset()).decimals(); + } + + /// @notice Generates an 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) { + 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) { + return string(abi.encodePacked("v4626", asset_.symbol())); + } + + /// @notice Round the amount of assets, up or down, to a multiple of the exchange rate. That way the + /// associated number of VTokens won't have any decimals + /// @param assets The amount of assets to round + /// @param rounding If the round should be up (Rounding.Up), or down (Rounding.Down) + /// @return The rounded amount of assets + function _roundAssets(uint256 assets, Rounding rounding) internal view returns (uint256) { + uint256 exchangeRate = vToken.exchangeRateStored(); + uint256 redeemTokens = (assets * EXP_SCALE) / exchangeRate; + + uint256 _redeemAmount = (redeemTokens * exchangeRate) / EXP_SCALE; + + if (_redeemAmount != 0 && _redeemAmount != assets && rounding == Rounding.Up) redeemTokens++; // round up + + // redeemAmount = exchangeRate * redeemTokens + return (exchangeRate * redeemTokens) / EXP_SCALE; + } +} From b71d4ad53ef45d3a812beadd3b7ffd8ed59823a3 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 10 Jun 2025 18:38:49 +0530 Subject: [PATCH 03/41] feat: update interface --- contracts/ERC4626/Interfaces/IComptroller.sol | 6 +++++ .../ERC4626/Interfaces/VTokenInterface.sol | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 contracts/ERC4626/Interfaces/VTokenInterface.sol diff --git a/contracts/ERC4626/Interfaces/IComptroller.sol b/contracts/ERC4626/Interfaces/IComptroller.sol index a906440a..b91fe047 100644 --- a/contracts/ERC4626/Interfaces/IComptroller.sol +++ b/contracts/ERC4626/Interfaces/IComptroller.sol @@ -10,9 +10,15 @@ 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) 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, uint); + + function getXVSAddress() external view returns (address); } diff --git a/contracts/ERC4626/Interfaces/VTokenInterface.sol b/contracts/ERC4626/Interfaces/VTokenInterface.sol new file mode 100644 index 00000000..e7d9bda0 --- /dev/null +++ b/contracts/ERC4626/Interfaces/VTokenInterface.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.25; + +import { IComptroller } from "./IComptroller.sol"; + +abstract contract VTokenInterface { + function mint(uint mintAmount) external virtual returns (uint); + + function redeem(uint redeemTokens) external virtual returns (uint); + + function redeemUnderlying(uint redeemAmount) external virtual returns (uint); + + function balanceOf(address owner) external view virtual returns (uint); + + function comptroller() external view virtual returns (IComptroller); + + function totalSupply() external view virtual returns (uint); + + function underlying() external view virtual returns (address); + + function getCash() external view virtual returns (uint); + + function exchangeRateStored() public view virtual returns (uint); + + function accrueInterest() public view virtual returns (uint); + + function totalReserves() public view virtual returns (uint); +} From 317d77ad16f7ff5101741976047d966ea8b23bc7 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 13:10:01 +0530 Subject: [PATCH 04/41] refactor: rename comptroller interface --- contracts/ERC4626/Interfaces/IComptroller.sol | 2 +- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/ERC4626/Interfaces/IComptroller.sol b/contracts/ERC4626/Interfaces/IComptroller.sol index b91fe047..5bbcedf3 100644 --- a/contracts/ERC4626/Interfaces/IComptroller.sol +++ b/contracts/ERC4626/Interfaces/IComptroller.sol @@ -18,7 +18,7 @@ interface IComptroller { function supplyCaps(address) external view returns (uint256); - function markets(address) external view returns (bool, uint); + function markets(address) external view returns (bool, uint256); function getXVSAddress() external view returns (address); } diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 40035fcf..3ae36272 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -5,7 +5,7 @@ import { ethers, upgrades } from "hardhat"; import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; import { - ComptrollerInterface, + IComptroller, VToken as CoreVToken, ERC20, VenusERC4626Factory, @@ -31,7 +31,7 @@ describe("VenusERC4626Factory", () => { let coreVToken: FakeContract; let isolatedVToken: FakeContract; let invalidVToken: FakeContract; - let coreComptroller: FakeContract; + let coreComptroller: FakeContract; let poolRegistry: FakeContract; let accessControl: FakeContract; let rewardRecipient: string; @@ -51,8 +51,8 @@ describe("VenusERC4626Factory", () => { rewardRecipient = deployer.address; // Setup core pool - coreComptroller = await smock.fake( - "contracts/interfaces/ComptrollerInterface.sol:ComptrollerInterface", + coreComptroller = await smock.fake( + "contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller", ); coreVToken.comptroller.returns(coreComptroller.address); coreVToken.underlying.returns(asset1.address); From 0790ebf8f665b2b7f6b49f71b9b667eccc30a722 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 13:29:01 +0530 Subject: [PATCH 05/41] fix: lint --- README.md | 1 + helpers/rateModelHelpers.ts | 114 ------------------- helpers/writeFile.ts | 8 -- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 8 +- tests/hardhat/Fork/constants.ts | 17 ++- 5 files changed, 12 insertions(+), 136 deletions(-) delete mode 100644 helpers/rateModelHelpers.ts delete mode 100644 helpers/writeFile.ts diff --git a/README.md b/README.md index b77a8201..88bb89be 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Venus Protocol introduces native ERC-4626 vaults, bringing standardized, composable yield vaults to the Venus ecosystem. This integration represents a significant advancement in making Venus's yield-bearing markets more accessible and composable within the broader DeFi ecosystem. ## Understanding ERC-4626 + ERC-4626 is a tokenized vault standard designed to unify how yield-bearing assets are deposited, managed, and withdrawn in DeFi protocols. It builds on the ERC-20 token standard and introduces a consistent interface for vaults that accept a specific asset (like USDC) and issue shares representing ownership in the vault. The primary goal of ERC-4626 is standardization—allowing developers to integrate with vaults without needing to understand their internal mechanics. Functions like deposit, withdraw, mint, and redeem, follow predictable behaviors across all compliant contracts. diff --git a/helpers/rateModelHelpers.ts b/helpers/rateModelHelpers.ts deleted file mode 100644 index 8fc2a7be..00000000 --- a/helpers/rateModelHelpers.ts +++ /dev/null @@ -1,114 +0,0 @@ -// These helpers convert the existing configs to stronger typed ones -// and provide a naming scheme for rate model contracts -import { BigNumber } from "ethers"; -import { parseUnits } from "ethers/lib/utils"; - -import { DeploymentInfo, InterestRateModels, VTokenConfig } from "./deploymentConfig"; - -export const mantissaToBps = (num: BigNumber) => { - return BigNumber.from(num).div(parseUnits("1", 14)).toString(); -}; - -export type TimeManagerParams = Pick; - -export type WpRateModelParams = { - model: InterestRateModels.WhitePaper; - baseRatePerYear: BigNumber; - multiplierPerYear: BigNumber; -}; - -export type JumpRateModelParams = { - model: InterestRateModels.JumpRate; - baseRatePerYear: BigNumber; - multiplierPerYear: BigNumber; - jumpMultiplierPerYear: BigNumber; - kink: BigNumber; -}; - -export type TwoKinksRateModelParams = { - model: InterestRateModels.TwoKinks; - baseRatePerYear: BigNumber; - multiplierPerYear: BigNumber; - kink: BigNumber; - baseRatePerYear2: BigNumber; - multiplierPerYear2: BigNumber; - kink2: BigNumber; - jumpMultiplierPerYear: BigNumber; -}; - -export type RateModelParams = WpRateModelParams | JumpRateModelParams | TwoKinksRateModelParams; - -export const getRateModelParams = (config: VTokenConfig): RateModelParams => { - if (config.rateModel === InterestRateModels.WhitePaper.toString()) { - const { baseRatePerYear, multiplierPerYear } = config; - return { - model: InterestRateModels.WhitePaper, - baseRatePerYear: BigNumber.from(baseRatePerYear), - multiplierPerYear: BigNumber.from(multiplierPerYear), - }; - } else if (config.rateModel === InterestRateModels.JumpRate.toString()) { - const { baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_ } = config; - return { - model: InterestRateModels.JumpRate, - baseRatePerYear: BigNumber.from(baseRatePerYear), - multiplierPerYear: BigNumber.from(multiplierPerYear), - jumpMultiplierPerYear: BigNumber.from(jumpMultiplierPerYear), - kink: BigNumber.from(kink_), - }; - } else if (config.rateModel === InterestRateModels.TwoKinks.toString()) { - const { - baseRatePerYear, - multiplierPerYear, - jumpMultiplierPerYear, - kink_, - kink2_, - multiplierPerYear2, - baseRatePerYear2, - } = config; - return { - model: InterestRateModels.TwoKinks, - baseRatePerYear: BigNumber.from(baseRatePerYear), - multiplierPerYear: BigNumber.from(multiplierPerYear), - kink: BigNumber.from(kink_), - baseRatePerYear2: BigNumber.from(baseRatePerYear2), - multiplierPerYear2: BigNumber.from(multiplierPerYear2), - kink2: BigNumber.from(kink2_), - jumpMultiplierPerYear: BigNumber.from(jumpMultiplierPerYear), - }; - } - throw new Error(`Unsupported rate model ${config.rateModel}`); -}; - -export const getTimeManagerSuffix = (params: TimeManagerParams) => { - if (params.isTimeBased) { - return "timeBased"; - } - return `bpy${params.blocksPerYear}`; -}; - -export const getRateModelName = (params: RateModelParams, timeManagerParams: TimeManagerParams): string => { - const suffix = getTimeManagerSuffix(timeManagerParams); - switch (params.model) { - case InterestRateModels.WhitePaper: { - const [b, m] = [params.baseRatePerYear, params.multiplierPerYear].map(mantissaToBps); - return `WhitePaperInterestRateModel_base${b}bps_slope${m}bps_${suffix}`; - } - case InterestRateModels.JumpRate: { - const { baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink } = params; - const [b, m, j, k] = [baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink].map(mantissaToBps); - return `JumpRateModelV2_base${b}bps_slope${m}bps_jump${j}bps_kink${k}bps_${suffix}`; - } - case InterestRateModels.TwoKinks: { - const [b, m, k, m2, b2, k2, j] = [ - params.baseRatePerYear, - params.multiplierPerYear, - params.kink, - params.multiplierPerYear2, - params.baseRatePerYear2, - params.kink2, - params.jumpMultiplierPerYear, - ].map(mantissaToBps); - return `TwoKinks_base${b}bps_slope${m}bps_kink${k}bps_slope2${m2}bps_base2${b2}bps_kink2${k2}bps_jump${j}bps_${suffix}`; - } - } -}; diff --git a/helpers/writeFile.ts b/helpers/writeFile.ts deleted file mode 100644 index ec428b9e..00000000 --- a/helpers/writeFile.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { mkdirSync, writeFileSync } from "fs"; - -const GENERATED_CONTRACTS_PATH = `${__dirname}/../contracts/generated`; - -export const writeGeneratedContract = (fileName: string, content: string) => { - mkdirSync(GENERATED_CONTRACTS_PATH, { recursive: true }); - writeFileSync(`${GENERATED_CONTRACTS_PATH}/${fileName}`, content, "utf-8"); -}; diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 3ae36272..2b81f5d4 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -5,15 +5,15 @@ import { ethers, upgrades } from "hardhat"; import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; import { - IComptroller, VToken as CoreVToken, ERC20, - VenusERC4626Factory, IAccessControlManagerV8, + IComptroller, VToken as IsolatedVToken, PoolRegistryInterface, UpgradeableBeacon, VenusERC4626Core, + VenusERC4626Factory, VenusERC4626Isolated, } from "../../typechain"; @@ -51,9 +51,7 @@ describe("VenusERC4626Factory", () => { rewardRecipient = deployer.address; // Setup core pool - coreComptroller = await smock.fake( - "contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller", - ); + 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]); diff --git a/tests/hardhat/Fork/constants.ts b/tests/hardhat/Fork/constants.ts index 59a2908e..84741c2e 100644 --- a/tests/hardhat/Fork/constants.ts +++ b/tests/hardhat/Fork/constants.ts @@ -6,6 +6,14 @@ import GovernanceEthMainnet from "@venusprotocol/governance-contracts/deployment import GovernanceOpBnbMainnet from "@venusprotocol/governance-contracts/deployments/opbnbmainnet.json"; import GovernanceOpBnbTestnet from "@venusprotocol/governance-contracts/deployments/opbnbtestnet.json"; import GovernanceSepTestnet from "@venusprotocol/governance-contracts/deployments/sepolia.json"; +import { contracts as ArbOneContracts } from "@venusprotocol/isolated-pools/deployments/arbitrumone.json"; +import { contracts as ArbSepContracts } from "@venusprotocol/isolated-pools/deployments/arbitrumsepolia.json"; +import { contracts as MainnetContracts } from "@venusprotocol/isolated-pools/deployments/bscmainnet.json"; +import { contracts as TestnetContracts } from "@venusprotocol/isolated-pools/deployments/bsctestnet.json"; +import { contracts as EthereumContracts } from "@venusprotocol/isolated-pools/deployments/ethereum.json"; +import { contracts as OpBnbMainnetContracts } from "@venusprotocol/isolated-pools/deployments/opbnbmainnet.json"; +import { contracts as OpBnbTestnetContracts } from "@venusprotocol/isolated-pools/deployments/opbnbtestnet.json"; +import { contracts as SepoliaContracts } from "@venusprotocol/isolated-pools/deployments/sepolia.json"; import OracleArbOne from "@venusprotocol/oracle/deployments/arbitrumone.json"; import OracleArbSep from "@venusprotocol/oracle/deployments/arbitrumsepolia.json"; import OracleBscMainnet from "@venusprotocol/oracle/deployments/bscmainnet.json"; @@ -22,15 +30,6 @@ import PsrEthereum from "@venusprotocol/protocol-reserve/deployments/ethereum.js import PsrOpBnbTestnet from "@venusprotocol/protocol-reserve/deployments/opbnbtestnet/ProtocolShareReserve.json"; import PsrSepTestnet from "@venusprotocol/protocol-reserve/deployments/sepolia.json"; -import { contracts as ArbOneContracts } from "@venusprotocol/isolated-pools/deployments/arbitrumone.json"; -import { contracts as ArbSepContracts } from "@venusprotocol/isolated-pools/deployments/arbitrumsepolia.json"; -import { contracts as MainnetContracts } from "@venusprotocol/isolated-pools/deployments/bscmainnet.json"; -import { contracts as TestnetContracts } from "@venusprotocol/isolated-pools/deployments/bsctestnet.json"; -import { contracts as EthereumContracts } from "@venusprotocol/isolated-pools/deployments/ethereum.json"; -import { contracts as OpBnbMainnetContracts } from "@venusprotocol/isolated-pools/deployments/opbnbmainnet.json"; -import { contracts as OpBnbTestnetContracts } from "@venusprotocol/isolated-pools/deployments/opbnbtestnet.json"; -import { contracts as SepoliaContracts } from "@venusprotocol/isolated-pools/deployments/sepolia.json"; - export const contractAddresses = { sepolia: { ADMIN: "0x94fa6078b6b8a26F0B6EDFFBE6501B22A10470fB", From 39671403e2801624a421b0e81fda962c3cd64dfa Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 15:33:35 +0530 Subject: [PATCH 06/41] refactor: removing unused functions --- helpers/deploymentUtils.ts | 103 -------------------------------- tests/hardhat/Fork/constants.ts | 6 -- 2 files changed, 109 deletions(-) diff --git a/helpers/deploymentUtils.ts b/helpers/deploymentUtils.ts index 9acff4c0..36d84c8d 100644 --- a/helpers/deploymentUtils.ts +++ b/helpers/deploymentUtils.ts @@ -1,15 +1,9 @@ import { deployments, ethers, getNamedAccounts } from "hardhat"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { Comptroller, ERC20, MockToken } from "../typechain"; import { DeploymentInfo, - PoolConfig, - RewardConfig, - TokenConfig, - VTokenConfig, blocksPerYear, - getTokenConfig, } from "./deploymentConfig"; export const toAddress = async (addressOrAlias: string): Promise => { @@ -24,103 +18,6 @@ export const toAddress = async (addressOrAlias: string): Promise => { return deployment.address; }; -export const getUnderlyingMock = async (assetSymbol: string): Promise => { - return ethers.getContract(`Mock${assetSymbol}`); -}; - -export const getUnderlyingToken = async (assetSymbol: string, tokensConfig: TokenConfig[]): Promise => { - const token = getTokenConfig(assetSymbol, tokensConfig); - let underlyingAddress = token.tokenAddress; - if (token.isMock) { - underlyingAddress = (await getUnderlyingMock(assetSymbol)).address; - } - return ethers.getContractAt("@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20", underlyingAddress); -}; - -export const getUnregisteredPools = async (poolConfig: PoolConfig[]): Promise => { - const registry = await ethers.getContract("PoolRegistry"); - const registeredPools = (await registry.getAllPools()).map((p: { comptroller: string }) => p.comptroller); - const isRegistered = await Promise.all( - poolConfig.map(async pool => { - const comptroller = await deployments.getOrNull(`Comptroller_${pool.name}`); - if (!comptroller) { - // If the Comptroller deployment doesn't exist, it's not registered - return false; - } - return registeredPools.includes(comptroller.address); - }), - ); - return poolConfig.filter((_, idx: number) => !isRegistered[idx]); -}; - -export const getUnregisteredVTokens = async (poolConfig: PoolConfig[]): Promise => { - const registry = await ethers.getContract("PoolRegistry"); - const registeredPools = await registry.getAllPools(); - const comptrollers = await Promise.all( - registeredPools.map(async (p: { comptroller: string }) => { - return ethers.getContractAt("Comptroller", p.comptroller); - }), - ); - const registeredVTokens = ( - await Promise.all( - comptrollers.map(async (comptroller: Comptroller) => { - return comptroller.getAllMarkets(); - }), - ) - ).flat(); - - return Promise.all( - poolConfig.map(async (pool: PoolConfig) => { - const isRegistered = await Promise.all( - pool.vtokens.map(async (vTokenConfig: VTokenConfig) => { - const vToken = await deployments.getOrNull(`VToken_${vTokenConfig.name}`); - if (!vToken) { - // If the VToken deployment doesn't exist, it's not registered - return false; - } - return registeredVTokens.includes(vToken.address); - }), - ); - return { ...pool, vtokens: pool.vtokens.filter((_, idx: number) => !isRegistered[idx]) }; - }), - ); -}; - -export const getUnregisteredRewardsDistributors = async (poolConfig: PoolConfig[]): Promise => { - const registry = await ethers.getContract("PoolRegistry"); - const registeredPools = await registry.getAllPools(); - const comptrollers = await Promise.all( - registeredPools.map(async (p: { comptroller: string }) => { - return ethers.getContractAt("Comptroller", p.comptroller); - }), - ); - - const registeredRewardDistributors = ( - await Promise.all( - comptrollers.map(async (comptroller: Comptroller) => { - return comptroller.getRewardDistributors(); - }), - ) - ).flat(); - - return Promise.all( - poolConfig.map(async (pool: PoolConfig) => { - const rewards = pool.rewards || []; - const isRegistered = await Promise.all( - rewards.map(async (reward: RewardConfig) => { - const rewardsDistributor = await deployments.getOrNull(`RewardsDistributor_${reward.asset}_${pool.name}`); - if (!rewardsDistributor) { - // If the RewardsDistributor deployment doesn't exist, it's not registered - return false; - } - return registeredRewardDistributors.includes(rewardsDistributor.address); - }), - ); - return { ...pool, rewards: rewards.filter((_, idx: number) => !isRegistered[idx]) }; - }), - ); -}; - export const getBlockOrTimestampBasedDeploymentInfo = (network: string): DeploymentInfo => { const blocksPerYear_ = blocksPerYear[network]; if (blocksPerYear_ === "time-based") { diff --git a/tests/hardhat/Fork/constants.ts b/tests/hardhat/Fork/constants.ts index 84741c2e..8fcbbdc0 100644 --- a/tests/hardhat/Fork/constants.ts +++ b/tests/hardhat/Fork/constants.ts @@ -75,14 +75,9 @@ export const contractAddresses = { ADMIN: GovernanceBscTestnet.contracts.NormalTimelock.address, ACM: GovernanceBscTestnet.contracts.AccessControlManager.address, TOKEN1: TestnetContracts.MockUSDD.address, - TOKEN2: TestnetContracts.MocklisUSD.address, - VTOKEN1: TestnetContracts.VToken_vUSDD_Stablecoins.address, - VTOKEN2: TestnetContracts.VToken_vlisUSD_Stablecoins.address, - COMPTROLLER: TestnetContracts.Comptroller_Stablecoins.address, PSR: PsrBscTestnet.contracts.ProtocolShareReserve.address, SHORTFALL: TestnetContracts.Shortfall.address, RISKFUND: PsrBscTestnet.contracts.RiskFundV2.address, - REWARD_DISTRIBUTOR1: TestnetContracts.RewardsDistributor_Stablecoins_0.address, POOL_REGISTRY: TestnetContracts.PoolRegistry.address, RESILIENT_ORACLE: OracleBscTestnet.contracts.ResilientOracle.address, CHAINLINK_ORACLE: OracleBscTestnet.contracts.ChainlinkOracle.address, @@ -101,7 +96,6 @@ export const contractAddresses = { ADMIN: GovernanceBscMainnet.contracts.NormalTimelock.address, ACM: GovernanceBscMainnet.contracts.AccessControlManager.address, VTOKEN1: MainnetContracts.VToken_vUSDD_Stablecoins.address, - VTOKEN2: MainnetContracts.VToken_vlisUSD_Stablecoins.address, COMPTROLLER: MainnetContracts.Comptroller_Stablecoins.address, PSR: PsrBscMainnet.contracts.ProtocolShareReserve.address, SHORTFALL: MainnetContracts.Shortfall.address, From b9d7bb98c80226a63953aeadea0eb6f3c1de7f97 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 15:45:08 +0530 Subject: [PATCH 07/41] fix: lint --- helpers/deploymentUtils.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/helpers/deploymentUtils.ts b/helpers/deploymentUtils.ts index 36d84c8d..3f7e1100 100644 --- a/helpers/deploymentUtils.ts +++ b/helpers/deploymentUtils.ts @@ -1,10 +1,7 @@ -import { deployments, ethers, getNamedAccounts } from "hardhat"; +import { deployments, getNamedAccounts } from "hardhat"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { - DeploymentInfo, - blocksPerYear, -} from "./deploymentConfig"; +import { DeploymentInfo, blocksPerYear } from "./deploymentConfig"; export const toAddress = async (addressOrAlias: string): Promise => { if (addressOrAlias.startsWith("0x")) { From a65034455a804222682d561a8ac0bf21a670901c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 16:37:50 +0530 Subject: [PATCH 08/41] feat: update deployment script for factory --- deploy/020-deploy-VenusERC4626Factory.ts | 18 +++++++++++++++--- helpers/deploymentConfig.ts | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/deploy/020-deploy-VenusERC4626Factory.ts b/deploy/020-deploy-VenusERC4626Factory.ts index e28c2f9c..6dfef9ef 100644 --- a/deploy/020-deploy-VenusERC4626Factory.ts +++ b/deploy/020-deploy-VenusERC4626Factory.ts @@ -16,6 +16,7 @@ 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"); @@ -25,8 +26,17 @@ 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, + autoMine: true, + skipIfAlreadyDeployed: true, + }); + + const CoreImplementation: DeployResult = await deploy("CoreImplementation", { + contract: "VenusERC4626Core", from: deployer, args: [], log: true, @@ -46,9 +56,11 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { methodName: "initialize", args: [ accessControlManagerAddress, + IsolatedImplementation.address, + CoreImplementation.address, poolRegistryAddress, + coreComptroller, rewardRecipientAddress, - venusERC4626Implementation.address, loopsLimit, ], }, diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index d019929f..a8500892 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -21,6 +21,7 @@ import { contracts as venusProtocolSepolia } from "@venusprotocol/venus-protocol import { contracts as venusProtocolZkSyncMainnet } from "@venusprotocol/venus-protocol/deployments/zksyncmainnet.json"; import { contracts as venusProtocolZkSyncSepolia } from "@venusprotocol/venus-protocol/deployments/zksyncsepolia.json"; import { BigNumber } from "ethers"; +import { Wallet } from "ethers"; import { ethers } from "hardhat"; import { DeploymentsExtension } from "hardhat-deploy/types"; @@ -208,6 +209,9 @@ const REDUCE_RESERVES_BLOCK_DELTA_BERA_CHAIN_BARTIO = "86400"; export const preconfiguredAddresses = { hardhat: { VTreasury: "account:deployer", + AccessControlManager: Wallet.createRandom().address, + PoolRegistry: Wallet.createRandom().address, + CoreComptroller: Wallet.createRandom().address, }, bsctestnet: { VTreasury: venusProtocolBscTestnet.VTreasury.address, From 33c6f565bd0406920d59946d949d8e76cbf88600 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 19:29:11 +0530 Subject: [PATCH 09/41] feat: updated VenusERC4626 contracts for Core and Isolated --- contracts/ERC4626/VenusERC4626Core.sol | 181 ++++++++++++--------- contracts/ERC4626/VenusERC4626Isolated.sol | 180 +++++++++++--------- 2 files changed, 206 insertions(+), 155 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index e1363b59..97b62f5e 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -7,28 +7,24 @@ import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC 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 { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; import { IComptroller, Action } from "./Interfaces/IComptroller.sol"; import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; +uint256 constant EXP_SCALE = 1e18; + /// @title VenusERC4626 /// @notice ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable { + using MathUpgradeable for uint256; using SafeERC20Upgradeable for ERC20Upgradeable; - enum Rounding { - Down, - Up - } - /// @notice Error code representing no errors in Venus operations. uint256 internal constant NO_ERROR = 0; - /// @notice Base unit for computations, usually used in scaling (multiplications, divisions) - uint256 internal constant EXP_SCALE = 1e18; - /// @notice The Venus vToken associated with this ERC4626 vault. VTokenInterface public vToken; @@ -174,11 +170,16 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG if (shares == 0) { revert ERC4626__ZeroAmount("deposit"); } + + uint256 totalSupplyBefore = totalSupply(); _deposit(_msgSender(), receiver, assets, shares); - afterDeposit(assets); - return shares; + uint256 actualShares = totalSupply() - totalSupplyBefore; + + return actualShares; } + /// @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) { ensureNonzeroAddress(receiver); @@ -196,10 +197,11 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG revert ERC4626__ZeroAmount("mint"); } _deposit(_msgSender(), receiver, assets, shares); - afterDeposit(assets); return assets; } + /// @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) { ensureNonzeroAddress(receiver); @@ -213,13 +215,10 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG revert ERC4626__WithdrawMoreThanMax(); } - uint256 shares = previewWithdraw(assets); - if (shares == 0) { - revert ERC4626__ZeroAmount("withdraw"); - } - uint256 actualAssets = beforeWithdraw(assets); - _withdraw(_msgSender(), receiver, owner, actualAssets, shares); - return shares; + (uint256 actualAssets, uint256 actualShares) = _beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, actualShares); + + return actualShares; } /// @inheritdoc ERC4626Upgradeable @@ -235,50 +234,15 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG revert ERC4626__RedeemMoreThanMax(); } - uint256 assets = previewRedeem(shares); - if (assets == 0) { + uint256 actualAssets = _beforeRedeem(shares); + if (actualAssets == 0) { revert ERC4626__ZeroAmount("redeem"); } - // actualAssets should be equal to assets, because of the round performed in previewRedeem - // but let's use the returned value anyway - uint256 actualAssets = beforeWithdraw(assets); _withdraw(_msgSender(), receiver, owner, actualAssets, shares); return actualAssets; } - /// @inheritdoc ERC4626Upgradeable - function previewDeposit(uint256 assets) public view virtual override returns (uint256) { - // round down the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals - uint256 adjustedAssets = _roundAssets(assets, Rounding.Down); - - return super.previewDeposit(adjustedAssets); - } - - /// @inheritdoc ERC4626Upgradeable - function previewMint(uint256 shares) public view virtual override returns (uint256) { - uint256 assets = super.previewMint(shares); - - // round down the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals - return _roundAssets(assets, Rounding.Down); - } - - /// @inheritdoc ERC4626Upgradeable - function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { - // round up the asset amount to redeem a natural number of vTokens, without decimals - uint256 adjustedAssets = _roundAssets(assets, Rounding.Up); - - return super.previewWithdraw(adjustedAssets); - } - - /// @inheritdoc ERC4626Upgradeable - function previewRedeem(uint256 shares) public view virtual override returns (uint256) { - uint256 assets = super.previewRedeem(shares); - - // round down the asset amount to redeem a natural number of vTokens, without decimals - return _roundAssets(assets, Rounding.Down); - } - /// @notice Returns the total amount of assets deposited /// @return Amount of assets deposited function totalAssets() public view virtual override returns (uint256) { @@ -349,28 +313,67 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG } } + /// @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) { + IERC20Upgradeable token = IERC20Upgradeable(asset()); + uint256 balanceBefore = token.balanceOf(address(this)); + + // Calculate the amount of vTokens equivalent to the amount of shares, rounding it down + uint256 vTokens = shares.mulDiv( + vToken.balanceOf(address(this)), + totalSupply() + 10 ** _decimalsOffset(), + MathUpgradeable.Rounding.Down + ); + + uint256 errorCode = vToken.redeem(vTokens); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + + uint256 balanceAfter = token.balanceOf(address(this)); + + // Return the amount of assets that was *actually* transferred in + return balanceAfter - balanceBefore; + } + /// @notice Redeems underlying assets before withdrawing from the vault. /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. /// @param assets The amount of underlying assets to redeem. - /// @return The amount of assets transferred in - function beforeWithdraw(uint256 assets) internal returns (uint256) { + /// @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) { IERC20Upgradeable token = IERC20Upgradeable(asset()); uint256 balanceBefore = token.balanceOf(address(this)); + uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); uint256 errorCode = vToken.redeemUnderlying(assets); if (errorCode != NO_ERROR) { revert VenusERC4626__VenusError(errorCode); } - uint256 balanceAfter = token.balanceOf(address(this)); - // Return the amount that was *actually* transferred - return balanceAfter - balanceBefore; + // Return the amount of assets *actually* transferred in + actualAssets = token.balanceOf(address(this)) - balanceBefore; + + uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); + if (actualVTokens == 0) { + revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); + } + // Return the shares equivalent to the burned vTokens + actualShares = actualVTokens.mulDiv( + totalSupply() + 10 ** _decimalsOffset(), + vTokenBalanceBefore, + MathUpgradeable.Rounding.Up + ); } /// @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 afterDeposit(uint256 assets) internal { + function _mintVTokens(uint256 assets) internal { ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); uint256 errorCode = vToken.mint(assets); if (errorCode != NO_ERROR) { @@ -389,6 +392,45 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG rewardRecipient = newRecipient; } + /// @notice Deposits the assets into the VToken and calculates the shares to mint based on the + /// 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 { + // 1. Track pre-transfer balances + uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); + uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); + + // 2. Perform asset transfer (original OZ 4626 logic) + SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); + + // 3. Calculate actual assets received (protects against fee-on-transfer) + uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; + + // 4. Mint vTokens with received assets + _mintVTokens(assetsReceived); + + // 5. Verify actual vTokens received + uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; + if (vTokensReceived == 0) { + revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); + } + uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; + + // 6. Recalculate shares based on actual received value + // This is the same operation performed by previewDeposit, adjusting the total assets + uint256 actualShares = actualAssetsValue.mulDiv( + totalSupply() + 10 ** _decimalsOffset(), + totalAssets() + 1 - actualAssetsValue, // remove the new assets deposited to the VToken in this operation + MathUpgradeable.Rounding.Down + ); + + // 7. Mint the corrected share amount + _mint(receiver, actualShares); + + emit Deposit(caller, receiver, assets, actualShares); + } + /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. /// @return Gap between 18 and the decimals of the asset token function _decimalsOffset() internal view virtual override returns (uint8) { @@ -408,21 +450,4 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG function _generateVaultSymbol(ERC20Upgradeable asset_) internal view returns (string memory) { return string(abi.encodePacked("v4626", asset_.symbol())); } - - /// @notice Round the amount of assets, up or down, to a multiple of the exchange rate. That way the - /// associated number of VTokens won't have any decimals - /// @param assets The amount of assets to round - /// @param rounding If the round should be up (Rounding.Up), or down (Rounding.Down) - /// @return The rounded amount of assets - function _roundAssets(uint256 assets, Rounding rounding) internal view returns (uint256) { - uint256 exchangeRate = vToken.exchangeRateStored(); - uint256 redeemTokens = (assets * EXP_SCALE) / exchangeRate; - - uint256 _redeemAmount = (redeemTokens * exchangeRate) / EXP_SCALE; - - if (_redeemAmount != 0 && _redeemAmount != assets && rounding == Rounding.Up) redeemTokens++; // round up - - // redeemAmount = exchangeRate * redeemTokens - return (exchangeRate * redeemTokens) / EXP_SCALE; - } } diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index bf1c8cc1..172c1f26 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -6,6 +6,7 @@ import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; @@ -27,13 +28,9 @@ contract VenusERC4626Isolated is MaxLoopsLimitHelper, ReentrancyGuardUpgradeable { + using MathUpgradeable for uint256; using SafeERC20Upgradeable for ERC20Upgradeable; - enum Rounding { - Down, - Up - } - /// @notice Error code representing no errors in Venus operations. uint256 internal constant NO_ERROR = 0; @@ -147,7 +144,7 @@ contract VenusERC4626Isolated is VToken _vToken = vToken; address _rewardRecipient = rewardRecipient; - RewardsDistributor[] memory rewardDistributors = comptroller.getRewardDistributors(); + RewardsDistributor[] memory rewardDistributors = _comptroller.getRewardDistributors(); _ensureMaxLoops(rewardDistributors.length); @@ -212,11 +209,16 @@ contract VenusERC4626Isolated is if (shares == 0) { revert ERC4626__ZeroAmount("deposit"); } + + uint256 totalSupplyBefore = totalSupply(); _deposit(_msgSender(), receiver, assets, shares); - afterDeposit(assets); - return shares; + uint256 actualShares = totalSupply() - totalSupplyBefore; + + return actualShares; } + /// @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) { ensureNonzeroAddress(receiver); @@ -233,10 +235,11 @@ contract VenusERC4626Isolated is revert ERC4626__ZeroAmount("mint"); } _deposit(_msgSender(), receiver, assets, shares); - afterDeposit(assets); return assets; } + /// @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) { ensureNonzeroAddress(receiver); @@ -250,13 +253,10 @@ contract VenusERC4626Isolated is revert ERC4626__WithdrawMoreThanMax(); } - uint256 shares = previewWithdraw(assets); - if (shares == 0) { - revert ERC4626__ZeroAmount("withdraw"); - } - uint256 actualAssets = beforeWithdraw(assets); - _withdraw(_msgSender(), receiver, owner, actualAssets, shares); - return shares; + (uint256 actualAssets, uint256 actualShares) = _beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, actualShares); + + return actualShares; } /// @inheritdoc ERC4626Upgradeable @@ -272,50 +272,15 @@ contract VenusERC4626Isolated is revert ERC4626__RedeemMoreThanMax(); } - uint256 assets = previewRedeem(shares); - if (assets == 0) { + uint256 actualAssets = _beforeRedeem(shares); + if (actualAssets == 0) { revert ERC4626__ZeroAmount("redeem"); } - // actualAssets should be equal to assets, because of the round performed in previewRedeem - // but let's use the returned value anyway - uint256 actualAssets = beforeWithdraw(assets); _withdraw(_msgSender(), receiver, owner, actualAssets, shares); return actualAssets; } - /// @inheritdoc ERC4626Upgradeable - function previewDeposit(uint256 assets) public view virtual override returns (uint256) { - // round down the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals - uint256 adjustedAssets = _roundAssets(assets, Rounding.Down); - - return super.previewDeposit(adjustedAssets); - } - - /// @inheritdoc ERC4626Upgradeable - function previewMint(uint256 shares) public view virtual override returns (uint256) { - uint256 assets = super.previewMint(shares); - - // round up the asset amount, to deposit an amont equivalent to a natural number of vTokens, without decimals - return _roundAssets(assets, Rounding.Up); - } - - /// @inheritdoc ERC4626Upgradeable - function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { - // round up the asset amount to redeem a natural number of vTokens, without decimals - uint256 adjustedAssets = _roundAssets(assets, Rounding.Up); - - return super.previewWithdraw(adjustedAssets); - } - - /// @inheritdoc ERC4626Upgradeable - function previewRedeem(uint256 shares) public view virtual override returns (uint256) { - uint256 assets = super.previewRedeem(shares); - - // round down the asset amount to redeem a natural number of vTokens, without decimals - return _roundAssets(assets, Rounding.Down); - } - /// @notice Returns the total amount of assets deposited /// @return Amount of assets deposited function totalAssets() public view virtual override returns (uint256) { @@ -386,28 +351,67 @@ contract VenusERC4626Isolated is } } + /// @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) { + IERC20Upgradeable token = IERC20Upgradeable(asset()); + uint256 balanceBefore = token.balanceOf(address(this)); + + // Calculate the amount of vTokens equivalent to the amount of shares, rounding it down + uint256 vTokens = shares.mulDiv( + vToken.balanceOf(address(this)), + totalSupply() + 10 ** _decimalsOffset(), + MathUpgradeable.Rounding.Down + ); + + uint256 errorCode = vToken.redeem(vTokens); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + + uint256 balanceAfter = token.balanceOf(address(this)); + + // Return the amount of assets that was *actually* transferred in + return balanceAfter - balanceBefore; + } + /// @notice Redeems underlying assets before withdrawing from the vault. /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. /// @param assets The amount of underlying assets to redeem. - /// @return The amount of assets transferred in - function beforeWithdraw(uint256 assets) internal returns (uint256) { + /// @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) { IERC20Upgradeable token = IERC20Upgradeable(asset()); uint256 balanceBefore = token.balanceOf(address(this)); + uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); uint256 errorCode = vToken.redeemUnderlying(assets); if (errorCode != NO_ERROR) { revert VenusERC4626__VenusError(errorCode); } - uint256 balanceAfter = token.balanceOf(address(this)); - // Return the amount that was *actually* transferred - return balanceAfter - balanceBefore; + // Return the amount of assets *actually* transferred in + actualAssets = token.balanceOf(address(this)) - balanceBefore; + + uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); + if (actualVTokens == 0) { + revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); + } + // Return the shares equivalent to the burned vTokens + actualShares = actualVTokens.mulDiv( + totalSupply() + 10 ** _decimalsOffset(), + vTokenBalanceBefore, + MathUpgradeable.Rounding.Up + ); } /// @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 afterDeposit(uint256 assets) internal { + function _mintVTokens(uint256 assets) internal { ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); uint256 errorCode = vToken.mint(assets); if (errorCode != NO_ERROR) { @@ -426,13 +430,52 @@ contract VenusERC4626Isolated is rewardRecipient = newRecipient; } + /// @notice Deposits the assets into the VToken and calculates the shares to mint based on the + /// 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 { + // 1. Track pre-transfer balances + uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); + uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); + + // 2. Perform asset transfer (original OZ 4626 logic) + SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); + + // 3. Calculate actual assets received (protects against fee-on-transfer) + uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; + + // 4. Mint vTokens with received assets + _mintVTokens(assetsReceived); + + // 5. Verify actual vTokens received + uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; + if (vTokensReceived == 0) { + revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); + } + uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; + + // 6. Recalculate shares based on actual received value + // This is the same operation performed by previewDeposit, adjusting the total assets + uint256 actualShares = actualAssetsValue.mulDiv( + totalSupply() + 10 ** _decimalsOffset(), + totalAssets() + 1 - actualAssetsValue, // remove the new assets deposited to the VToken in this operation + MathUpgradeable.Rounding.Down + ); + + // 7. Mint the corrected share amount + _mint(receiver, actualShares); + + emit Deposit(caller, receiver, assets, actualShares); + } + /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. /// @return Gap between 18 and the decimals of the asset token function _decimalsOffset() internal view virtual override returns (uint8) { return 18 - ERC20Upgradeable(asset()).decimals(); } - /// @notice Generates an returns the derived name of the vault considering the asset name + /// @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) { @@ -445,21 +488,4 @@ contract VenusERC4626Isolated is function _generateVaultSymbol(ERC20Upgradeable asset_) internal view returns (string memory) { return string(abi.encodePacked("v4626", asset_.symbol())); } - - /// @notice Round the amount of assets, up or down, to a multiple of the exchange rate. That way the - /// associated number of VTokens won't have any decimals - /// @param assets The amount of assets to round - /// @param rounding If the round should be up (Rounding.Up), or down (Rounding.Down) - /// @return The rounded amount of assets - function _roundAssets(uint256 assets, Rounding rounding) internal view returns (uint256) { - uint256 exchangeRate = vToken.exchangeRateStored(); - uint256 redeemTokens = (assets * EXP_SCALE) / exchangeRate; - - uint256 _redeemAmount = (redeemTokens * exchangeRate) / EXP_SCALE; - - if (_redeemAmount != 0 && _redeemAmount != assets && rounding == Rounding.Up) redeemTokens++; // round up - - // redeemAmount = exchangeRate * redeemTokens - return (exchangeRate * redeemTokens) / EXP_SCALE; - } } From e8c47a2ff403af2e615290d87a2f2e04ae9fac90 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 11 Jun 2025 20:49:41 +0530 Subject: [PATCH 10/41] feat: abstract contract for VenusERC4626 --- contracts/ERC4626/Base/VenusERC4626.sol | 386 ++++++++++++++++++ contracts/ERC4626/VenusERC4626Core.sol | 436 +------------------- contracts/ERC4626/VenusERC4626Isolated.sol | 451 +-------------------- 3 files changed, 422 insertions(+), 851 deletions(-) create mode 100644 contracts/ERC4626/Base/VenusERC4626.sol diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol new file mode 100644 index 00000000..8143c058 --- /dev/null +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.25; + +import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +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 { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; + +import { IComptroller, Action } from "../Interfaces/IComptroller.sol"; + +uint256 constant EXP_SCALE = 1e18; + +/// @title VenusERC4626 +/// @notice Abstract ERC4626 wrapper for Venus vTokens +abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable { + using MathUpgradeable for uint256; + using SafeERC20Upgradeable for ERC20Upgradeable; + + /// @notice Error code representing no errors in Venus operations. + uint256 internal constant NO_ERROR = 0; + + /// @notice The Venus Comptroller contract, responsible for market operations. + IComptroller public comptroller; + + /// @notice The recipient of rewards distributed by the Venus Protocol. + address public rewardRecipient; + + /// @notice Emitted when rewards are claimed. + /// @param amount The amount of reward tokens claimed. + /// @param rewardToken The address of the reward token claimed. + event ClaimRewards(uint256 amount, address indexed rewardToken); + + /// @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 Event emitted when tokens are swept + event SweepToken(address indexed token, address indexed receiver, uint256 amount); + + /// @notice Thrown when a Venus protocol call returns an error. + /// @dev This error is triggered if a Venus operation (such as minting or redeeming vTokens) fails. + /// @param errorCode The error code returned by the Venus protocol. + error VenusERC4626__VenusError(uint256 errorCode); + + /// @notice Thrown when a deposit exceeds the maximum allowed limit. + /// @dev This error is triggered if the deposit amount is greater than `maxDeposit(receiver)`. + error ERC4626__DepositMoreThanMax(); + + /// @notice Thrown when a mint operation exceeds the maximum allowed limit. + /// @dev This error is triggered if the mint amount is greater than `maxMint(receiver)`. + error ERC4626__MintMoreThanMax(); + + /// @notice Thrown when a withdrawal exceeds the maximum available assets. + /// @dev This error is triggered if the withdrawal amount is greater than `maxWithdraw(owner)`. + error ERC4626__WithdrawMoreThanMax(); + + /// @notice Thrown when a redemption exceeds the maximum redeemable shares. + /// @dev This error is triggered if the redemption amount is greater than `maxRedeem(owner)`. + error ERC4626__RedeemMoreThanMax(); + + /// @notice Thrown when attempting an operation with a zero amount. + /// @dev This error prevents unnecessary transactions with zero amounts in deposit, withdraw, mint, or redeem operations. + /// @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() { + _disableInitializers(); + } + + /// @notice Initializes the VenusERC4626 vault with the VToken address + /// @param vToken_ The VToken associated with the vault + function initialize(address vToken_) public virtual initializer { + ensureNonzeroAddress(vToken_); + _initializeVToken(vToken_); + + ERC20Upgradeable asset = ERC20Upgradeable(_getUnderlying(vToken_)); + __ERC20_init(_generateVaultName(asset), _generateVaultSymbol(asset)); + __ERC4626_init(asset); + __ReentrancyGuard_init(); + } + + /// @notice Sets a new reward recipient address + /// @param newRecipient The address of the new reward recipient + function setRewardRecipient(address newRecipient) external virtual { + _checkAccessAllowed("setRewardRecipient(address)"); + _setRewardRecipient(newRecipient); + } + + /// @notice Sweeps tokens from the contract to the owner + /// @param token Address of the token to sweep + function sweepToken(IERC20Upgradeable token) external virtual onlyOwner { + uint256 balance = token.balanceOf(address(this)); + + if (balance > 0) { + address owner_ = owner(); + SafeERC20Upgradeable.safeTransfer(token, owner_, balance); + emit SweepToken(address(token), owner_, balance); + } + } + + /// @notice Claims rewards from the Venus protocol + /// @dev Must be implemented by child contracts + function claimRewards() external virtual; + + /// @notice Second initialization function to complete vault configuration + /// @param accessControlManager_ Address of the ACM contract + /// @param rewardRecipient_ Address that will receive rewards + /// @param vaultOwner_ Owner of the vault + function initialize2( + address accessControlManager_, + address rewardRecipient_, + address vaultOwner_ + ) public virtual reinitializer(2) { + ensureNonzeroAddress(vaultOwner_); + + __AccessControlled_init(accessControlManager_); + _setRewardRecipient(rewardRecipient_); + _transferOwnership(vaultOwner_); + } + + /// @inheritdoc ERC4626Upgradeable + function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + + vToken.accrueInterest(); + if (assets == 0) { + revert ERC4626__ZeroAmount("deposit"); + } + if (assets > maxDeposit(receiver)) { + revert ERC4626__DepositMoreThanMax(); + } + + uint256 shares = previewDeposit(assets); + if (shares == 0) { + revert ERC4626__ZeroAmount("deposit"); + } + + uint256 totalSupplyBefore = totalSupply(); + _deposit(_msgSender(), receiver, assets, shares); + uint256 actualShares = totalSupply() - totalSupplyBefore; + + return actualShares; + } + + /// @inheritdoc ERC4626Upgradeable + function mint(uint256 shares, address receiver) public virtual override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + + vToken.accrueInterest(); + if (shares == 0) { + revert ERC4626__ZeroAmount("mint"); + } + if (shares > maxMint(receiver)) { + revert ERC4626__MintMoreThanMax(); + } + + uint256 assets = previewMint(shares); + if (assets == 0) { + revert ERC4626__ZeroAmount("mint"); + } + _deposit(_msgSender(), receiver, assets, shares); + return assets; + } + + /// @inheritdoc ERC4626Upgradeable + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + ensureNonzeroAddress(owner); + + vToken.accrueInterest(); + if (assets == 0) { + revert ERC4626__ZeroAmount("withdraw"); + } + if (assets > maxWithdraw(owner)) { + revert ERC4626__WithdrawMoreThanMax(); + } + + (uint256 actualAssets, uint256 actualShares) = _beforeWithdraw(assets); + _withdraw(_msgSender(), receiver, owner, actualAssets, actualShares); + + return actualShares; + } + + /// @inheritdoc ERC4626Upgradeable + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual override nonReentrant returns (uint256) { + ensureNonzeroAddress(receiver); + ensureNonzeroAddress(owner); + + vToken.accrueInterest(); + if (shares == 0) { + revert ERC4626__ZeroAmount("redeem"); + } + if (shares > maxRedeem(owner)) { + revert ERC4626__RedeemMoreThanMax(); + } + + uint256 actualAssets = _beforeRedeem(shares); + if (actualAssets == 0) { + revert ERC4626__ZeroAmount("redeem"); + } + + _withdraw(_msgSender(), receiver, owner, actualAssets, shares); + return actualAssets; + } + + /// @inheritdoc ERC4626Upgradeable + function totalAssets() public view virtual override returns (uint256) { + return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; + } + + /// @inheritdoc ERC4626Upgradeable + function maxDeposit(address /*account*/) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.MINT)) { + return 0; + } + + uint256 supplyCap = comptroller.supplyCaps(address(vToken)); + uint256 totalSupply_ = (vToken.totalSupply() * vToken.exchangeRateStored()) / EXP_SCALE; + return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; + } + + /// @inheritdoc ERC4626Upgradeable + function maxMint(address /*account*/) public view virtual override returns (uint256) { + return convertToShares(maxDeposit(address(0))); + } + + /// @inheritdoc ERC4626Upgradeable + function maxWithdraw(address receiver) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { + return 0; + } + + uint256 cash = vToken.getCash(); + uint256 totalReserves = vToken.totalReserves(); + uint256 assetsBalance = convertToAssets(balanceOf(receiver)); + + if (cash < totalReserves) { + return 0; + } else { + uint256 availableCash = cash - totalReserves; + return availableCash < assetsBalance ? availableCash : assetsBalance; + } + } + + /// @inheritdoc ERC4626Upgradeable + function maxRedeem(address receiver) public view virtual override returns (uint256) { + if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { + return 0; + } + + uint256 cash = vToken.getCash(); + uint256 totalReserves = vToken.totalReserves(); + if (cash < totalReserves) { + return 0; + } else { + uint256 availableCash = cash - totalReserves; + uint256 availableCashInShares = convertToShares(availableCash); + uint256 shareBalance = balanceOf(receiver); + return availableCashInShares < shareBalance ? availableCashInShares : shareBalance; + } + } + + /// @notice Must be implemented by child contracts to initialize vToken + function _initializeVToken(address vToken_) internal virtual; + + /// @notice Internal function to redeem shares + function _beforeRedeem(uint256 shares) internal virtual returns (uint256) { + IERC20Upgradeable token = IERC20Upgradeable(asset()); + uint256 balanceBefore = token.balanceOf(address(this)); + + uint256 vTokens = shares.mulDiv( + vToken.balanceOf(address(this)), + totalSupply() + 10 ** _decimalsOffset(), + MathUpgradeable.Rounding.Down + ); + + uint256 errorCode = vToken.redeem(vTokens); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + + uint256 balanceAfter = token.balanceOf(address(this)); + + return balanceAfter - balanceBefore; + } + + /// @notice Internal function to handle withdrawals + 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)); + + uint256 errorCode = vToken.redeemUnderlying(assets); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + + actualAssets = token.balanceOf(address(this)) - balanceBefore; + + uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); + if (actualVTokens == 0) { + revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); + } + actualShares = actualVTokens.mulDiv( + totalSupply() + 10 ** _decimalsOffset(), + vTokenBalanceBefore, + MathUpgradeable.Rounding.Up + ); + } + + /// @notice Internal function to mint vTokens + function _mintVTokens(uint256 assets) internal virtual { + ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); + uint256 errorCode = vToken.mint(assets); + if (errorCode != NO_ERROR) { + revert VenusERC4626__VenusError(errorCode); + } + } + + /// @notice Internal function to set reward recipient + function _setRewardRecipient(address newRecipient) internal virtual { + ensureNonzeroAddress(newRecipient); + + emit RewardRecipientUpdated(rewardRecipient, newRecipient); + rewardRecipient = newRecipient; + } + + /// @inheritdoc ERC4626Upgradeable + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); + uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); + + SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); + + uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; + _mintVTokens(assetsReceived); + + uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; + if (vTokensReceived == 0) { + revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); + } + uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; + + uint256 actualShares = actualAssetsValue.mulDiv( + totalSupply() + 10 ** _decimalsOffset(), + totalAssets() + 1 - actualAssetsValue, + MathUpgradeable.Rounding.Down + ); + + _mint(receiver, actualShares); + + emit Deposit(caller, receiver, assets, actualShares); + } + + /// @inheritdoc ERC4626Upgradeable + function _decimalsOffset() internal view virtual override returns (uint8) { + return 18 - ERC20Upgradeable(asset()).decimals(); + } + + /// @notice Generates vault name + function _generateVaultName(ERC20Upgradeable asset_) internal view virtual returns (string memory) { + return string(abi.encodePacked("ERC4626-Wrapped Venus ", asset_.name())); + } + + /// @notice Generates vault symbol + function _generateVaultSymbol(ERC20Upgradeable asset_) internal view virtual returns (string memory) { + return string(abi.encodePacked("v4626", asset_.symbol())); + } + + /// @notice Must be implemented by child contracts to get underlying asset + function _getUnderlying(address vToken_) internal view virtual returns (address); +} diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index 97b62f5e..f61fd699 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -1,124 +1,18 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.25; -import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -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 { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; - -import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; -import { IComptroller, Action } from "./Interfaces/IComptroller.sol"; +import { VenusERC4626 } from "./VenusERC4626.sol"; import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; +import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; -uint256 constant EXP_SCALE = 1e18; - -/// @title VenusERC4626 -/// @notice ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. -contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable { - using MathUpgradeable for uint256; - using SafeERC20Upgradeable for ERC20Upgradeable; - - /// @notice Error code representing no errors in Venus operations. - uint256 internal constant NO_ERROR = 0; - +/// @title VenusERC4626Core +/// @notice ERC4626 wrapper for Venus Core Pool vTokens +contract VenusERC4626Core is VenusERC4626 { /// @notice The Venus vToken associated with this ERC4626 vault. VTokenInterface public vToken; - /// @notice The Venus Comptroller contract, responsible for market operations. - IComptroller public comptroller; - - /// @notice The recipient of rewards distributed by the Venus Protocol. - address public rewardRecipient; - - /// @notice Emitted when rewards are claimed. - /// @param amount The amount of reward tokens claimed. - /// @param rewardToken The address of the reward token claimed. - event ClaimRewards(uint256 amount, address indexed rewardToken); - - /// @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 Event emitted when tokens are swept - event SweepToken(address indexed token, address indexed receiver, uint256 amount); - - /// @notice Thrown when a Venus protocol call returns an error. - /// @dev This error is triggered if a Venus operation (such as minting or redeeming vTokens) fails. - /// @param errorCode The error code returned by the Venus protocol. - error VenusERC4626__VenusError(uint256 errorCode); - - /// @notice Thrown when a deposit exceeds the maximum allowed limit. - /// @dev This error is triggered if the deposit amount is greater than `maxDeposit(receiver)`. - error ERC4626__DepositMoreThanMax(); - - /// @notice Thrown when a mint operation exceeds the maximum allowed limit. - /// @dev This error is triggered if the mint amount is greater than `maxMint(receiver)`. - error ERC4626__MintMoreThanMax(); - - /// @notice Thrown when a withdrawal exceeds the maximum available assets. - /// @dev This error is triggered if the withdrawal amount is greater than `maxWithdraw(owner)`. - error ERC4626__WithdrawMoreThanMax(); - - /// @notice Thrown when a redemption exceeds the maximum redeemable shares. - /// @dev This error is triggered if the redemption amount is greater than `maxRedeem(owner)`. - error ERC4626__RedeemMoreThanMax(); - - /// @notice Thrown when attempting an operation with a zero amount. - /// @dev This error prevents unnecessary transactions with zero amounts in deposit, withdraw, mint, or redeem operations. - /// @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 = VTokenInterface(vToken_); - comptroller = IComptroller(address(vToken.comptroller())); - ERC20Upgradeable asset = ERC20Upgradeable(vToken.underlying()); - - __ERC20_init(_generateVaultName(asset), _generateVaultSymbol(asset)); - __ERC4626_init(asset); - __ReentrancyGuard_init(); - } - - /// @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 { - _checkAccessAllowed("setRewardRecipient(address)"); - _setRewardRecipient(newRecipient); - } - - /// @notice Sweeps the input token address tokens from the contract and sends them to the owner - /// @param token Address of the token - /// @custom:event SweepToken emits on success - /// @custom:access Only owner - function sweepToken(IERC20Upgradeable token) external onlyOwner { - uint256 balance = token.balanceOf(address(this)); - - if (balance > 0) { - address owner_ = owner(); - SafeERC20Upgradeable.safeTransfer(token, owner_, balance); - emit SweepToken(address(token), owner_, balance); - } - } - - /// @notice Claims XVS rewards from Venus Core Pool and transfers them to rewardRecipient - function claimRewards() external { + /// @inheritdoc VenusERC4626 + function claimRewards() external override { comptroller.claimVenus(address(this)); address xvsAddress = comptroller.getXVSAddress(); @@ -138,316 +32,14 @@ contract VenusERC4626Core is ERC4626Upgradeable, AccessControlledV8, ReentrancyG } } - /// @notice Second function to invoked 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 initialize2( - address accessControlManager_, - address rewardRecipient_, - address vaultOwner_ - ) public reinitializer(2) { - ensureNonzeroAddress(vaultOwner_); - - __AccessControlled_init(accessControlManager_); - _setRewardRecipient(rewardRecipient_); - _transferOwnership(vaultOwner_); - } - - /// @inheritdoc ERC4626Upgradeable - function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256) { - ensureNonzeroAddress(receiver); - - vToken.accrueInterest(); - if (assets == 0) { - revert ERC4626__ZeroAmount("deposit"); - } - if (assets > maxDeposit(receiver)) { - revert ERC4626__DepositMoreThanMax(); - } - - uint256 shares = previewDeposit(assets); - if (shares == 0) { - revert ERC4626__ZeroAmount("deposit"); - } - - uint256 totalSupplyBefore = totalSupply(); - _deposit(_msgSender(), receiver, assets, shares); - uint256 actualShares = totalSupply() - totalSupplyBefore; - - return actualShares; - } - - /// @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) { - ensureNonzeroAddress(receiver); - - vToken.accrueInterest(); - if (shares == 0) { - revert ERC4626__ZeroAmount("mint"); - } - if (shares > maxMint(receiver)) { - revert ERC4626__MintMoreThanMax(); - } - - uint256 assets = previewMint(shares); - if (assets == 0) { - revert ERC4626__ZeroAmount("mint"); - } - _deposit(_msgSender(), receiver, assets, shares); - return assets; - } - - /// @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) { - ensureNonzeroAddress(receiver); - ensureNonzeroAddress(owner); - - vToken.accrueInterest(); - if (assets == 0) { - revert ERC4626__ZeroAmount("withdraw"); - } - if (assets > maxWithdraw(owner)) { - revert ERC4626__WithdrawMoreThanMax(); - } - - (uint256 actualAssets, uint256 actualShares) = _beforeWithdraw(assets); - _withdraw(_msgSender(), receiver, owner, actualAssets, actualShares); - - return actualShares; - } - - /// @inheritdoc ERC4626Upgradeable - function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { - ensureNonzeroAddress(receiver); - ensureNonzeroAddress(owner); - - vToken.accrueInterest(); - if (shares == 0) { - revert ERC4626__ZeroAmount("redeem"); - } - if (shares > maxRedeem(owner)) { - revert ERC4626__RedeemMoreThanMax(); - } - - uint256 actualAssets = _beforeRedeem(shares); - if (actualAssets == 0) { - revert ERC4626__ZeroAmount("redeem"); - } - - _withdraw(_msgSender(), receiver, owner, actualAssets, shares); - return actualAssets; - } - - /// @notice Returns the total amount of assets deposited - /// @return Amount of assets deposited - function totalAssets() public view virtual override returns (uint256) { - return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; - } - - /// @notice Returns the maximum deposit allowed based on Venus supply caps. - /// @dev If minting is paused or the supply cap is reached, returns 0. - /// @param /*account*/ The address of the account. - /// @return The maximum amount of assets that can be deposited. - function maxDeposit(address /*account*/) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.MINT)) { - return 0; - } - - uint256 supplyCap = comptroller.supplyCaps(address(vToken)); - uint256 totalSupply_ = (vToken.totalSupply() * vToken.exchangeRateStored()) / EXP_SCALE; - return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; - } - - /// @notice Returns the maximum amount of shares that can be minted. - /// @dev This is derived from the maximum deposit amount converted to shares. - /// @param /*account*/ The address of the account. - /// @return The maximum number of shares that can be minted. - function maxMint(address /*account*/) public view virtual override returns (uint256) { - return convertToShares(maxDeposit(address(0))); - } - - /// @notice Returns the maximum amount that can be withdrawn. - /// @dev The withdrawable amount is limited by the available cash in the vault. - /// @param receiver The address of the account withdrawing. - /// @return The maximum amount of assets that can be withdrawn. - function maxWithdraw(address receiver) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { - return 0; - } - - uint256 cash = vToken.getCash(); - uint256 totalReserves = vToken.totalReserves(); - uint256 assetsBalance = convertToAssets(balanceOf(receiver)); - - if (cash < totalReserves) { - return 0; - } else { - uint256 availableCash = cash - totalReserves; - return availableCash < assetsBalance ? availableCash : assetsBalance; - } - } - - /// @notice Returns the maximum amount of shares that can be redeemed. - /// @dev Redemption is limited by the available cash in the vault. - /// @param receiver The address of the account redeeming. - /// @return The maximum number of shares that can be redeemed. - function maxRedeem(address receiver) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { - return 0; - } - - uint256 cash = vToken.getCash(); - uint256 totalReserves = vToken.totalReserves(); - if (cash < totalReserves) { - return 0; - } else { - uint256 availableCash = cash - totalReserves; - uint256 availableCashInShares = convertToShares(availableCash); - uint256 shareBalance = balanceOf(receiver); - return availableCashInShares < shareBalance ? availableCashInShares : shareBalance; - } - } - - /// @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) { - IERC20Upgradeable token = IERC20Upgradeable(asset()); - uint256 balanceBefore = token.balanceOf(address(this)); - - // Calculate the amount of vTokens equivalent to the amount of shares, rounding it down - uint256 vTokens = shares.mulDiv( - vToken.balanceOf(address(this)), - totalSupply() + 10 ** _decimalsOffset(), - MathUpgradeable.Rounding.Down - ); - - uint256 errorCode = vToken.redeem(vTokens); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - - uint256 balanceAfter = token.balanceOf(address(this)); - - // Return the amount of assets that was *actually* transferred in - return balanceAfter - balanceBefore; - } - - /// @notice Redeems underlying assets before withdrawing from the vault. - /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. - /// @param assets The amount of underlying assets to redeem. - /// @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) { - IERC20Upgradeable token = IERC20Upgradeable(asset()); - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); - - uint256 errorCode = vToken.redeemUnderlying(assets); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - - // Return the amount of assets *actually* transferred in - actualAssets = token.balanceOf(address(this)) - balanceBefore; - - uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); - if (actualVTokens == 0) { - revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); - } - // Return the shares equivalent to the burned vTokens - actualShares = actualVTokens.mulDiv( - totalSupply() + 10 ** _decimalsOffset(), - vTokenBalanceBefore, - MathUpgradeable.Rounding.Up - ); - } - - /// @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 { - ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); - uint256 errorCode = vToken.mint(assets); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - } - - /// @notice Sets a new reward recipient address - /// @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 { - ensureNonzeroAddress(newRecipient); - - emit RewardRecipientUpdated(rewardRecipient, newRecipient); - rewardRecipient = newRecipient; - } - - /// @notice Deposits the assets into the VToken and calculates the shares to mint based on the - /// 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 { - // 1. Track pre-transfer balances - uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); - uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); - - // 2. Perform asset transfer (original OZ 4626 logic) - SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); - - // 3. Calculate actual assets received (protects against fee-on-transfer) - uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; - - // 4. Mint vTokens with received assets - _mintVTokens(assetsReceived); - - // 5. Verify actual vTokens received - uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; - if (vTokensReceived == 0) { - revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); - } - uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; - - // 6. Recalculate shares based on actual received value - // This is the same operation performed by previewDeposit, adjusting the total assets - uint256 actualShares = actualAssetsValue.mulDiv( - totalSupply() + 10 ** _decimalsOffset(), - totalAssets() + 1 - actualAssetsValue, // remove the new assets deposited to the VToken in this operation - MathUpgradeable.Rounding.Down - ); - - // 7. Mint the corrected share amount - _mint(receiver, actualShares); - - emit Deposit(caller, receiver, assets, actualShares); - } - - /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. - /// @return Gap between 18 and the decimals of the asset token - function _decimalsOffset() internal view virtual override returns (uint8) { - return 18 - ERC20Upgradeable(asset()).decimals(); - } - - /// @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) { - return string(abi.encodePacked("ERC4626-Wrapped Venus ", asset_.name())); + /// @inheritdoc VenusERC4626 + function _initializeVToken(address vToken_) internal override { + vToken = VTokenInterface(vToken_); + comptroller = IComptroller(address(vToken.comptroller())); } - /// @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) { - return string(abi.encodePacked("v4626", asset_.symbol())); + /// @inheritdoc VenusERC4626 + function _getUnderlying(address vToken_) internal view override returns (address) { + return VTokenInterface(vToken_).underlying(); } } diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 172c1f26..930c88d4 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -1,145 +1,28 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.25; -import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -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 { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; -import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; - -import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; +import { VenusERC4626 } from "./Base/VenusERC4626.sol"; +import { VToken } from "@venusprotocol/isolated-pools/contracts/VToken.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 { Action } from "./Interfaces/IComptroller.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 VenusERC4626Isolated is - ERC4626Upgradeable, - AccessControlledV8, - MaxLoopsLimitHelper, - ReentrancyGuardUpgradeable -{ - using MathUpgradeable for uint256; - using SafeERC20Upgradeable for ERC20Upgradeable; +import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; - /// @notice Error code representing no errors in Venus operations. - uint256 internal constant NO_ERROR = 0; +/// @title VenusERC4626Isolated +/// @notice ERC4626 wrapper for Venus Isolated Pool vTokens +contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { + using MaxLoopsLimitHelper for uint256; /// @notice The Venus vToken associated with this ERC4626 vault. VToken public vToken; - /// @notice The Venus Comptroller contract, responsible for market operations. - IComptroller public comptroller; - - /// @notice The recipient of rewards distributed by the Venus Protocol. - address public rewardRecipient; - - /// @notice Emitted when rewards are claimed. - /// @param amount The amount of reward tokens claimed. - /// @param rewardToken The address of the reward token claimed. - event ClaimRewards(uint256 amount, address indexed rewardToken); - - /// @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 Event emitted when tokens are swept - event SweepToken(address indexed token, address indexed receiver, uint256 amount); - - /// @notice Thrown when a Venus protocol call returns an error. - /// @dev This error is triggered if a Venus operation (such as minting or redeeming vTokens) fails. - /// @param errorCode The error code returned by the Venus protocol. - error VenusERC4626__VenusError(uint256 errorCode); - - /// @notice Thrown when a deposit exceeds the maximum allowed limit. - /// @dev This error is triggered if the deposit amount is greater than `maxDeposit(receiver)`. - error ERC4626__DepositMoreThanMax(); - - /// @notice Thrown when a mint operation exceeds the maximum allowed limit. - /// @dev This error is triggered if the mint amount is greater than `maxMint(receiver)`. - error ERC4626__MintMoreThanMax(); - - /// @notice Thrown when a withdrawal exceeds the maximum available assets. - /// @dev This error is triggered if the withdrawal amount is greater than `maxWithdraw(owner)`. - error ERC4626__WithdrawMoreThanMax(); - - /// @notice Thrown when a redemption exceeds the maximum redeemable shares. - /// @dev This error is triggered if the redemption amount is greater than `maxRedeem(owner)`. - error ERC4626__RedeemMoreThanMax(); - - /// @notice Thrown when attempting an operation with a zero amount. - /// @dev This error prevents unnecessary transactions with zero amounts in deposit, withdraw, mint, or redeem operations. - /// @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 - */ + /// @notice Sets the maximum loops limit 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 { - _checkAccessAllowed("setRewardRecipient(address)"); - _setRewardRecipient(newRecipient); - } - - /// @notice Sweeps the input token address tokens from the contract and sends them to the owner - /// @param token Address of the token - /// @custom:event SweepToken emits on success - /// @custom:access Only owner - function sweepToken(IERC20Upgradeable token) external onlyOwner { - uint256 balance = token.balanceOf(address(this)); - - if (balance > 0) { - address owner_ = owner(); - SafeERC20Upgradeable.safeTransfer(token, owner_, balance); - emit SweepToken(address(token), owner_, balance); - } - } - - /// @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 { + /// @inheritdoc VenusERC4626 + function claimRewards() external override { IComptroller _comptroller = comptroller; VToken _vToken = vToken; address _rewardRecipient = rewardRecipient; @@ -154,14 +37,12 @@ contract VenusERC4626Isolated is VToken[] memory vTokens = new VToken[](1); vTokens[0] = _vToken; - RewardsDistributor(rewardDistributor).claimRewardToken(address(this), vTokens); + 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), @@ -169,22 +50,19 @@ contract VenusERC4626Isolated is IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS ) {} catch {} + + emit ClaimRewards(rewardBalance, address(rewardToken)); } - emit ClaimRewards(rewardBalance, address(rewardToken)); } } - /// @notice Second function to invoked 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 + /// @notice Initializes the isolated pool vault with additional parameters function initialize2( address accessControlManager_, address rewardRecipient_, uint256 loopsLimit_, address vaultOwner_ - ) public reinitializer(2) { + ) public override reinitializer(2) { ensureNonzeroAddress(vaultOwner_); __AccessControlled_init(accessControlManager_); @@ -193,299 +71,14 @@ contract VenusERC4626Isolated is _transferOwnership(vaultOwner_); } - /// @inheritdoc ERC4626Upgradeable - function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256) { - ensureNonzeroAddress(receiver); - - vToken.accrueInterest(); - if (assets == 0) { - revert ERC4626__ZeroAmount("deposit"); - } - if (assets > maxDeposit(receiver)) { - revert ERC4626__DepositMoreThanMax(); - } - - uint256 shares = previewDeposit(assets); - if (shares == 0) { - revert ERC4626__ZeroAmount("deposit"); - } - - uint256 totalSupplyBefore = totalSupply(); - _deposit(_msgSender(), receiver, assets, shares); - uint256 actualShares = totalSupply() - totalSupplyBefore; - - return actualShares; - } - - /// @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) { - ensureNonzeroAddress(receiver); - - vToken.accrueInterest(); - if (shares == 0) { - revert ERC4626__ZeroAmount("mint"); - } - if (shares > maxMint(receiver)) { - revert ERC4626__MintMoreThanMax(); - } - uint256 assets = previewMint(shares); - if (assets == 0) { - revert ERC4626__ZeroAmount("mint"); - } - _deposit(_msgSender(), receiver, assets, shares); - return assets; - } - - /// @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) { - ensureNonzeroAddress(receiver); - ensureNonzeroAddress(owner); - - vToken.accrueInterest(); - if (assets == 0) { - revert ERC4626__ZeroAmount("withdraw"); - } - if (assets > maxWithdraw(owner)) { - revert ERC4626__WithdrawMoreThanMax(); - } - - (uint256 actualAssets, uint256 actualShares) = _beforeWithdraw(assets); - _withdraw(_msgSender(), receiver, owner, actualAssets, actualShares); - - return actualShares; - } - - /// @inheritdoc ERC4626Upgradeable - function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { - ensureNonzeroAddress(receiver); - ensureNonzeroAddress(owner); - - vToken.accrueInterest(); - if (shares == 0) { - revert ERC4626__ZeroAmount("redeem"); - } - if (shares > maxRedeem(owner)) { - revert ERC4626__RedeemMoreThanMax(); - } - - uint256 actualAssets = _beforeRedeem(shares); - if (actualAssets == 0) { - revert ERC4626__ZeroAmount("redeem"); - } - - _withdraw(_msgSender(), receiver, owner, actualAssets, shares); - return actualAssets; - } - - /// @notice Returns the total amount of assets deposited - /// @return Amount of assets deposited - function totalAssets() public view virtual override returns (uint256) { - return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; - } - - /// @notice Returns the maximum deposit allowed based on Venus supply caps. - /// @dev If minting is paused or the supply cap is reached, returns 0. - /// @param /*account*/ The address of the account. - /// @return The maximum amount of assets that can be deposited. - function maxDeposit(address /*account*/) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.MINT)) { - return 0; - } - - uint256 supplyCap = comptroller.supplyCaps(address(vToken)); - uint256 totalSupply_ = (vToken.totalSupply() * vToken.exchangeRateStored()) / EXP_SCALE; - return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; - } - - /// @notice Returns the maximum amount of shares that can be minted. - /// @dev This is derived from the maximum deposit amount converted to shares. - /// @param /*account*/ The address of the account. - /// @return The maximum number of shares that can be minted. - function maxMint(address /*account*/) public view virtual override returns (uint256) { - return convertToShares(maxDeposit(address(0))); - } - - /// @notice Returns the maximum amount that can be withdrawn. - /// @dev The withdrawable amount is limited by the available cash in the vault. - /// @param receiver The address of the account withdrawing. - /// @return The maximum amount of assets that can be withdrawn. - function maxWithdraw(address receiver) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { - return 0; - } - - uint256 cash = vToken.getCash(); - uint256 totalReserves = vToken.totalReserves(); - uint256 assetsBalance = convertToAssets(balanceOf(receiver)); - - if (cash < totalReserves) { - return 0; - } else { - uint256 availableCash = cash - totalReserves; - return availableCash < assetsBalance ? availableCash : assetsBalance; - } - } - - /// @notice Returns the maximum amount of shares that can be redeemed. - /// @dev Redemption is limited by the available cash in the vault. - /// @param receiver The address of the account redeeming. - /// @return The maximum number of shares that can be redeemed. - function maxRedeem(address receiver) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { - return 0; - } - - uint256 cash = vToken.getCash(); - uint256 totalReserves = vToken.totalReserves(); - if (cash < totalReserves) { - return 0; - } else { - uint256 availableCash = cash - totalReserves; - uint256 availableCashInShares = convertToShares(availableCash); - uint256 shareBalance = balanceOf(receiver); - return availableCashInShares < shareBalance ? availableCashInShares : shareBalance; - } - } - - /// @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) { - IERC20Upgradeable token = IERC20Upgradeable(asset()); - uint256 balanceBefore = token.balanceOf(address(this)); - - // Calculate the amount of vTokens equivalent to the amount of shares, rounding it down - uint256 vTokens = shares.mulDiv( - vToken.balanceOf(address(this)), - totalSupply() + 10 ** _decimalsOffset(), - MathUpgradeable.Rounding.Down - ); - - uint256 errorCode = vToken.redeem(vTokens); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - - uint256 balanceAfter = token.balanceOf(address(this)); - - // Return the amount of assets that was *actually* transferred in - return balanceAfter - balanceBefore; - } - - /// @notice Redeems underlying assets before withdrawing from the vault. - /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. - /// @param assets The amount of underlying assets to redeem. - /// @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) { - IERC20Upgradeable token = IERC20Upgradeable(asset()); - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); - - uint256 errorCode = vToken.redeemUnderlying(assets); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - - // Return the amount of assets *actually* transferred in - actualAssets = token.balanceOf(address(this)) - balanceBefore; - - uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); - if (actualVTokens == 0) { - revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); - } - // Return the shares equivalent to the burned vTokens - actualShares = actualVTokens.mulDiv( - totalSupply() + 10 ** _decimalsOffset(), - vTokenBalanceBefore, - MathUpgradeable.Rounding.Up - ); - } - - /// @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 { - ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); - uint256 errorCode = vToken.mint(assets); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - } - - /// @notice Sets a new reward recipient address - /// @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 { - ensureNonzeroAddress(newRecipient); - - emit RewardRecipientUpdated(rewardRecipient, newRecipient); - rewardRecipient = newRecipient; - } - - /// @notice Deposits the assets into the VToken and calculates the shares to mint based on the - /// 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 { - // 1. Track pre-transfer balances - uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); - uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); - - // 2. Perform asset transfer (original OZ 4626 logic) - SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); - - // 3. Calculate actual assets received (protects against fee-on-transfer) - uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; - - // 4. Mint vTokens with received assets - _mintVTokens(assetsReceived); - - // 5. Verify actual vTokens received - uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; - if (vTokensReceived == 0) { - revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); - } - uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; - - // 6. Recalculate shares based on actual received value - // This is the same operation performed by previewDeposit, adjusting the total assets - uint256 actualShares = actualAssetsValue.mulDiv( - totalSupply() + 10 ** _decimalsOffset(), - totalAssets() + 1 - actualAssetsValue, // remove the new assets deposited to the VToken in this operation - MathUpgradeable.Rounding.Down - ); - - // 7. Mint the corrected share amount - _mint(receiver, actualShares); - - emit Deposit(caller, receiver, assets, actualShares); - } - - /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. - /// @return Gap between 18 and the decimals of the asset token - function _decimalsOffset() internal view virtual override returns (uint8) { - return 18 - ERC20Upgradeable(asset()).decimals(); - } - - /// @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) { - return string(abi.encodePacked("ERC4626-Wrapped Venus ", asset_.name())); + /// @inheritdoc VenusERC4626 + function _initializeVToken(address vToken_) internal override { + vToken = VToken(vToken_); + comptroller = IComptroller(address(vToken.comptroller())); } - /// @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) { - return string(abi.encodePacked("v4626", asset_.symbol())); + /// @inheritdoc VenusERC4626 + function _getUnderlying(address vToken_) internal view override returns (address) { + return VToken(vToken_).underlying(); } } From c55995fb2cacdf5eae6fee6e463140661008c094 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 17 Jun 2025 20:39:31 +0530 Subject: [PATCH 11/41] refactor: separated initialize logic --- contracts/ERC4626/Base/VenusERC4626.sol | 16 +- contracts/ERC4626/VenusERC4626.sol | 485 --------------------- contracts/ERC4626/VenusERC4626Isolated.sol | 32 +- 3 files changed, 23 insertions(+), 510 deletions(-) delete mode 100644 contracts/ERC4626/VenusERC4626.sol diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index 8143c058..669486ef 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -11,6 +11,7 @@ import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/ import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; import { IComptroller, Action } from "../Interfaces/IComptroller.sol"; +import { VTokenInterface } from "../Interfaces/VTokenInterface.sol"; uint256 constant EXP_SCALE = 1e18; @@ -23,6 +24,9 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @notice Error code representing no errors in Venus operations. uint256 internal constant NO_ERROR = 0; + /// @notice The Venus vToken associated with this ERC4626 vault. + VTokenInterface public vToken; + /// @notice The Venus Comptroller contract, responsible for market operations. IComptroller public comptroller; @@ -77,9 +81,11 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @param vToken_ The VToken associated with the vault function initialize(address vToken_) public virtual initializer { ensureNonzeroAddress(vToken_); - _initializeVToken(vToken_); - ERC20Upgradeable asset = ERC20Upgradeable(_getUnderlying(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(); @@ -274,9 +280,6 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr } } - /// @notice Must be implemented by child contracts to initialize vToken - function _initializeVToken(address vToken_) internal virtual; - /// @notice Internal function to redeem shares function _beforeRedeem(uint256 shares) internal virtual returns (uint256) { IERC20Upgradeable token = IERC20Upgradeable(asset()); @@ -380,7 +383,4 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr function _generateVaultSymbol(ERC20Upgradeable asset_) internal view virtual returns (string memory) { return string(abi.encodePacked("v4626", asset_.symbol())); } - - /// @notice Must be implemented by child contracts to get underlying asset - function _getUnderlying(address vToken_) internal view virtual returns (address); } diff --git a/contracts/ERC4626/VenusERC4626.sol b/contracts/ERC4626/VenusERC4626.sol deleted file mode 100644 index d599b885..00000000 --- a/contracts/ERC4626/VenusERC4626.sol +++ /dev/null @@ -1,485 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.25; - -import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -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 { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import { MathUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.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 { 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"; - -/// @title VenusERC4626 -/// @notice ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. -contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, MaxLoopsLimitHelper, ReentrancyGuardUpgradeable { - using MathUpgradeable for uint256; - using SafeERC20Upgradeable for ERC20Upgradeable; - - /// @notice Error code representing no errors in Venus operations. - uint256 internal constant NO_ERROR = 0; - - /// @notice The Venus vToken associated with this ERC4626 vault. - VToken public vToken; - - /// @notice The Venus Comptroller contract, responsible for market operations. - IComptroller public comptroller; - - /// @notice The recipient of rewards distributed by the Venus Protocol. - address public rewardRecipient; - - /// @notice Emitted when rewards are claimed. - /// @param amount The amount of reward tokens claimed. - /// @param rewardToken The address of the reward token claimed. - event ClaimRewards(uint256 amount, address indexed rewardToken); - - /// @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 Event emitted when tokens are swept - event SweepToken(address indexed token, address indexed receiver, uint256 amount); - - /// @notice Thrown when a Venus protocol call returns an error. - /// @dev This error is triggered if a Venus operation (such as minting or redeeming vTokens) fails. - /// @param errorCode The error code returned by the Venus protocol. - error VenusERC4626__VenusError(uint256 errorCode); - - /// @notice Thrown when a deposit exceeds the maximum allowed limit. - /// @dev This error is triggered if the deposit amount is greater than `maxDeposit(receiver)`. - error ERC4626__DepositMoreThanMax(); - - /// @notice Thrown when a mint operation exceeds the maximum allowed limit. - /// @dev This error is triggered if the mint amount is greater than `maxMint(receiver)`. - error ERC4626__MintMoreThanMax(); - - /// @notice Thrown when a withdrawal exceeds the maximum available assets. - /// @dev This error is triggered if the withdrawal amount is greater than `maxWithdraw(owner)`. - error ERC4626__WithdrawMoreThanMax(); - - /// @notice Thrown when a redemption exceeds the maximum redeemable shares. - /// @dev This error is triggered if the redemption amount is greater than `maxRedeem(owner)`. - error ERC4626__RedeemMoreThanMax(); - - /// @notice Thrown when attempting an operation with a zero amount. - /// @dev This error prevents unnecessary transactions with zero amounts in deposit, withdraw, mint, or redeem operations. - /// @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 { - _checkAccessAllowed("setRewardRecipient(address)"); - _setRewardRecipient(newRecipient); - } - - /// @notice Sweeps the input token address tokens from the contract and sends them to the owner - /// @param token Address of the token - /// @custom:event SweepToken emits on success - /// @custom:access Only owner - function sweepToken(IERC20Upgradeable token) external onlyOwner { - uint256 balance = token.balanceOf(address(this)); - - if (balance > 0) { - address owner_ = owner(); - SafeERC20Upgradeable.safeTransfer(token, owner_, balance); - emit SweepToken(address(token), owner_, balance); - } - } - - /// @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_); - } - - /// @inheritdoc ERC4626Upgradeable - function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256) { - ensureNonzeroAddress(receiver); - - vToken.accrueInterest(); - if (assets == 0) { - revert ERC4626__ZeroAmount("deposit"); - } - if (assets > maxDeposit(receiver)) { - revert ERC4626__DepositMoreThanMax(); - } - - uint256 shares = previewDeposit(assets); - if (shares == 0) { - revert ERC4626__ZeroAmount("deposit"); - } - - uint256 totalSupplyBefore = totalSupply(); - _deposit(_msgSender(), receiver, assets, shares); - uint256 actualShares = totalSupply() - totalSupplyBefore; - - return actualShares; - } - - /// @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) { - ensureNonzeroAddress(receiver); - - vToken.accrueInterest(); - if (shares == 0) { - revert ERC4626__ZeroAmount("mint"); - } - if (shares > maxMint(receiver)) { - revert ERC4626__MintMoreThanMax(); - } - uint256 assets = previewMint(shares); - if (assets == 0) { - revert ERC4626__ZeroAmount("mint"); - } - _deposit(_msgSender(), receiver, assets, shares); - return assets; - } - - /// @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) { - ensureNonzeroAddress(receiver); - ensureNonzeroAddress(owner); - - vToken.accrueInterest(); - if (assets == 0) { - revert ERC4626__ZeroAmount("withdraw"); - } - if (assets > maxWithdraw(owner)) { - revert ERC4626__WithdrawMoreThanMax(); - } - - (uint256 actualAssets, uint256 actualShares) = _beforeWithdraw(assets); - _withdraw(_msgSender(), receiver, owner, actualAssets, actualShares); - - return actualShares; - } - - /// @inheritdoc ERC4626Upgradeable - function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) { - ensureNonzeroAddress(receiver); - ensureNonzeroAddress(owner); - - vToken.accrueInterest(); - if (shares == 0) { - revert ERC4626__ZeroAmount("redeem"); - } - if (shares > maxRedeem(owner)) { - revert ERC4626__RedeemMoreThanMax(); - } - - uint256 actualAssets = _beforeRedeem(shares); - if (actualAssets == 0) { - revert ERC4626__ZeroAmount("redeem"); - } - - _withdraw(_msgSender(), receiver, owner, actualAssets, shares); - return actualAssets; - } - - /// @notice Returns the total amount of assets deposited - /// @return Amount of assets deposited - function totalAssets() public view virtual override returns (uint256) { - return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; - } - - /// @notice Returns the maximum deposit allowed based on Venus supply caps. - /// @dev If minting is paused or the supply cap is reached, returns 0. - /// @param /*account*/ The address of the account. - /// @return The maximum amount of assets that can be deposited. - function maxDeposit(address /*account*/) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.MINT)) { - return 0; - } - - uint256 supplyCap = comptroller.supplyCaps(address(vToken)); - uint256 totalSupply_ = (vToken.totalSupply() * vToken.exchangeRateStored()) / EXP_SCALE; - return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; - } - - /// @notice Returns the maximum amount of shares that can be minted. - /// @dev This is derived from the maximum deposit amount converted to shares. - /// @param /*account*/ The address of the account. - /// @return The maximum number of shares that can be minted. - function maxMint(address /*account*/) public view virtual override returns (uint256) { - return convertToShares(maxDeposit(address(0))); - } - - /// @notice Returns the maximum amount that can be withdrawn. - /// @dev The withdrawable amount is limited by the available cash in the vault. - /// @param receiver The address of the account withdrawing. - /// @return The maximum amount of assets that can be withdrawn. - function maxWithdraw(address receiver) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { - return 0; - } - - uint256 cash = vToken.getCash(); - uint256 totalReserves = vToken.totalReserves(); - uint256 assetsBalance = convertToAssets(balanceOf(receiver)); - - if (cash < totalReserves) { - return 0; - } else { - uint256 availableCash = cash - totalReserves; - return availableCash < assetsBalance ? availableCash : assetsBalance; - } - } - - /// @notice Returns the maximum amount of shares that can be redeemed. - /// @dev Redemption is limited by the available cash in the vault. - /// @param receiver The address of the account redeeming. - /// @return The maximum number of shares that can be redeemed. - function maxRedeem(address receiver) public view virtual override returns (uint256) { - if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { - return 0; - } - - uint256 cash = vToken.getCash(); - uint256 totalReserves = vToken.totalReserves(); - if (cash < totalReserves) { - return 0; - } else { - uint256 availableCash = cash - totalReserves; - uint256 availableCashInShares = convertToShares(availableCash); - uint256 shareBalance = balanceOf(receiver); - return availableCashInShares < shareBalance ? availableCashInShares : shareBalance; - } - } - - /// @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) { - IERC20Upgradeable token = IERC20Upgradeable(asset()); - uint256 balanceBefore = token.balanceOf(address(this)); - - // Calculate the amount of vTokens equivalent to the amount of shares, rounding it down - uint256 vTokens = shares.mulDiv( - vToken.balanceOf(address(this)), - totalSupply() + 10 ** _decimalsOffset(), - MathUpgradeable.Rounding.Down - ); - - uint256 errorCode = vToken.redeem(vTokens); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - - uint256 balanceAfter = token.balanceOf(address(this)); - - // Return the amount of assets that was *actually* transferred in - return balanceAfter - balanceBefore; - } - - /// @notice Redeems underlying assets before withdrawing from the vault. - /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. - /// @param assets The amount of underlying assets to redeem. - /// @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) { - IERC20Upgradeable token = IERC20Upgradeable(asset()); - uint256 balanceBefore = token.balanceOf(address(this)); - uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); - - uint256 errorCode = vToken.redeemUnderlying(assets); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - - // Return the amount of assets *actually* transferred in - actualAssets = token.balanceOf(address(this)) - balanceBefore; - - uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); - if (actualVTokens == 0) { - revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); - } - // Return the shares equivalent to the burned vTokens - actualShares = actualVTokens.mulDiv( - totalSupply() + 10 ** _decimalsOffset(), - vTokenBalanceBefore, - MathUpgradeable.Rounding.Up - ); - } - - /// @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 { - ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); - uint256 errorCode = vToken.mint(assets); - if (errorCode != NO_ERROR) { - revert VenusERC4626__VenusError(errorCode); - } - } - - /// @notice Sets a new reward recipient address - /// @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 { - ensureNonzeroAddress(newRecipient); - - emit RewardRecipientUpdated(rewardRecipient, newRecipient); - rewardRecipient = newRecipient; - } - - /// @notice Deposits the assets into the VToken and calculates the shares to mint based on the - /// 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 { - // 1. Track pre-transfer balances - uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); - uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); - - // 2. Perform asset transfer (original OZ 4626 logic) - SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); - - // 3. Calculate actual assets received (protects against fee-on-transfer) - uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; - - // 4. Mint vTokens with received assets - _mintVTokens(assetsReceived); - - // 5. Verify actual vTokens received - uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; - if (vTokensReceived == 0) { - revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); - } - uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; - - // 6. Recalculate shares based on actual received value - // This is the same operation performed by previewDeposit, adjusting the total assets - uint256 actualShares = actualAssetsValue.mulDiv( - totalSupply() + 10 ** _decimalsOffset(), - totalAssets() + 1 - actualAssetsValue, // remove the new assets deposited to the VToken in this operation - MathUpgradeable.Rounding.Down - ); - - // 7. Mint the corrected share amount - _mint(receiver, actualShares); - - emit Deposit(caller, receiver, assets, actualShares); - } - - /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. - /// @return Gap between 18 and the decimals of the asset token - function _decimalsOffset() internal view virtual override returns (uint8) { - return 18 - ERC20Upgradeable(asset()).decimals(); - } - - /// @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) { - 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) { - return string(abi.encodePacked("v4626", asset_.symbol())); - } -} diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 930c88d4..38e4a047 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -2,7 +2,10 @@ 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 { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "@venusprotocol/isolated-pools/contracts/MaxLoopsLimitHelper.sol"; import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; @@ -10,12 +13,16 @@ import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; /// @title VenusERC4626Isolated /// @notice ERC4626 wrapper for Venus Isolated Pool vTokens contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { - using MaxLoopsLimitHelper for uint256; + uint256 public constant LOOPS_LIMIT = 100; - /// @notice The Venus vToken associated with this ERC4626 vault. - VToken public vToken; + /// @notice Initializes the VenusERC4626Isolated contract + /// @param vToken_ The address of the vToken to be wrapped + function initialize(address vToken_) public virtual override initializer { + super.initialize(vToken_); + } /// @notice Sets the maximum loops limit + /// @param loopsLimit Number of loops limit function setMaxLoopsLimit(uint256 loopsLimit) external { _checkAccessAllowed("setMaxLoopsLimit(uint256)"); _setMaxLoopsLimit(loopsLimit); @@ -24,7 +31,7 @@ contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { /// @inheritdoc VenusERC4626 function claimRewards() external override { IComptroller _comptroller = comptroller; - VToken _vToken = vToken; + VToken _vToken = VToken(address(vToken)); address _rewardRecipient = rewardRecipient; RewardsDistributor[] memory rewardDistributors = _comptroller.getRewardDistributors(); @@ -57,28 +64,19 @@ contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { } /// @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 function initialize2( address accessControlManager_, address rewardRecipient_, - uint256 loopsLimit_, address vaultOwner_ ) public override reinitializer(2) { ensureNonzeroAddress(vaultOwner_); __AccessControlled_init(accessControlManager_); - _setMaxLoopsLimit(loopsLimit_); + _setMaxLoopsLimit(LOOPS_LIMIT); _setRewardRecipient(rewardRecipient_); _transferOwnership(vaultOwner_); } - - /// @inheritdoc VenusERC4626 - function _initializeVToken(address vToken_) internal override { - vToken = VToken(vToken_); - comptroller = IComptroller(address(vToken.comptroller())); - } - - /// @inheritdoc VenusERC4626 - function _getUnderlying(address vToken_) internal view override returns (address) { - return VToken(vToken_).underlying(); - } } From aa4c7b41b487a9f90da75710b4180bb7eaf837e0 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 17 Jun 2025 20:41:09 +0530 Subject: [PATCH 12/41] test: tests for VenusERC4626Isolated --- ...C4626.sol => MockVenusERC4626Isolated.sol} | 6 +- tests/hardhat/ERC4626/VenusERC4626Isolated.ts | 410 ++++++++++++++++++ 2 files changed, 413 insertions(+), 3 deletions(-) rename contracts/test/Mocks/{MockVenusERC4626.sol => MockVenusERC4626Isolated.sol} (93%) create mode 100644 tests/hardhat/ERC4626/VenusERC4626Isolated.ts diff --git a/contracts/test/Mocks/MockVenusERC4626.sol b/contracts/test/Mocks/MockVenusERC4626Isolated.sol similarity index 93% rename from contracts/test/Mocks/MockVenusERC4626.sol rename to contracts/test/Mocks/MockVenusERC4626Isolated.sol index 8a9f8357..c8625490 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; +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/tests/hardhat/ERC4626/VenusERC4626Isolated.ts b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts new file mode 100644 index 00000000..b6f89112 --- /dev/null +++ b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts @@ -0,0 +1,410 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import chai from "chai"; +import { BigNumber } from "ethers"; +import { ethers, upgrades } from "hardhat"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; + +import { + AccessControlManager, + ERC20, + IComptroller, + IProtocolShareReserve, + IRewardsDistributor, + MockVenusERC4626Isolated, + VToken, +} from "../../../typechain"; + +const { expect } = chai; +chai.use(smock.matchers); + +describe("VenusERC4626Isolated", () => { + let deployer: SignerWithAddress; + let user: SignerWithAddress; + let vaultOwner: SignerWithAddress; + let venusERC4626Isolated: MockVenusERC4626Isolated; + let asset: FakeContract; + let xvs: FakeContract; + let vToken: FakeContract; + let comptroller: FakeContract; + let accessControlManager: FakeContract; + let rewardDistributor: 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("VToken"); + comptroller = await smock.fake("contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller"); + accessControlManager = await smock.fake("AccessControlManager"); + rewardDistributor = await smock.fake("IRewardsDistributor"); + 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("MockVenusERC4626Isolated"); + + venusERC4626Isolated = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + initializer: "initialize", + }); + await venusERC4626Isolated.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address); + }); + + describe("Initialization", () => { + it("should deploy with correct parameters", async () => { + 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(venusERC4626Isolated.setRewardRecipient(newRecipient)) + .to.emit(venusERC4626Isolated, "RewardRecipientUpdated") + .withArgs(rewardRecipient, newRecipient); + }); + + it("should allow authorized accounts to update maxLoopsLimit", async () => { + const maxLoopsLimit = await venusERC4626Isolated.maxLoopsLimit(); + const newMaxLoopLimit = maxLoopsLimit.add(10); + await expect(venusERC4626Isolated.setMaxLoopsLimit(newMaxLoopLimit)) + .to.emit(venusERC4626Isolated, "MaxLoopsLimitUpdated") + .withArgs(maxLoopsLimit, newMaxLoopLimit); + }); + }); + + describe("Mint Operations", () => { + const mintShares = ethers.utils.parseUnits("10", 18); + 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 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 venusERC4626Isolated.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 with proper vToken accounting", async () => { + const tx = await venusERC4626Isolated.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 venusERC4626Isolated.balanceOf(user.address)).to.equal(actualShares); + }); + + it("should return correct assets amount", async () => { + 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(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(venusERC4626Isolated.connect(user).mint(mintShares, user.address)).to.be.reverted; + }); + + it("should fail mint zero shares", async () => { + await expect(venusERC4626Isolated.connect(user).mint(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "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 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 venusERC4626Isolated.previewDeposit(depositAmount); + + const tx = await venusERC4626Isolated.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 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( + 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(venusERC4626Isolated.connect(user).deposit(ethers.utils.parseEther("1"), user.address)).to.be + .reverted; + }); + + it("should fail deposit zero amount", async () => { + await expect(venusERC4626Isolated.connect(user).deposit(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "ERC4626__ZeroAmount") + .withArgs("deposit"); + }); + }); + + describe("Withdraw Operations", () => { + const depositAmount = ethers.utils.parseEther("10"); + const withdrawAmount = ethers.utils.parseEther("5"); + let expectedShares: 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 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 venusERC4626Isolated.previewWithdraw(withdrawAmount); + + 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"); + 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( + venusERC4626Isolated.connect(user).withdraw(withdrawAmount, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Isolated, "VenusERC4626__VenusError"); + }); + + it("should fail withdraw with no balance", async () => { + 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(venusERC4626Isolated.connect(user).withdraw(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "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 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 venusERC4626Isolated.previewRedeem(redeemShares); + }); + + it("should redeem shares successfully", async () => { + 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"); + 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 venusERC4626Isolated + .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( + venusERC4626Isolated.connect(user).redeem(redeemShares, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Isolated, "VenusERC4626__VenusError"); + }); + + it("should fail redeem zero shares", async () => { + await expect(venusERC4626Isolated.connect(user).redeem(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Isolated, "ERC4626__ZeroAmount") + .withArgs("redeem"); + }); + }); + + describe("Reward Distribution", () => { + const rewardAmount = ethers.utils.parseEther("10"); + + beforeEach(async () => { + comptroller.getRewardDistributors.returns([rewardDistributor.address]); + rewardDistributor.rewardToken.returns(xvs.address); + xvs.balanceOf.whenCalledWith(venusERC4626Isolated.address).returns(rewardAmount); + }); + + describe("When rewardRecipient is EOA", () => { + it("should revert the transaction", async () => { + xvs.transfer.returns(true); + + 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("MockVenusERC4626Isolated"); + venusERC4626Isolated = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + initializer: "initialize", + }); + await venusERC4626Isolated.initialize2( + accessControlManager.address, + rewardRecipientPSR.address, + vaultOwner.address, + ); + comptroller.getRewardDistributors.returns([rewardDistributor.address]); + rewardDistributor.rewardToken.returns(xvs.address); + xvs.balanceOf.whenCalledWith(venusERC4626Isolated.address).returns(rewardAmount); + }); + + it("should claim rewards and update PSR state", async () => { + xvs.transfer.returns(true); + + await expect(venusERC4626Isolated.connect(user).claimRewards()) + .to.emit(venusERC4626Isolated, "ClaimRewards") + .withArgs(rewardAmount, xvs.address); + + expect(rewardDistributor.claimRewardToken).to.have.been.calledWith(venusERC4626Isolated.address, [ + vToken.address, + ]); + + expect(rewardRecipientPSR.updateAssetsState).to.have.been.calledWith(comptroller.address, xvs.address, 2); + }); + }); + }); +}); From 3b57fe37b78fe6e4ec1b340eff0bfde061ca5326 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 18 Jun 2025 14:16:23 +0530 Subject: [PATCH 13/41] refactor: change initialize logic for Core wrapper --- contracts/ERC4626/VenusERC4626Core.sol | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index f61fd699..6110f582 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.25; -import { VenusERC4626 } from "./VenusERC4626.sol"; -import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; +import { VenusERC4626 } from "./Base/VenusERC4626.sol"; +import { IERC20Upgradeable, SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; /// @title VenusERC4626Core /// @notice ERC4626 wrapper for Venus Core Pool vTokens contract VenusERC4626Core is VenusERC4626 { - /// @notice The Venus vToken associated with this ERC4626 vault. - VTokenInterface public vToken; + /// @notice Initializes the VenusERC4626Core contract + /// @param vToken_ The address of the vToken to be wrapped + function initialize(address vToken_) public virtual override initializer { + super.initialize(vToken_); + } /// @inheritdoc VenusERC4626 function claimRewards() external override { @@ -31,15 +34,4 @@ contract VenusERC4626Core is VenusERC4626 { emit ClaimRewards(rewardAmount, xvsAddress); } } - - /// @inheritdoc VenusERC4626 - function _initializeVToken(address vToken_) internal override { - vToken = VTokenInterface(vToken_); - comptroller = IComptroller(address(vToken.comptroller())); - } - - /// @inheritdoc VenusERC4626 - function _getUnderlying(address vToken_) internal view override returns (address) { - return VTokenInterface(vToken_).underlying(); - } } From 75c937bcd78c6d1d5849601c7f18e54d59287ecf Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 18 Jun 2025 14:17:34 +0530 Subject: [PATCH 14/41] fix: minor changes --- contracts/ERC4626/VenusERC4626Factory.sol | 2 +- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 6 +++--- tests/hardhat/ERC4626/VenusERC4626Isolated.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 7aacbc95..c138fa8f 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -202,7 +202,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { ) ) ); - vault.initialize2(address(_accessControlManager), rewardRecipient, 100, owner()); + vault.initialize2(address(_accessControlManager), rewardRecipient, owner()); return ERC4626Upgradeable(address(vault)); } diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 2b81f5d4..69c7a0d7 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -44,9 +44,9 @@ describe("VenusERC4626Factory", () => { // 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("VToken"); - isolatedVToken = await smock.fake("VToken"); - invalidVToken = await smock.fake("VToken"); + coreVToken = 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; diff --git a/tests/hardhat/ERC4626/VenusERC4626Isolated.ts b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts index b6f89112..b76377a0 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Isolated.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts @@ -37,7 +37,7 @@ describe("VenusERC4626Isolated", () => { // 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("VToken"); + vToken = await smock.fake("@venusprotocol/isolated-pools/contracts/VToken.sol:VToken"); comptroller = await smock.fake("contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller"); accessControlManager = await smock.fake("AccessControlManager"); rewardDistributor = await smock.fake("IRewardsDistributor"); From 5e846b4fe416ace1be94bbbdf23d17380685b6be Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 18 Jun 2025 14:29:47 +0530 Subject: [PATCH 15/41] test: tests for VenusERC4626Core --- contracts/test/Mocks/MockVenusERC4626Core.sol | 84 +++++++ contracts/test/VBep20Immutable.sol | 51 ++++ .../{VenusERC4626.ts => VenusERC4626Core.ts} | 222 +++++++++--------- 3 files changed, 245 insertions(+), 112 deletions(-) create mode 100644 contracts/test/Mocks/MockVenusERC4626Core.sol create mode 100644 contracts/test/VBep20Immutable.sol rename tests/hardhat/ERC4626/{VenusERC4626.ts => VenusERC4626Core.ts} (57%) diff --git a/contracts/test/Mocks/MockVenusERC4626Core.sol b/contracts/test/Mocks/MockVenusERC4626Core.sol new file mode 100644 index 00000000..86cbb382 --- /dev/null +++ b/contracts/test/Mocks/MockVenusERC4626Core.sol @@ -0,0 +1,84 @@ +// 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; + + // 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/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/tests/hardhat/ERC4626/VenusERC4626.ts b/tests/hardhat/ERC4626/VenusERC4626Core.ts similarity index 57% rename from tests/hardhat/ERC4626/VenusERC4626.ts rename to tests/hardhat/ERC4626/VenusERC4626Core.ts index 2a7572ef..623e23b2 100644 --- a/tests/hardhat/ERC4626/VenusERC4626.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Core.ts @@ -1,33 +1,30 @@ import { FakeContract, smock } from "@defi-wonderland/smock"; import chai from "chai"; -import { BigNumber } from "ethers"; import { ethers, upgrades } from "hardhat"; import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; import { - AccessControlManager, + AccessControlManagerMock, ERC20, IComptroller, IProtocolShareReserve, - IRewardsDistributor, - MockVenusERC4626, - VToken, + MockVenusERC4626Core, + VBep20Immutable, } from "../../../typechain"; const { expect } = chai; chai.use(smock.matchers); -describe("VenusERC4626", () => { +describe("VenusERC4626Core", () => { let deployer: SignerWithAddress; let user: SignerWithAddress; let vaultOwner: SignerWithAddress; - let venusERC4626: MockVenusERC4626; + let venusERC4626Core: MockVenusERC4626Core; let asset: FakeContract; let xvs: FakeContract; - let vToken: FakeContract; + let vToken: FakeContract; let comptroller: FakeContract; - let accessControlManager: FakeContract; - let rewardDistributor: FakeContract; + let accessControlManager: FakeContract; let rewardRecipient: string; let rewardRecipientPSR: FakeContract; @@ -37,10 +34,9 @@ describe("VenusERC4626", () => { // 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("VToken"); + vToken = await smock.fake("contracts/test/VBep20Immutable.sol:VBep20Immutable"); comptroller = await smock.fake("contracts/ERC4626/Interfaces/IComptroller.sol:IComptroller"); - accessControlManager = await smock.fake("AccessControlManager"); - rewardDistributor = await smock.fake("IRewardsDistributor"); + accessControlManager = await smock.fake("AccessControlManagerMock"); rewardRecipient = deployer.address; rewardRecipientPSR = await smock.fake( "contracts/ERC4626/Interfaces/IProtocolShareReserve.sol:IProtocolShareReserve", @@ -52,45 +48,38 @@ describe("VenusERC4626", () => { vToken.comptroller.returns(comptroller.address); // Deploy and initialize MockVenusERC4626 - const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626"); + const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Core"); - venusERC4626 = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + venusERC4626Core = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", }); - await venusERC4626.initialize2(accessControlManager.address, rewardRecipient, 100, vaultOwner.address); + + await venusERC4626Core.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address); }); 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(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(venusERC4626.setRewardRecipient(newRecipient)) - .to.emit(venusERC4626, "RewardRecipientUpdated") + await expect(venusERC4626Core.setRewardRecipient(newRecipient)) + .to.emit(venusERC4626Core, "RewardRecipientUpdated") .withArgs(rewardRecipient, newRecipient); }); - - it("should allow authorized accounts to update maxLoopsLimit", async () => { - const maxLoopsLimit = await venusERC4626.maxLoopsLimit(); - const newMaxLoopLimit = maxLoopsLimit.add(10); - await expect(venusERC4626.setMaxLoopsLimit(newMaxLoopLimit)) - .to.emit(venusERC4626, "MaxLoopsLimitUpdated") - .withArgs(maxLoopsLimit, newMaxLoopLimit); - }); }); describe("Mint Operations", () => { - const mintShares = ethers.utils.parseUnits("10", 18); + const mintShares = ethers.utils.parseEther("10"); let expectedAssets: ethers.BigNumber; beforeEach(async () => { @@ -99,12 +88,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 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 venusERC4626.previewMint(mintShares); + expectedAssets = await venusERC4626Core.previewMint(mintShares); asset.balanceOf.returnsAtCall(0, ethers.BigNumber.from(0)); asset.balanceOf.returnsAtCall(1, expectedAssets); @@ -113,8 +102,8 @@ describe("VenusERC4626", () => { vToken.balanceOf.returnsAtCall(1, expectedAssets); }); - it("should mint shares successfully with proper vToken accounting", async () => { - const tx = await venusERC4626.connect(user).mint(mintShares, user.address); + 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"); @@ -127,30 +116,30 @@ describe("VenusERC4626", () => { expect(vToken.mint).to.have.been.calledWith(actualAssets); - expect(await venusERC4626.balanceOf(user.address)).to.equal(actualShares); + expect(await venusERC4626Core.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 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(venusERC4626.connect(user).mint(mintShares, user.address)).to.be.revertedWithCustomError( - venusERC4626, + 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(venusERC4626.connect(user).mint(mintShares, user.address)).to.be.reverted; + await expect(venusERC4626Core.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(venusERC4626Core.connect(user).mint(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") .withArgs("mint"); }); }); @@ -172,16 +161,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 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 venusERC4626.previewDeposit(depositAmount); + expectedShares = await venusERC4626Core.previewDeposit(depositAmount); - const tx = await venusERC4626.connect(user).deposit(depositAmount, user.address); + const tx = await venusERC4626Core.connect(user).deposit(depositAmount, user.address); const receipt = await tx.wait(); const depositEvent = receipt.events?.find(e => e.event === "Deposit"); @@ -193,24 +182,24 @@ 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 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( - venusERC4626.connect(user).deposit(ethers.utils.parseEther("50"), user.address), - ).to.be.revertedWithCustomError(venusERC4626, "VenusERC4626__VenusError"); + 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(venusERC4626.connect(user).deposit(ethers.utils.parseEther("1"), user.address)).to.be.reverted; + await expect(venusERC4626Core.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(venusERC4626Core.connect(user).deposit(0, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") .withArgs("deposit"); }); }); @@ -218,7 +207,7 @@ describe("VenusERC4626", () => { describe("Withdraw Operations", () => { const depositAmount = ethers.utils.parseEther("10"); const withdrawAmount = ethers.utils.parseEther("5"); - let expectedShares: BigNumber; + let expectedShares: ethers.BigNumber; beforeEach(async () => { asset.transferFrom.returns(true); @@ -239,18 +228,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 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 venusERC4626.previewWithdraw(withdrawAmount); + expectedShares = await venusERC4626Core.previewWithdraw(withdrawAmount); - const tx = await venusERC4626.connect(user).withdraw(withdrawAmount, user.address, user.address); + 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"); @@ -268,20 +257,20 @@ 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"); + venusERC4626Core.connect(user).withdraw(withdrawAmount, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Core, "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 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(venusERC4626.connect(user).withdraw(0, user.address, user.address)) - .to.be.revertedWithCustomError(venusERC4626, "ERC4626__ZeroAmount") + await expect(venusERC4626Core.connect(user).withdraw(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") .withArgs("withdraw"); }); }); @@ -309,18 +298,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 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 venusERC4626.previewRedeem(redeemShares); + expectedRedeemAssets = await venusERC4626Core.previewRedeem(redeemShares); }); it("should redeem shares successfully", async () => { - const tx = await venusERC4626.connect(user).redeem(redeemShares, user.address, user.address); + 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"); @@ -335,23 +324,22 @@ describe("VenusERC4626", () => { }); it("should return correct assets amount", async () => { - const returnedAssets = await venusERC4626 + 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( - venusERC4626.connect(user).redeem(redeemShares, user.address, user.address), - ).to.be.revertedWithCustomError(venusERC4626, "VenusERC4626__VenusError"); + venusERC4626Core.connect(user).redeem(redeemShares, user.address, user.address), + ).to.be.revertedWithCustomError(venusERC4626Core, "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(venusERC4626Core.connect(user).redeem(0, user.address, user.address)) + .to.be.revertedWithCustomError(venusERC4626Core, "ERC4626__ZeroAmount") .withArgs("redeem"); }); }); @@ -359,48 +347,58 @@ describe("VenusERC4626", () => { describe("Reward Distribution", () => { const rewardAmount = ethers.utils.parseEther("10"); - beforeEach(async () => { - comptroller.getRewardDistributors.returns([rewardDistributor.address]); - rewardDistributor.rewardToken.returns(xvs.address); - xvs.balanceOf.whenCalledWith(venusERC4626.address).returns(rewardAmount); - }); - describe("When rewardRecipient is EOA", () => { - it("should revert the transaction", async () => { + it("should claim rewards and transfer to recipient", async () => { + comptroller.getXVSAddress.returns(xvs.address); + xvs.balanceOf.whenCalledWith(venusERC4626Core.address).returns(rewardAmount); xvs.transfer.returns(true); - await expect(venusERC4626.connect(user).claimRewards()).to.be.reverted; + await expect(venusERC4626Core.claimRewards()) + .to.emit(venusERC4626Core, "ClaimRewards") + .withArgs(rewardAmount, xvs.address); + + expect(comptroller.claimVenus).to.have.been.calledWith(venusERC4626Core.address); + + expect(xvs.transfer).to.have.been.calledWith(rewardRecipient, rewardAmount); + expect(rewardRecipientPSR.updateAssetsState).to.not.have.been.called; }); }); describe("When rewardRecipient is ProtocolShareReserve", () => { + let venusERC4626WithPSR: MockVenusERC4626Core; + beforeEach(async () => { - // Redeploy with PSR as rewardRecipient - const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626"); - venusERC4626 = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { + // Deploy new instance with PSR as reward recipient + const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Core"); + venusERC4626WithPSR = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", }); - await venusERC4626.initialize2( + + await venusERC4626WithPSR.initialize2( accessControlManager.address, rewardRecipientPSR.address, - 100, vaultOwner.address, ); - comptroller.getRewardDistributors.returns([rewardDistributor.address]); - rewardDistributor.rewardToken.returns(xvs.address); - xvs.balanceOf.whenCalledWith(venusERC4626.address).returns(rewardAmount); - }); - it("should claim rewards and update PSR state", async () => { + comptroller.getXVSAddress.returns(xvs.address); + xvs.balanceOf.whenCalledWith(venusERC4626WithPSR.address).returns(rewardAmount); xvs.transfer.returns(true); + }); - await expect(venusERC4626.connect(user).claimRewards()) - .to.emit(venusERC4626, "ClaimRewards") + it("should claim rewards and update PSR state", async () => { + await expect(venusERC4626WithPSR.claimRewards()) + .to.emit(venusERC4626WithPSR, "ClaimRewards") .withArgs(rewardAmount, xvs.address); - expect(rewardDistributor.claimRewardToken).to.have.been.calledWith(venusERC4626.address, [vToken.address]); + expect(comptroller.claimVenus).to.have.been.calledWith(venusERC4626WithPSR.address); + expect(xvs.transfer).to.have.been.calledWith(rewardRecipientPSR.address, rewardAmount); - expect(rewardRecipientPSR.updateAssetsState).to.have.been.calledWith(comptroller.address, xvs.address, 2); + // Verify PSR state update + expect(rewardRecipientPSR.updateAssetsState).to.have.been.calledWith( + comptroller.address, + xvs.address, + 2, // ERC4626_WRAPPER_REWARDS + ); }); }); }); From a71fa9deba0cdafc099b5f64b50d7b27efd93348 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Fri, 20 Jun 2025 11:56:52 +0530 Subject: [PATCH 16/41] add deployment chain id for networks --- deployments/arbitrumone.json | 5 +++++ deployments/arbitrumone/.chainId | 1 + deployments/arbitrumone_addresses.json | 0 deployments/arbitrumsepolia.json | 5 +++++ deployments/arbitrumsepolia/.chainId | 1 + deployments/arbitrumsepolia_addresses.json | 0 deployments/basemainnet.json | 5 +++++ deployments/basemainnet/.chainId | 1 + deployments/basemainnet_addresses.json | 0 deployments/basesepolia.json | 5 +++++ deployments/basesepolia/.chainId | 1 + deployments/basesepolia_addresses.json | 0 deployments/berachainbartio.json | 5 +++++ deployments/berachainbartio/.chainId | 1 + deployments/berachainbartio_addresses.json | 0 deployments/bscmainnet.json | 5 +++++ deployments/bscmainnet/.chainId | 1 + deployments/bscmainnet_addresses.json | 0 deployments/bsctestnet.json | 5 +++++ deployments/bsctestnet/.chainId | 1 + deployments/bsctestnet_addresses.json | 0 deployments/ethereum.json | 5 +++++ deployments/ethereum/.chainId | 1 + deployments/ethereum_addresses.json | 0 deployments/opbnbmainnet.json | 5 +++++ deployments/opbnbmainnet/.chainId | 1 + deployments/opbnbmainnet_addresses.json | 0 deployments/opbnbtestnet.json | 5 +++++ deployments/opbnbtestnet/.chainId | 1 + deployments/opbnbtestnet_addresses.json | 0 deployments/opmainnet.json | 5 +++++ deployments/opmainnet/.chainId | 1 + deployments/opmainnet_addresses.json | 0 deployments/opsepolia.json | 5 +++++ deployments/opsepolia/.chainId | 1 + deployments/opsepolia_addresses.json | 0 deployments/sepolia.json | 5 +++++ deployments/sepolia/.chainId | 1 + deployments/sepolia_addresses.json | 0 deployments/unichainmainnet.json | 5 +++++ deployments/unichainmainnet/.chainId | 1 + deployments/unichainmainnet_addresses.json | 0 deployments/unichainsepolia.json | 5 +++++ deployments/unichainsepolia/.chainId | 1 + deployments/unichainsepolia_addresses.json | 0 deployments/zksyncmainnet.json | 5 +++++ deployments/zksyncmainnet/.chainId | 1 + deployments/zksyncmainnet_addresses.json | 0 deployments/zksyncsepolia.json | 5 +++++ deployments/zksyncsepolia/.chainId | 1 + deployments/zksyncsepolia_addresses.json | 0 51 files changed, 102 insertions(+) create mode 100644 deployments/arbitrumone.json create mode 100644 deployments/arbitrumone/.chainId create mode 100644 deployments/arbitrumone_addresses.json create mode 100644 deployments/arbitrumsepolia.json create mode 100644 deployments/arbitrumsepolia/.chainId create mode 100644 deployments/arbitrumsepolia_addresses.json create mode 100644 deployments/basemainnet.json create mode 100644 deployments/basemainnet/.chainId create mode 100644 deployments/basemainnet_addresses.json create mode 100644 deployments/basesepolia.json create mode 100644 deployments/basesepolia/.chainId create mode 100644 deployments/basesepolia_addresses.json create mode 100644 deployments/berachainbartio.json create mode 100644 deployments/berachainbartio/.chainId create mode 100644 deployments/berachainbartio_addresses.json create mode 100644 deployments/bscmainnet.json create mode 100644 deployments/bscmainnet/.chainId create mode 100644 deployments/bscmainnet_addresses.json create mode 100644 deployments/bsctestnet.json create mode 100644 deployments/bsctestnet/.chainId create mode 100644 deployments/bsctestnet_addresses.json create mode 100644 deployments/ethereum.json create mode 100644 deployments/ethereum/.chainId create mode 100644 deployments/ethereum_addresses.json create mode 100644 deployments/opbnbmainnet.json create mode 100644 deployments/opbnbmainnet/.chainId create mode 100644 deployments/opbnbmainnet_addresses.json create mode 100644 deployments/opbnbtestnet.json create mode 100644 deployments/opbnbtestnet/.chainId create mode 100644 deployments/opbnbtestnet_addresses.json create mode 100644 deployments/opmainnet.json create mode 100644 deployments/opmainnet/.chainId create mode 100644 deployments/opmainnet_addresses.json create mode 100644 deployments/opsepolia.json create mode 100644 deployments/opsepolia/.chainId create mode 100644 deployments/opsepolia_addresses.json create mode 100644 deployments/sepolia.json create mode 100644 deployments/sepolia/.chainId create mode 100644 deployments/sepolia_addresses.json create mode 100644 deployments/unichainmainnet.json create mode 100644 deployments/unichainmainnet/.chainId create mode 100644 deployments/unichainmainnet_addresses.json create mode 100644 deployments/unichainsepolia.json create mode 100644 deployments/unichainsepolia/.chainId create mode 100644 deployments/unichainsepolia_addresses.json create mode 100644 deployments/zksyncmainnet.json create mode 100644 deployments/zksyncmainnet/.chainId create mode 100644 deployments/zksyncmainnet_addresses.json create mode 100644 deployments/zksyncsepolia.json create mode 100644 deployments/zksyncsepolia/.chainId create mode 100644 deployments/zksyncsepolia_addresses.json diff --git a/deployments/arbitrumone.json b/deployments/arbitrumone.json new file mode 100644 index 00000000..4e3a56a6 --- /dev/null +++ b/deployments/arbitrumone.json @@ -0,0 +1,5 @@ +{ + "name": "arbitrumone", + "chainId": "42161", + "contracts": {} +} diff --git a/deployments/arbitrumone/.chainId b/deployments/arbitrumone/.chainId new file mode 100644 index 00000000..7df83ecb --- /dev/null +++ b/deployments/arbitrumone/.chainId @@ -0,0 +1 @@ +42161 \ No newline at end of file diff --git a/deployments/arbitrumone_addresses.json b/deployments/arbitrumone_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/arbitrumsepolia.json b/deployments/arbitrumsepolia.json new file mode 100644 index 00000000..8180a7c9 --- /dev/null +++ b/deployments/arbitrumsepolia.json @@ -0,0 +1,5 @@ +{ + "name": "arbitrumsepolia", + "chainId": "421614", + "contracts": {} +} diff --git a/deployments/arbitrumsepolia/.chainId b/deployments/arbitrumsepolia/.chainId new file mode 100644 index 00000000..71ba4d63 --- /dev/null +++ b/deployments/arbitrumsepolia/.chainId @@ -0,0 +1 @@ +421614 \ No newline at end of file diff --git a/deployments/arbitrumsepolia_addresses.json b/deployments/arbitrumsepolia_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/basemainnet.json b/deployments/basemainnet.json new file mode 100644 index 00000000..c60e2e52 --- /dev/null +++ b/deployments/basemainnet.json @@ -0,0 +1,5 @@ +{ + "name": "basemainnet", + "chainId": "8453", + "contracts": {} +} diff --git a/deployments/basemainnet/.chainId b/deployments/basemainnet/.chainId new file mode 100644 index 00000000..2a0c2638 --- /dev/null +++ b/deployments/basemainnet/.chainId @@ -0,0 +1 @@ +8453 \ No newline at end of file diff --git a/deployments/basemainnet_addresses.json b/deployments/basemainnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/basesepolia.json b/deployments/basesepolia.json new file mode 100644 index 00000000..0e19f007 --- /dev/null +++ b/deployments/basesepolia.json @@ -0,0 +1,5 @@ +{ + "name": "basesepolia", + "chainId": "84532", + "contracts": {} +} diff --git a/deployments/basesepolia/.chainId b/deployments/basesepolia/.chainId new file mode 100644 index 00000000..667f99da --- /dev/null +++ b/deployments/basesepolia/.chainId @@ -0,0 +1 @@ +84532 \ No newline at end of file diff --git a/deployments/basesepolia_addresses.json b/deployments/basesepolia_addresses.json new file mode 100644 index 00000000..e69de29b 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..e69de29b diff --git a/deployments/bscmainnet.json b/deployments/bscmainnet.json new file mode 100644 index 00000000..b1a740e8 --- /dev/null +++ b/deployments/bscmainnet.json @@ -0,0 +1,5 @@ +{ + "name": "bscmainnet", + "chainId": "56", + "contracts": {} +} diff --git a/deployments/bscmainnet/.chainId b/deployments/bscmainnet/.chainId new file mode 100644 index 00000000..2ebc6516 --- /dev/null +++ b/deployments/bscmainnet/.chainId @@ -0,0 +1 @@ +56 \ No newline at end of file diff --git a/deployments/bscmainnet_addresses.json b/deployments/bscmainnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/bsctestnet.json b/deployments/bsctestnet.json new file mode 100644 index 00000000..eed803e9 --- /dev/null +++ b/deployments/bsctestnet.json @@ -0,0 +1,5 @@ +{ + "name": "bsctestnet", + "chainId": "97", + "contracts": {} +} diff --git a/deployments/bsctestnet/.chainId b/deployments/bsctestnet/.chainId new file mode 100644 index 00000000..c4fbb1cf --- /dev/null +++ b/deployments/bsctestnet/.chainId @@ -0,0 +1 @@ +97 \ No newline at end of file diff --git a/deployments/bsctestnet_addresses.json b/deployments/bsctestnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/ethereum.json b/deployments/ethereum.json new file mode 100644 index 00000000..dd24474d --- /dev/null +++ b/deployments/ethereum.json @@ -0,0 +1,5 @@ +{ + "name": "ethereum", + "chainId": "1", + "contracts": {} +} diff --git a/deployments/ethereum/.chainId b/deployments/ethereum/.chainId new file mode 100644 index 00000000..56a6051c --- /dev/null +++ b/deployments/ethereum/.chainId @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/deployments/ethereum_addresses.json b/deployments/ethereum_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/opbnbmainnet.json b/deployments/opbnbmainnet.json new file mode 100644 index 00000000..6a503c8d --- /dev/null +++ b/deployments/opbnbmainnet.json @@ -0,0 +1,5 @@ +{ + "name": "opbnbmainnet", + "chainId": "204", + "contracts": {} +} diff --git a/deployments/opbnbmainnet/.chainId b/deployments/opbnbmainnet/.chainId new file mode 100644 index 00000000..cbd6012b --- /dev/null +++ b/deployments/opbnbmainnet/.chainId @@ -0,0 +1 @@ +204 \ No newline at end of file diff --git a/deployments/opbnbmainnet_addresses.json b/deployments/opbnbmainnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/opbnbtestnet.json b/deployments/opbnbtestnet.json new file mode 100644 index 00000000..0c7a9865 --- /dev/null +++ b/deployments/opbnbtestnet.json @@ -0,0 +1,5 @@ +{ + "name": "opbnbtestnet", + "chainId": "5611", + "contracts": {} +} diff --git a/deployments/opbnbtestnet/.chainId b/deployments/opbnbtestnet/.chainId new file mode 100644 index 00000000..6d5a04e4 --- /dev/null +++ b/deployments/opbnbtestnet/.chainId @@ -0,0 +1 @@ +5611 \ No newline at end of file diff --git a/deployments/opbnbtestnet_addresses.json b/deployments/opbnbtestnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/opmainnet.json b/deployments/opmainnet.json new file mode 100644 index 00000000..58d00eb2 --- /dev/null +++ b/deployments/opmainnet.json @@ -0,0 +1,5 @@ +{ + "name": "opmainnet", + "chainId": "10", + "contracts": {} +} diff --git a/deployments/opmainnet/.chainId b/deployments/opmainnet/.chainId new file mode 100644 index 00000000..9a037142 --- /dev/null +++ b/deployments/opmainnet/.chainId @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/deployments/opmainnet_addresses.json b/deployments/opmainnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/opsepolia.json b/deployments/opsepolia.json new file mode 100644 index 00000000..9dbc8be4 --- /dev/null +++ b/deployments/opsepolia.json @@ -0,0 +1,5 @@ +{ + "name": "opsepolia", + "chainId": "11155420", + "contracts": {} +} diff --git a/deployments/opsepolia/.chainId b/deployments/opsepolia/.chainId new file mode 100644 index 00000000..03f37de8 --- /dev/null +++ b/deployments/opsepolia/.chainId @@ -0,0 +1 @@ +11155420 \ No newline at end of file diff --git a/deployments/opsepolia_addresses.json b/deployments/opsepolia_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/sepolia.json b/deployments/sepolia.json new file mode 100644 index 00000000..21a16fd4 --- /dev/null +++ b/deployments/sepolia.json @@ -0,0 +1,5 @@ +{ + "name": "sepolia", + "chainId": "11155111", + "contracts": {} +} diff --git a/deployments/sepolia/.chainId b/deployments/sepolia/.chainId new file mode 100644 index 00000000..bd8d1cd4 --- /dev/null +++ b/deployments/sepolia/.chainId @@ -0,0 +1 @@ +11155111 \ No newline at end of file diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/unichainmainnet.json b/deployments/unichainmainnet.json new file mode 100644 index 00000000..a722ac12 --- /dev/null +++ b/deployments/unichainmainnet.json @@ -0,0 +1,5 @@ +{ + "name": "unichainmainnet", + "chainId": "130", + "contracts": {} +} diff --git a/deployments/unichainmainnet/.chainId b/deployments/unichainmainnet/.chainId new file mode 100644 index 00000000..8306ec15 --- /dev/null +++ b/deployments/unichainmainnet/.chainId @@ -0,0 +1 @@ +130 \ No newline at end of file diff --git a/deployments/unichainmainnet_addresses.json b/deployments/unichainmainnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/unichainsepolia.json b/deployments/unichainsepolia.json new file mode 100644 index 00000000..fbca591f --- /dev/null +++ b/deployments/unichainsepolia.json @@ -0,0 +1,5 @@ +{ + "name": "unichainsepolia", + "chainId": "1301", + "contracts": {} +} diff --git a/deployments/unichainsepolia/.chainId b/deployments/unichainsepolia/.chainId new file mode 100644 index 00000000..568efcad --- /dev/null +++ b/deployments/unichainsepolia/.chainId @@ -0,0 +1 @@ +1301 \ No newline at end of file diff --git a/deployments/unichainsepolia_addresses.json b/deployments/unichainsepolia_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/zksyncmainnet.json b/deployments/zksyncmainnet.json new file mode 100644 index 00000000..2443751f --- /dev/null +++ b/deployments/zksyncmainnet.json @@ -0,0 +1,5 @@ +{ + "name": "zksyncmainnet", + "chainId": "324", + "contracts": {} +} diff --git a/deployments/zksyncmainnet/.chainId b/deployments/zksyncmainnet/.chainId new file mode 100644 index 00000000..5f1a9f39 --- /dev/null +++ b/deployments/zksyncmainnet/.chainId @@ -0,0 +1 @@ +324 \ No newline at end of file diff --git a/deployments/zksyncmainnet_addresses.json b/deployments/zksyncmainnet_addresses.json new file mode 100644 index 00000000..e69de29b diff --git a/deployments/zksyncsepolia.json b/deployments/zksyncsepolia.json new file mode 100644 index 00000000..99f2fb04 --- /dev/null +++ b/deployments/zksyncsepolia.json @@ -0,0 +1,5 @@ +{ + "name": "zksyncsepolia", + "chainId": "300", + "contracts": {} +} diff --git a/deployments/zksyncsepolia/.chainId b/deployments/zksyncsepolia/.chainId new file mode 100644 index 00000000..f1efb205 --- /dev/null +++ b/deployments/zksyncsepolia/.chainId @@ -0,0 +1 @@ +300 \ No newline at end of file diff --git a/deployments/zksyncsepolia_addresses.json b/deployments/zksyncsepolia_addresses.json new file mode 100644 index 00000000..e69de29b From 2f95779dcb368ebfabe3c9a38bde307fe9306d7e Mon Sep 17 00:00:00 2001 From: Debugger022 <104391977+Debugger022@users.noreply.github.com> Date: Fri, 20 Jun 2025 06:29:40 +0000 Subject: [PATCH 17/41] feat: updating deployment files --- deployments/arbitrumone_addresses.json | 5 +++++ deployments/arbitrumsepolia_addresses.json | 5 +++++ deployments/basemainnet_addresses.json | 5 +++++ deployments/basesepolia_addresses.json | 5 +++++ deployments/berachainbartio_addresses.json | 5 +++++ deployments/bscmainnet_addresses.json | 5 +++++ deployments/bsctestnet_addresses.json | 5 +++++ deployments/ethereum_addresses.json | 5 +++++ deployments/opbnbmainnet_addresses.json | 5 +++++ deployments/opbnbtestnet_addresses.json | 5 +++++ deployments/opmainnet_addresses.json | 5 +++++ deployments/opsepolia_addresses.json | 5 +++++ deployments/sepolia_addresses.json | 5 +++++ deployments/unichainmainnet_addresses.json | 5 +++++ deployments/unichainsepolia_addresses.json | 5 +++++ deployments/zksyncmainnet_addresses.json | 5 +++++ deployments/zksyncsepolia_addresses.json | 5 +++++ 17 files changed, 85 insertions(+) diff --git a/deployments/arbitrumone_addresses.json b/deployments/arbitrumone_addresses.json index e69de29b..e65d7090 100644 --- a/deployments/arbitrumone_addresses.json +++ b/deployments/arbitrumone_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "arbitrumone", + "chainId": "42161", + "addresses": {} +} diff --git a/deployments/arbitrumsepolia_addresses.json b/deployments/arbitrumsepolia_addresses.json index e69de29b..8d639415 100644 --- a/deployments/arbitrumsepolia_addresses.json +++ b/deployments/arbitrumsepolia_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "arbitrumsepolia", + "chainId": "421614", + "addresses": {} +} diff --git a/deployments/basemainnet_addresses.json b/deployments/basemainnet_addresses.json index e69de29b..b625360e 100644 --- a/deployments/basemainnet_addresses.json +++ b/deployments/basemainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "basemainnet", + "chainId": "8453", + "addresses": {} +} diff --git a/deployments/basesepolia_addresses.json b/deployments/basesepolia_addresses.json index e69de29b..428a835c 100644 --- a/deployments/basesepolia_addresses.json +++ b/deployments/basesepolia_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "basesepolia", + "chainId": "84532", + "addresses": {} +} diff --git a/deployments/berachainbartio_addresses.json b/deployments/berachainbartio_addresses.json index e69de29b..737e8adf 100644 --- a/deployments/berachainbartio_addresses.json +++ b/deployments/berachainbartio_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "berachainbartio", + "chainId": "80084", + "addresses": {} +} diff --git a/deployments/bscmainnet_addresses.json b/deployments/bscmainnet_addresses.json index e69de29b..8d0acc3d 100644 --- a/deployments/bscmainnet_addresses.json +++ b/deployments/bscmainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "bscmainnet", + "chainId": "56", + "addresses": {} +} diff --git a/deployments/bsctestnet_addresses.json b/deployments/bsctestnet_addresses.json index e69de29b..c0beb7d1 100644 --- a/deployments/bsctestnet_addresses.json +++ b/deployments/bsctestnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "bsctestnet", + "chainId": "97", + "addresses": {} +} diff --git a/deployments/ethereum_addresses.json b/deployments/ethereum_addresses.json index e69de29b..de562ee9 100644 --- a/deployments/ethereum_addresses.json +++ b/deployments/ethereum_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "ethereum", + "chainId": "1", + "addresses": {} +} diff --git a/deployments/opbnbmainnet_addresses.json b/deployments/opbnbmainnet_addresses.json index e69de29b..cf3419c4 100644 --- a/deployments/opbnbmainnet_addresses.json +++ b/deployments/opbnbmainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "opbnbmainnet", + "chainId": "204", + "addresses": {} +} diff --git a/deployments/opbnbtestnet_addresses.json b/deployments/opbnbtestnet_addresses.json index e69de29b..fdd8ea75 100644 --- a/deployments/opbnbtestnet_addresses.json +++ b/deployments/opbnbtestnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "opbnbtestnet", + "chainId": "5611", + "addresses": {} +} diff --git a/deployments/opmainnet_addresses.json b/deployments/opmainnet_addresses.json index e69de29b..afe9164b 100644 --- a/deployments/opmainnet_addresses.json +++ b/deployments/opmainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "opmainnet", + "chainId": "10", + "addresses": {} +} diff --git a/deployments/opsepolia_addresses.json b/deployments/opsepolia_addresses.json index e69de29b..baeb5917 100644 --- a/deployments/opsepolia_addresses.json +++ b/deployments/opsepolia_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "opsepolia", + "chainId": "11155420", + "addresses": {} +} diff --git a/deployments/sepolia_addresses.json b/deployments/sepolia_addresses.json index e69de29b..96d56ddd 100644 --- a/deployments/sepolia_addresses.json +++ b/deployments/sepolia_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "sepolia", + "chainId": "11155111", + "addresses": {} +} diff --git a/deployments/unichainmainnet_addresses.json b/deployments/unichainmainnet_addresses.json index e69de29b..cb744299 100644 --- a/deployments/unichainmainnet_addresses.json +++ b/deployments/unichainmainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "unichainmainnet", + "chainId": "130", + "addresses": {} +} diff --git a/deployments/unichainsepolia_addresses.json b/deployments/unichainsepolia_addresses.json index e69de29b..466a162e 100644 --- a/deployments/unichainsepolia_addresses.json +++ b/deployments/unichainsepolia_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "unichainsepolia", + "chainId": "1301", + "addresses": {} +} diff --git a/deployments/zksyncmainnet_addresses.json b/deployments/zksyncmainnet_addresses.json index e69de29b..6b0d84de 100644 --- a/deployments/zksyncmainnet_addresses.json +++ b/deployments/zksyncmainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "zksyncmainnet", + "chainId": "324", + "addresses": {} +} diff --git a/deployments/zksyncsepolia_addresses.json b/deployments/zksyncsepolia_addresses.json index e69de29b..4d4a0b2e 100644 --- a/deployments/zksyncsepolia_addresses.json +++ b/deployments/zksyncsepolia_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "zksyncsepolia", + "chainId": "300", + "addresses": {} +} From e8fd66581b68c53f38cb316d69af05207f9481c5 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Fri, 20 Jun 2025 14:16:42 +0530 Subject: [PATCH 18/41] refactor: align solidity version and some minor changes --- contracts/ERC4626/Base/VenusERC4626.sol | 6 +++++- .../ERC4626/Interfaces/IProtocolShareReserve.sol | 2 +- contracts/ERC4626/Interfaces/VTokenInterface.sol | 2 +- contracts/ERC4626/VenusERC4626Core.sol | 2 +- contracts/ERC4626/VenusERC4626Factory.sol | 13 ++++++------- contracts/ERC4626/VenusERC4626Isolated.sol | 2 +- contracts/test/Mocks/MockVenusERC4626Core.sol | 2 +- contracts/test/Mocks/MockVenusERC4626Isolated.sol | 2 +- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index 669486ef..be71061a 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -154,6 +154,8 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr return actualShares; } + /// @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 virtual override nonReentrant returns (uint256) { ensureNonzeroAddress(receiver); @@ -174,6 +176,8 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr return assets; } + /// @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, 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 index e7d9bda0..f0615def 100644 --- a/contracts/ERC4626/Interfaces/VTokenInterface.sol +++ b/contracts/ERC4626/Interfaces/VTokenInterface.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { IComptroller } from "./IComptroller.sol"; diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index 6110f582..7380231c 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { VenusERC4626 } from "./Base/VenusERC4626.sol"; import { IERC20Upgradeable, SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index c138fa8f..6fbf0b1a 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.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"; @@ -14,7 +13,7 @@ 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 as IsolatedVTokenInterface } from "@venusprotocol/isolated-pools/contracts/VTokenInterfaces.sol"; +import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; /// @title ERC4626Factory /// @notice Factory contract for deploying ERC4626 vaults (core and isolated) with beacon proxies. @@ -22,7 +21,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { // --- Constants --- /// @notice Salt used to deterministically deploy isolated pool vaults - bytes32 public constant ISOLATED_SALT = keccak256("Venus-Isolated-ERC4626"); + 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"); @@ -67,7 +66,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Thrown when the vToken provided is not valid (either unlisted or not part of the pool registry) error InvalidVToken(); - /// @notice Constructor (disable initializer for upgradeable contract) + /// @notice Constructor /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -146,8 +145,8 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { if (!listed) revert InvalidVToken(); vault = _deployCoreVault(vToken); } else { - address underlying = IsolatedVTokenInterface(vToken).underlying(); - address comptroller = address(IsolatedVTokenInterface(vToken).comptroller()); + address underlying = VTokenInterface(vToken).underlying(); + address comptroller = address(VTokenInterface(vToken).comptroller()); if (vToken != poolRegistry.getVTokenForAsset(comptroller, underlying)) { revert InvalidVToken(); } diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 38e4a047..c98a7f80 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { VenusERC4626 } from "./Base/VenusERC4626.sol"; import { IComptroller } from "./Interfaces/IComptroller.sol"; diff --git a/contracts/test/Mocks/MockVenusERC4626Core.sol b/contracts/test/Mocks/MockVenusERC4626Core.sol index 86cbb382..9f808abd 100644 --- a/contracts/test/Mocks/MockVenusERC4626Core.sol +++ b/contracts/test/Mocks/MockVenusERC4626Core.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { VenusERC4626Core } from "../../ERC4626/VenusERC4626Core.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; diff --git a/contracts/test/Mocks/MockVenusERC4626Isolated.sol b/contracts/test/Mocks/MockVenusERC4626Isolated.sol index c8625490..8c4e808d 100644 --- a/contracts/test/Mocks/MockVenusERC4626Isolated.sol +++ b/contracts/test/Mocks/MockVenusERC4626Isolated.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.25; +pragma solidity 0.8.25; import { VenusERC4626Isolated } from "../../ERC4626/VenusERC4626Isolated.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; From f289c155a43d11d52b94d99e388aa8bed8f47295 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 26 Jun 2025 12:06:32 +0530 Subject: [PATCH 19/41] fix: detailed natspec comments --- contracts/ERC4626/Base/VenusERC4626.sol | 98 +++++++++++++++++++------ 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index be71061a..0766582f 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -16,7 +16,7 @@ import { VTokenInterface } from "../Interfaces/VTokenInterface.sol"; uint256 constant EXP_SCALE = 1e18; /// @title VenusERC4626 -/// @notice Abstract ERC4626 wrapper for Venus vTokens +/// @notice Abstract ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable { using MathUpgradeable for uint256; using SafeERC20Upgradeable for ERC20Upgradeable; @@ -74,11 +74,14 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @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 with the VToken address - /// @param vToken_ The VToken associated with the vault + /// @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 virtual initializer { ensureNonzeroAddress(vToken_); @@ -93,13 +96,16 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @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 virtual { _checkAccessAllowed("setRewardRecipient(address)"); _setRewardRecipient(newRecipient); } - /// @notice Sweeps tokens from the contract to the owner - /// @param token Address of the token to sweep + /// @notice Sweeps the input token address tokens from the contract and sends them to the owner + /// @param token Address of the token + /// @custom:event SweepToken emits on success + /// @custom:access Only owner function sweepToken(IERC20Upgradeable token) external virtual onlyOwner { uint256 balance = token.balanceOf(address(this)); @@ -114,10 +120,10 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @dev Must be implemented by child contracts function claimRewards() external virtual; - /// @notice Second initialization function to complete vault configuration + /// @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_ Address that will receive rewards - /// @param vaultOwner_ Owner of the vault + /// @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 initialize2( address accessControlManager_, address rewardRecipient_, @@ -227,12 +233,16 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr return actualAssets; } - /// @inheritdoc ERC4626Upgradeable + /// @notice Returns the total amount of assets deposited + /// @return Amount of assets deposited function totalAssets() public view virtual override returns (uint256) { return (vToken.balanceOf(address(this)) * vToken.exchangeRateStored()) / EXP_SCALE; } - /// @inheritdoc ERC4626Upgradeable + /// @notice Returns the maximum deposit allowed based on Venus supply caps. + /// @dev If minting is paused or the supply cap is reached, returns 0. + /// @param /*account*/ The address of the account. + /// @return The maximum amount of assets that can be deposited. function maxDeposit(address /*account*/) public view virtual override returns (uint256) { if (comptroller.actionPaused(address(vToken), Action.MINT)) { return 0; @@ -243,12 +253,18 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr return supplyCap > totalSupply_ ? supplyCap - totalSupply_ : 0; } - /// @inheritdoc ERC4626Upgradeable + /// @notice Returns the maximum amount of shares that can be minted. + /// @dev This is derived from the maximum deposit amount converted to shares. + /// @param /*account*/ The address of the account. + /// @return The maximum number of shares that can be minted. function maxMint(address /*account*/) public view virtual override returns (uint256) { return convertToShares(maxDeposit(address(0))); } - /// @inheritdoc ERC4626Upgradeable + /// @notice Returns the maximum amount that can be withdrawn. + /// @dev The withdrawable amount is limited by the available cash in the vault. + /// @param receiver The address of the account withdrawing. + /// @return The maximum amount of assets that can be withdrawn. function maxWithdraw(address receiver) public view virtual override returns (uint256) { if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { return 0; @@ -266,7 +282,10 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr } } - /// @inheritdoc ERC4626Upgradeable + /// @notice Returns the maximum amount of shares that can be redeemed. + /// @dev Redemption is limited by the available cash in the vault. + /// @param receiver The address of the account redeeming. + /// @return The maximum number of shares that can be redeemed. function maxRedeem(address receiver) public view virtual override returns (uint256) { if (comptroller.actionPaused(address(vToken), Action.REDEEM)) { return 0; @@ -284,11 +303,15 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr } } - /// @notice Internal function to redeem shares + /// @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 virtual returns (uint256) { IERC20Upgradeable token = IERC20Upgradeable(asset()); uint256 balanceBefore = token.balanceOf(address(this)); + // Calculate the amount of vTokens equivalent to the amount of shares, rounding it down uint256 vTokens = shares.mulDiv( vToken.balanceOf(address(this)), totalSupply() + 10 ** _decimalsOffset(), @@ -302,10 +325,16 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr uint256 balanceAfter = token.balanceOf(address(this)); + // Return the amount of assets that was *actually* transferred in return balanceAfter - balanceBefore; } - /// @notice Internal function to handle withdrawals + /// @notice Redeems underlying assets before withdrawing from the vault. + /// @dev Calls `redeemUnderlying` on the vToken contract. Reverts on error. + /// @param assets The amount of underlying assets to redeem. + /// @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 virtual returns (uint256 actualAssets, uint256 actualShares) { IERC20Upgradeable token = IERC20Upgradeable(asset()); uint256 balanceBefore = token.balanceOf(address(this)); @@ -316,12 +345,15 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr revert VenusERC4626__VenusError(errorCode); } + // Return the amount of assets *actually* transferred in actualAssets = token.balanceOf(address(this)) - balanceBefore; uint256 actualVTokens = vTokenBalanceBefore - vToken.balanceOf(address(this)); if (actualVTokens == 0) { revert ERC4626__ZeroAmount("actualVTokens at _beforeWithdraw"); } + + // Return the shares equivalent to the burned vTokens actualShares = actualVTokens.mulDiv( totalSupply() + 10 ** _decimalsOffset(), vTokenBalanceBefore, @@ -329,7 +361,9 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr ); } - /// @notice Internal function to mint vTokens + /// @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 virtual { ERC20Upgradeable(asset()).safeApprove(address(vToken), assets); uint256 errorCode = vToken.mint(assets); @@ -338,7 +372,10 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr } } - /// @notice Internal function to set reward recipient + /// @notice Sets a new reward recipient address + /// @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 virtual { ensureNonzeroAddress(newRecipient); @@ -346,44 +383,61 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr rewardRecipient = newRecipient; } + /// @notice Deposits the assets into the VToken and calculates the shares to mint based on the + /// 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 virtual override { + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override { + // 1. Track pre-transfer balances uint256 assetBalanceBefore = IERC20Upgradeable(asset()).balanceOf(address(this)); uint256 vTokenBalanceBefore = vToken.balanceOf(address(this)); + // 2. Perform asset transfer (original OZ 4626 logic) SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(asset()), caller, address(this), assets); + // 3. Calculate actual assets received (protects against fee-on-transfer) uint256 assetsReceived = IERC20Upgradeable(asset()).balanceOf(address(this)) - assetBalanceBefore; + + // 4. Mint vTokens with received assets _mintVTokens(assetsReceived); + // 5. Verify actual vTokens received uint256 vTokensReceived = vToken.balanceOf(address(this)) - vTokenBalanceBefore; if (vTokensReceived == 0) { revert ERC4626__ZeroAmount("vTokensReceived at _deposit"); } uint256 actualAssetsValue = (vTokensReceived * vToken.exchangeRateStored()) / EXP_SCALE; + // 6. Recalculate shares based on actual received value + // This is the same operation performed by previewDeposit, adjusting the total assets uint256 actualShares = actualAssetsValue.mulDiv( totalSupply() + 10 ** _decimalsOffset(), - totalAssets() + 1 - actualAssetsValue, + totalAssets() + 1 - actualAssetsValue, // remove the new assets deposited to the VToken in this operation MathUpgradeable.Rounding.Down ); + // 7. Mint the corrected share amount _mint(receiver, actualShares); emit Deposit(caller, receiver, assets, actualShares); } - /// @inheritdoc ERC4626Upgradeable + /// @notice Override `_decimalsOffset` to normalize decimals to 18 for all VenusERC4626 vaults. + /// @return Gap between 18 and the decimals of the asset token function _decimalsOffset() internal view virtual override returns (uint8) { return 18 - ERC20Upgradeable(asset()).decimals(); } - /// @notice Generates vault name + /// @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 virtual returns (string memory) { return string(abi.encodePacked("ERC4626-Wrapped Venus ", asset_.name())); } - /// @notice Generates vault symbol + /// @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 virtual returns (string memory) { return string(abi.encodePacked("v4626", asset_.symbol())); } From f65eb096f9721da3ef7f98ed35cbd397f52f5dfd Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 26 Jun 2025 14:18:16 +0530 Subject: [PATCH 20/41] fix: correct storage layout --- contracts/ERC4626/VenusERC4626Factory.sol | 52 ++++++++++++----------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 6fbf0b1a..9a8cf7b7 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -18,33 +18,31 @@ import { VTokenInterface } from "./Interfaces/VTokenInterface.sol"; /// @title ERC4626Factory /// @notice Factory contract for deploying ERC4626 vaults (core and isolated) with beacon proxies. contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { - // --- Constants --- - /// @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"); - - // --- State Variables --- - /// @notice Beacon for isolated vaults + /// @dev Previously named `beacon` UpgradeableBeacon public isolatedBeacon; - /// @notice Beacon for core vaults - UpgradeableBeacon public coreBeacon; - /// @notice PoolRegistry contract to validate isolated pool vTokens PoolRegistryInterface public poolRegistry; - /// @notice Comptroller for core pool validation - IComptroller public coreComptroller; - /// @notice Address to which rewards will be distributed address public rewardRecipient; - /// @notice Mapping from vToken to deployed ERC4626 vault - mapping(address => ERC4626Upgradeable) public vaults; + /// @notice Mapping from vToken to deployed ERC4626 vaults + mapping(address vToken => ERC4626Upgradeable vault) public createdVaults; + + /// @notice Salt used to deterministically deploy core pool vaults + bytes32 public constant CORE_SALT = keccak256("Venus-Core-ERC4626"); + + /// @notice Beacon for core vaults + UpgradeableBeacon public coreBeacon; + + /// @notice Comptroller for core pool validation + IComptroller public coreComptroller; /// @notice Mapping indicating whether a vault belongs to core pool mapping(address => bool) public isCoreVault; @@ -60,11 +58,11 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @param newRecipient The new reward recipient address event RewardRecipientUpdated(address indexed oldRecipient, address indexed newRecipient); - /// @notice Thrown when a vault already exists for the given vToken - error VaultAlreadyExists(); + /// @notice Thrown when the provided vToken is not registered in PoolRegistry + error VenusERC4626Factory__InvalidVToken(); - /// @notice Thrown when the vToken provided is not valid (either unlisted or not part of the pool registry) - error InvalidVToken(); + /// @notice Thrown when a VenusERC4626 already exists for the provided vToken + error VenusERC4626Factory__ERC4626AlreadyExists(); /// @notice Constructor /// @custom:oz-upgrades-unsafe-allow constructor @@ -101,9 +99,15 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { coreBeacon = new UpgradeableBeacon(coreImplementation); poolRegistry = PoolRegistryInterface(poolRegistry_); - coreComptroller = IComptroller(coreComptroller_); rewardRecipient = rewardRecipient_; + if (coreComptroller_ != address(0)) { + coreComptroller = IComptroller(coreComptroller_); + } else { + coreComptroller = IComptroller(address(0)); + } + + // The owner of the beacon will initially be the owner of the factory isolatedBeacon.transferOwnership(owner()); coreBeacon.transferOwnership(owner()); } @@ -138,23 +142,23 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @custom:error InvalidVToken if the vToken is invalid or unlisted /// @custom:event Emits VaultCreated event on successful deployment function createERC4626(address vToken, bool isCore) external returns (ERC4626Upgradeable vault) { - if (address(vaults[vToken]) != address(0)) revert VaultAlreadyExists(); + if (address(createdVaults[vToken]) != address(0)) revert VenusERC4626Factory__ERC4626AlreadyExists(); if (isCore) { (bool listed, ) = coreComptroller.markets(vToken); - if (!listed) revert InvalidVToken(); + if (!listed) revert VenusERC4626Factory__InvalidVToken(); vault = _deployCoreVault(vToken); } else { address underlying = VTokenInterface(vToken).underlying(); address comptroller = address(VTokenInterface(vToken).comptroller()); if (vToken != poolRegistry.getVTokenForAsset(comptroller, underlying)) { - revert InvalidVToken(); + revert VenusERC4626Factory__InvalidVToken(); } vault = _deployIsolatedVault(vToken); } - vaults[vToken] = vault; + createdVaults[vToken] = vault; isCoreVault[vToken] = isCore; emit VaultCreated(vToken, address(vault), isCore); } From 98e05bee42caeeabaa38b0e13df823415fd21128 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 26 Jun 2025 15:59:31 +0530 Subject: [PATCH 21/41] refactor: remove isCore param --- contracts/ERC4626/VenusERC4626Factory.sol | 30 +++++++++++++---- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 34 ++++++++++---------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 9a8cf7b7..0df7cc74 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -136,17 +136,17 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Creates an ERC4626 vault for the given vToken /// @param vToken Address of the vToken - /// @param isCore Indicates if the vToken is part of the core pool /// @return vault The deployed ERC4626 vault /// @custom:error VaultAlreadyExists if a vault already exists for the vToken /// @custom:error InvalidVToken if the vToken is invalid or unlisted /// @custom:event Emits VaultCreated event on successful deployment - function createERC4626(address vToken, bool isCore) external returns (ERC4626Upgradeable vault) { + function createERC4626(address vToken) external returns (ERC4626Upgradeable vault) { if (address(createdVaults[vToken]) != address(0)) revert VenusERC4626Factory__ERC4626AlreadyExists(); + bool isCore = _isCoreVToken(vToken); + isCoreVault[vToken] = isCore; + if (isCore) { - (bool listed, ) = coreComptroller.markets(vToken); - if (!listed) revert VenusERC4626Factory__InvalidVToken(); vault = _deployCoreVault(vToken); } else { address underlying = VTokenInterface(vToken).underlying(); @@ -159,15 +159,15 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { } createdVaults[vToken] = vault; - isCoreVault[vToken] = isCore; emit VaultCreated(vToken, address(vault), isCore); } /// @notice Computes the deterministic vault address for a given vToken /// @param vToken Address of the vToken - /// @param isCore Indicates if the vault is for core pool /// @return The computed vault address - function computeVaultAddress(address vToken, bool isCore) public view returns (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 @@ -193,6 +193,22 @@ 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 (address(coreComptroller) == address(0)) { + return false; + } + + try coreComptroller.markets(vToken) returns (bool listed, uint256) { + return listed; + } catch { + return false; + } + } + /// @dev Deploys a new isolated pool vault /// @param vToken Address of the isolated pool vToken /// @return The deployed vault as ERC4626Upgradeable diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 69c7a0d7..dff6cf8e 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -113,7 +113,7 @@ describe("VenusERC4626Factory", () => { describe("Vault Creation", () => { it("should create core vault and emit event", async () => { - const tx = await factory.createERC4626(coreVToken.address, true); + const tx = await factory.createERC4626(coreVToken.address); const receipt = await tx.wait(); const event = receipt.events?.find(e => e.event === "VaultCreated"); @@ -122,7 +122,7 @@ describe("VenusERC4626Factory", () => { }); it("should create isolated vault and emit event", async () => { - const tx = await factory.createERC4626(isolatedVToken.address, false); + const tx = await factory.createERC4626(isolatedVToken.address); const receipt = await tx.wait(); const event = receipt.events?.find(e => e.event === "VaultCreated"); @@ -132,40 +132,40 @@ describe("VenusERC4626Factory", () => { it("should revert for invalid core vToken", async () => { coreComptroller.markets.whenCalledWith(invalidVToken.address).returns([false, 0]); - await expect(factory.createERC4626(invalidVToken.address, true)).to.be.revertedWithCustomError( + await expect(factory.createERC4626(invalidVToken.address)).to.be.revertedWithCustomError( factory, - "InvalidVToken", + "VenusERC4626Factory__InvalidVToken", ); }); it("should revert for invalid isolated vToken", async () => { poolRegistry.getVTokenForAsset.returns(constants.AddressZero); - await expect(factory.createERC4626(invalidVToken.address, false)).to.be.revertedWithCustomError( + await expect(factory.createERC4626(invalidVToken.address)).to.be.revertedWithCustomError( factory, - "InvalidVToken", + "VenusERC4626Factory__InvalidVToken", ); }); it("should revert for duplicate vToken", async () => { - await factory.createERC4626(coreVToken.address, true); - await expect(factory.createERC4626(coreVToken.address, true)).to.be.revertedWithCustomError( + await factory.createERC4626(coreVToken.address); + await expect(factory.createERC4626(coreVToken.address)).to.be.revertedWithCustomError( factory, - "VaultAlreadyExists", + "VenusERC4626Factory__ERC4626AlreadyExists", ); }); }); describe("CREATE2 Functionality", () => { it("should deploy core vault to predicted address", async () => { - const predicted = await factory.computeVaultAddress(coreVToken.address, true); - const tx = await factory.createERC4626(coreVToken.address, true); + const predicted = await factory.computeVaultAddress(coreVToken.address); + const tx = await factory.createERC4626(coreVToken.address); const deployed = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.args?.vault; expect(deployed).to.equal(predicted); }); it("should deploy isolated vault to predicted address", async () => { - const predicted = await factory.computeVaultAddress(isolatedVToken.address, false); - const tx = await factory.createERC4626(isolatedVToken.address, false); + const predicted = await factory.computeVaultAddress(isolatedVToken.address); + const tx = await factory.createERC4626(isolatedVToken.address); const deployed = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.args?.vault; expect(deployed).to.equal(predicted); }); @@ -200,7 +200,7 @@ describe("VenusERC4626Factory", () => { describe("Beacon Verification", () => { it("should use correct beacon for core vault", async () => { - const tx = await factory.createERC4626(coreVToken.address, true); + const tx = await factory.createERC4626(coreVToken.address); const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.args?.vault; const beaconSlot = ethers.utils.hexlify( @@ -212,7 +212,7 @@ describe("VenusERC4626Factory", () => { }); it("should use correct beacon for isolated vault", async () => { - const tx = await factory.createERC4626(isolatedVToken.address, false); + const tx = await factory.createERC4626(isolatedVToken.address); const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.args?.vault; const beaconSlot = ethers.utils.hexlify( @@ -226,7 +226,7 @@ describe("VenusERC4626Factory", () => { describe("Vault Initialization", () => { it("should initialize core vault with correct parameters", async () => { - const tx = await factory.createERC4626(coreVToken.address, true); + const tx = await factory.createERC4626(coreVToken.address); const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.args?.vault; const vault = await ethers.getContractAt("VenusERC4626Core", vaultAddress); @@ -235,7 +235,7 @@ describe("VenusERC4626Factory", () => { }); it("should initialize isolated vault with correct parameters", async () => { - const tx = await factory.createERC4626(isolatedVToken.address, false); + const tx = await factory.createERC4626(isolatedVToken.address); const vaultAddress = (await tx.wait()).events?.find(e => e.event === "VaultCreated")?.args?.vault; const vault = await ethers.getContractAt("VenusERC4626Isolated", vaultAddress); From 81e7994a4fd3d82e2d370b72ee5f81a16c7056ba Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 26 Jun 2025 16:11:10 +0530 Subject: [PATCH 22/41] fix: use interface instead of abstract contract --- .../ERC4626/Interfaces/VTokenInterface.sol | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/ERC4626/Interfaces/VTokenInterface.sol b/contracts/ERC4626/Interfaces/VTokenInterface.sol index f0615def..9b630038 100644 --- a/contracts/ERC4626/Interfaces/VTokenInterface.sol +++ b/contracts/ERC4626/Interfaces/VTokenInterface.sol @@ -2,26 +2,26 @@ pragma solidity 0.8.25; import { IComptroller } from "./IComptroller.sol"; -abstract contract VTokenInterface { - function mint(uint mintAmount) external virtual returns (uint); +interface VTokenInterface { + function mint(uint256 mintAmount) external returns (uint256); - function redeem(uint redeemTokens) external virtual returns (uint); + function redeem(uint256 redeemTokens) external returns (uint256); - function redeemUnderlying(uint redeemAmount) external virtual returns (uint); + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); - function balanceOf(address owner) external view virtual returns (uint); + function balanceOf(address owner) external view returns (uint256); - function comptroller() external view virtual returns (IComptroller); + function comptroller() external view returns (IComptroller); - function totalSupply() external view virtual returns (uint); + function totalSupply() external view returns (uint256); - function underlying() external view virtual returns (address); + function underlying() external view returns (address); - function getCash() external view virtual returns (uint); + function getCash() external view returns (uint256); - function exchangeRateStored() public view virtual returns (uint); + function exchangeRateStored() external view returns (uint256); - function accrueInterest() public view virtual returns (uint); + function accrueInterest() external view returns (uint256); - function totalReserves() public view virtual returns (uint); + function totalReserves() external view returns (uint256); } From 199de531d5dc32740bfa9df5c033e7e05f7b0f87 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 30 Jun 2025 16:27:27 +0530 Subject: [PATCH 23/41] fix: minor fixes --- contracts/ERC4626/VenusERC4626Factory.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 0df7cc74..6b6b6a3a 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -22,6 +22,9 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @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 Beacon for isolated vaults /// @dev Previously named `beacon` UpgradeableBeacon public isolatedBeacon; @@ -35,9 +38,6 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Mapping from vToken to deployed ERC4626 vaults mapping(address vToken => ERC4626Upgradeable vault) public createdVaults; - /// @notice Salt used to deterministically deploy core pool vaults - bytes32 public constant CORE_SALT = keccak256("Venus-Core-ERC4626"); - /// @notice Beacon for core vaults UpgradeableBeacon public coreBeacon; @@ -89,7 +89,6 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { ensureNonzeroAddress(isolatedImplementation); ensureNonzeroAddress(coreImplementation); ensureNonzeroAddress(poolRegistry_); - ensureNonzeroAddress(coreComptroller_); ensureNonzeroAddress(rewardRecipient_); __AccessControlled_init(accessControlManager); @@ -103,8 +102,6 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { if (coreComptroller_ != address(0)) { coreComptroller = IComptroller(coreComptroller_); - } else { - coreComptroller = IComptroller(address(0)); } // The owner of the beacon will initially be the owner of the factory @@ -141,6 +138,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @custom:error InvalidVToken if the vToken is invalid or unlisted /// @custom:event Emits VaultCreated event on successful deployment function createERC4626(address vToken) external returns (ERC4626Upgradeable vault) { + ensureNonzeroAddress(vToken); if (address(createdVaults[vToken]) != address(0)) revert VenusERC4626Factory__ERC4626AlreadyExists(); bool isCore = _isCoreVToken(vToken); From 666f288183869a16c51567b488abd4e2da6b14a3 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 30 Jun 2025 18:24:24 +0530 Subject: [PATCH 24/41] refactor: remove try-catch --- contracts/ERC4626/VenusERC4626Factory.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 6b6b6a3a..02882d46 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -200,11 +200,8 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { return false; } - try coreComptroller.markets(vToken) returns (bool listed, uint256) { - return listed; - } catch { - return false; - } + (bool listed, ) = coreComptroller.markets(vToken); + return listed; } /// @dev Deploys a new isolated pool vault From f238e6ebb9b74fdfeaffc5ea2a1a7441342022ca Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 1 Jul 2025 12:24:21 +0530 Subject: [PATCH 25/41] refactor: make coreComptroller immutable --- contracts/ERC4626/VenusERC4626Factory.sol | 32 ++++++++----------- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 33 +++++++++++++------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 02882d46..5fe11c51 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -42,16 +42,17 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { UpgradeableBeacon public coreBeacon; /// @notice Comptroller for core pool validation - IComptroller public coreComptroller; + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IComptroller public immutable CORE_COMPTROLLER; /// @notice Mapping indicating whether a vault belongs to core pool mapping(address => bool) public isCoreVault; /// @notice Emitted when a new vault is created - /// @param vToken The address of the vToken for which the vault was created - /// @param vault The deployed ERC4626 vault address - /// @param isCore Whether the vault is for a core pool - event VaultCreated(address indexed vToken, address indexed vault, bool isCore); + /// @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 @@ -66,7 +67,10 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Constructor /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + constructor(address coreComptroller_) { + ensureNonzeroAddress(coreComptroller_); + CORE_COMPTROLLER = IComptroller(coreComptroller_); + _disableInitializers(); } @@ -75,14 +79,12 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @param isolatedImplementation Implementation address for isolated vaults /// @param coreImplementation Implementation address for core vaults /// @param poolRegistry_ Pool registry address - /// @param coreComptroller_ Core pool comptroller address /// @param rewardRecipient_ Initial reward recipient address function initialize( address accessControlManager, address isolatedImplementation, address coreImplementation, address poolRegistry_, - address coreComptroller_, address rewardRecipient_, uint256 loopsLimitNumber ) external initializer { @@ -100,10 +102,6 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { poolRegistry = PoolRegistryInterface(poolRegistry_); rewardRecipient = rewardRecipient_; - if (coreComptroller_ != address(0)) { - coreComptroller = IComptroller(coreComptroller_); - } - // The owner of the beacon will initially be the owner of the factory isolatedBeacon.transferOwnership(owner()); coreBeacon.transferOwnership(owner()); @@ -136,7 +134,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @return vault The deployed ERC4626 vault /// @custom:error VaultAlreadyExists if a vault already exists for the vToken /// @custom:error InvalidVToken if the vToken is invalid or unlisted - /// @custom:event Emits VaultCreated event on successful deployment + /// @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(); @@ -157,7 +155,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { } createdVaults[vToken] = vault; - emit VaultCreated(vToken, address(vault), isCore); + emit CreateERC4626(vToken, address(vault), isCore); } /// @notice Computes the deterministic vault address for a given vToken @@ -196,11 +194,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @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 (address(coreComptroller) == address(0)) { - return false; - } - - (bool listed, ) = coreComptroller.markets(vToken); + (bool listed, ) = CORE_COMPTROLLER.markets(vToken); return listed; } diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index dff6cf8e..1d88518c 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -74,6 +74,7 @@ describe("VenusERC4626Factory", () => { // Deploy factory const Factory = await ethers.getContractFactory("VenusERC4626Factory"); + factory = await upgrades.deployProxy( Factory, [ @@ -81,11 +82,13 @@ describe("VenusERC4626Factory", () => { venusERC4626IsolatedImpl.address, venusERC4626CoreImpl.address, poolRegistry.address, - coreComptroller.address, rewardRecipient, 100, ], - { initializer: "initialize" }, + { + initializer: "initialize", + constructorArgs: [coreComptroller.address], + }, ); isolatedBeacon = await ethers.getContractAt("UpgradeableBeacon", await factory.isolatedBeacon()); @@ -93,10 +96,18 @@ describe("VenusERC4626Factory", () => { }); describe("Initialization", () => { + it("should revert if coreComptroller is address(0) in constructor", async () => { + const Factory = await ethers.getContractFactory("VenusERC4626Factory"); + await expect(Factory.deploy(constants.AddressZero)).to.be.revertedWithCustomError( + factory, + "ZeroAddressNotAllowed", + ); + }); + 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.coreComptroller()).to.equal(coreComptroller.address); + expect(await factory.CORE_COMPTROLLER()).to.equal(coreComptroller.address); expect(await factory.rewardRecipient()).to.equal(rewardRecipient); }); @@ -115,7 +126,7 @@ describe("VenusERC4626Factory", () => { 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 === "VaultCreated"); + const event = receipt.events?.find(e => e.event === "CreateERC4626"); expect(event?.args?.vToken).to.equal(coreVToken.address); expect(event?.args?.isCore).to.be.true; @@ -124,7 +135,7 @@ describe("VenusERC4626Factory", () => { it("should create isolated vault and emit event", async () => { const tx = await factory.createERC4626(isolatedVToken.address); const receipt = await tx.wait(); - const event = receipt.events?.find(e => e.event === "VaultCreated"); + const event = receipt.events?.find(e => e.event === "CreateERC4626"); expect(event?.args?.vToken).to.equal(isolatedVToken.address); expect(event?.args?.isCore).to.be.false; @@ -159,14 +170,14 @@ describe("VenusERC4626Factory", () => { 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 === "VaultCreated")?.args?.vault; + const deployed = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; expect(deployed).to.equal(predicted); }); 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 === "VaultCreated")?.args?.vault; + const deployed = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; expect(deployed).to.equal(predicted); }); }); @@ -201,7 +212,7 @@ describe("VenusERC4626Factory", () => { 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 === "VaultCreated")?.args?.vault; + 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), @@ -213,7 +224,7 @@ describe("VenusERC4626Factory", () => { 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 === "VaultCreated")?.args?.vault; + 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), @@ -227,7 +238,7 @@ describe("VenusERC4626Factory", () => { 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 === "VaultCreated")?.args?.vault; + 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()); @@ -236,7 +247,7 @@ describe("VenusERC4626Factory", () => { 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 === "VaultCreated")?.args?.vault; + const vaultAddress = (await tx.wait()).events?.find(e => e.event === "CreateERC4626")?.args?.vault; const vault = await ethers.getContractAt("VenusERC4626Isolated", vaultAddress); expect(await vault.owner()).to.equal(await factory.owner()); From e33dfad1b7303043904b367fad29066013c900df Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 1 Jul 2025 15:27:25 +0530 Subject: [PATCH 26/41] refactor: add storage gap in VenusERC4626 --- contracts/ERC4626/Base/VenusERC4626.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index 0766582f..3093a50a 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -33,6 +33,10 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @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. From 718442141824a8d855385cb9c7de593bc795a841 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 1 Jul 2025 15:29:13 +0530 Subject: [PATCH 27/41] refactor: make xvsAddress immutable --- contracts/ERC4626/VenusERC4626Core.sol | 20 +++++++++++++++---- contracts/test/Mocks/MockVenusERC4626Core.sol | 5 +++++ tests/hardhat/ERC4626/VenusERC4626Core.ts | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index 7380231c..e7f52baf 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -4,10 +4,23 @@ 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"; /// @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 /// @param vToken_ The address of the vToken to be wrapped function initialize(address vToken_) public virtual override initializer { @@ -18,8 +31,7 @@ contract VenusERC4626Core is VenusERC4626 { function claimRewards() external override { comptroller.claimVenus(address(this)); - address xvsAddress = comptroller.getXVSAddress(); - IERC20Upgradeable xvs = IERC20Upgradeable(xvsAddress); + IERC20Upgradeable xvs = IERC20Upgradeable(XVS_ADDRESS); uint256 rewardAmount = xvs.balanceOf(address(this)); if (rewardAmount > 0) { @@ -27,11 +39,11 @@ contract VenusERC4626Core is VenusERC4626 { bytes memory data = abi.encodeCall( IProtocolShareReserve.updateAssetsState, - (address(comptroller), xvsAddress, IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS) + (address(comptroller), XVS_ADDRESS, IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS) ); rewardRecipient.call(data); - emit ClaimRewards(rewardAmount, xvsAddress); + emit ClaimRewards(rewardAmount, XVS_ADDRESS); } } } diff --git a/contracts/test/Mocks/MockVenusERC4626Core.sol b/contracts/test/Mocks/MockVenusERC4626Core.sol index 9f808abd..346b1348 100644 --- a/contracts/test/Mocks/MockVenusERC4626Core.sol +++ b/contracts/test/Mocks/MockVenusERC4626Core.sol @@ -14,6 +14,11 @@ contract MockVenusERC4626Core is VenusERC4626Core { 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; diff --git a/tests/hardhat/ERC4626/VenusERC4626Core.ts b/tests/hardhat/ERC4626/VenusERC4626Core.ts index 623e23b2..26eb1d04 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Core.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Core.ts @@ -52,6 +52,7 @@ describe("VenusERC4626Core", () => { venusERC4626Core = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", + constructorArgs: [xvs.address], }); await venusERC4626Core.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address); @@ -349,7 +350,6 @@ describe("VenusERC4626Core", () => { describe("When rewardRecipient is EOA", () => { it("should claim rewards and transfer to recipient", async () => { - comptroller.getXVSAddress.returns(xvs.address); xvs.balanceOf.whenCalledWith(venusERC4626Core.address).returns(rewardAmount); xvs.transfer.returns(true); @@ -372,6 +372,7 @@ describe("VenusERC4626Core", () => { const VenusERC4626Factory = await ethers.getContractFactory("MockVenusERC4626Core"); venusERC4626WithPSR = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", + constructorArgs: [xvs.address], }); await venusERC4626WithPSR.initialize2( @@ -380,7 +381,6 @@ describe("VenusERC4626Core", () => { vaultOwner.address, ); - comptroller.getXVSAddress.returns(xvs.address); xvs.balanceOf.whenCalledWith(venusERC4626WithPSR.address).returns(rewardAmount); xvs.transfer.returns(true); }); From b60227449cbf82662b8b0ec24b2ac43eeed50e91 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 1 Jul 2025 15:30:42 +0530 Subject: [PATCH 28/41] fix: update deployment script --- deploy/020-deploy-VenusERC4626Factory.ts | 5 +++-- helpers/deploymentConfig.ts | 1 + tests/hardhat/ERC4626/VenusERC4626Factory.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deploy/020-deploy-VenusERC4626Factory.ts b/deploy/020-deploy-VenusERC4626Factory.ts index 6dfef9ef..5c6f5eb2 100644 --- a/deploy/020-deploy-VenusERC4626Factory.ts +++ b/deploy/020-deploy-VenusERC4626Factory.ts @@ -19,6 +19,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 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"); // Fetch the zk-compatible ProxyAdmin artifact const defaultProxyAdmin = await artifacts.readArtifact( @@ -38,7 +39,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const CoreImplementation: DeployResult = await deploy("CoreImplementation", { contract: "VenusERC4626Core", from: deployer, - args: [], + args: [xvsAddress], log: true, autoMine: true, skipIfAlreadyDeployed: true, @@ -59,7 +60,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { IsolatedImplementation.address, CoreImplementation.address, poolRegistryAddress, - coreComptroller, rewardRecipientAddress, loopsLimit, ], @@ -70,6 +70,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }, upgradeIndex: 0, }, + args: [coreComptroller], autoMine: true, log: true, skipIfAlreadyDeployed: true, diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index 9ecad40f..27087cca 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -59,6 +59,7 @@ export const preconfiguredAddresses = { AccessControlManager: Wallet.createRandom().address, PoolRegistry: Wallet.createRandom().address, CoreComptroller: Wallet.createRandom().address, + XVS: Wallet.createRandom().address, }, bsctestnet: { NormalTimelock: governanceBscTestnet.NormalTimelock.address, diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 1d88518c..7746a271 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -51,6 +51,7 @@ describe("VenusERC4626Factory", () => { rewardRecipient = deployer.address; // 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); @@ -67,7 +68,7 @@ describe("VenusERC4626Factory", () => { // Deploy implementations const VenusERC4626Core = await ethers.getContractFactory("VenusERC4626Core"); - venusERC4626CoreImpl = await VenusERC4626Core.deploy(); + venusERC4626CoreImpl = await VenusERC4626Core.deploy(xvsAddress); const VenusERC4626Isolated = await ethers.getContractFactory("VenusERC4626Isolated"); venusERC4626IsolatedImpl = await VenusERC4626Isolated.deploy(); From b23cf0d440c517feb3bf1ef008112eb2d4456a8a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 2 Jul 2025 13:36:24 +0530 Subject: [PATCH 29/41] fix: minor fix --- contracts/ERC4626/VenusERC4626Factory.sol | 27 +++++++++++----------- contracts/ERC4626/VenusERC4626Isolated.sol | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 5fe11c51..cc47632c 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -75,29 +75,30 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { } /// @notice Initializes the factory contract - /// @param accessControlManager Access control manager address - /// @param isolatedImplementation Implementation address for isolated vaults - /// @param coreImplementation Implementation address for core vaults + /// @param accessControlManager_ Access control manager address + /// @param isolatedImplementation_ Implementation address for isolated vaults + /// @param coreImplementation_ Implementation address for core vaults /// @param poolRegistry_ Pool registry address /// @param rewardRecipient_ Initial reward recipient address + /// @param loopsLimitNumber_ Maximum number of loops function initialize( - address accessControlManager, - address isolatedImplementation, - address coreImplementation, + address accessControlManager_, + address isolatedImplementation_, + address coreImplementation_, address poolRegistry_, address rewardRecipient_, - uint256 loopsLimitNumber + uint256 loopsLimitNumber_ ) external initializer { - ensureNonzeroAddress(isolatedImplementation); - ensureNonzeroAddress(coreImplementation); + ensureNonzeroAddress(isolatedImplementation_); + ensureNonzeroAddress(coreImplementation_); ensureNonzeroAddress(poolRegistry_); ensureNonzeroAddress(rewardRecipient_); - __AccessControlled_init(accessControlManager); - _setMaxLoopsLimit(loopsLimitNumber); + __AccessControlled_init(accessControlManager_); + _setMaxLoopsLimit(loopsLimitNumber_); - isolatedBeacon = new UpgradeableBeacon(isolatedImplementation); - coreBeacon = new UpgradeableBeacon(coreImplementation); + isolatedBeacon = new UpgradeableBeacon(isolatedImplementation_); + coreBeacon = new UpgradeableBeacon(coreImplementation_); poolRegistry = PoolRegistryInterface(poolRegistry_); rewardRecipient = rewardRecipient_; diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index c98a7f80..3af41396 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -38,7 +38,7 @@ contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { _ensureMaxLoops(rewardDistributors.length); - for (uint256 i = 0; i < rewardDistributors.length; i++) { + for (uint256 i; i < rewardDistributors.length; i++) { RewardsDistributor rewardDistributor = rewardDistributors[i]; IERC20Upgradeable rewardToken = IERC20Upgradeable(address(rewardDistributor.rewardToken())); From 3abc1bae5a70ea0a0c570102355e8a18e816f38e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 3 Jul 2025 11:49:31 +0530 Subject: [PATCH 30/41] feat: add check for vBNB --- contracts/ERC4626/VenusERC4626Factory.sol | 30 +++++++++++++++----- deploy/020-deploy-VenusERC4626Factory.ts | 3 +- helpers/deploymentConfig.ts | 1 + tests/hardhat/ERC4626/VenusERC4626Factory.ts | 19 +++++++------ 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index cc47632c..62d6b545 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -25,6 +25,14 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Salt used to deterministically deploy core pool vaults bytes32 public constant CORE_SALT = keccak256("Venus-Core-ERC4626"); + /// @notice Comptroller for core pool validation + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IComptroller public immutable CORE_COMPTROLLER; + + /// @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; @@ -41,10 +49,6 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Beacon for core vaults UpgradeableBeacon public coreBeacon; - /// @notice Comptroller for core pool validation - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IComptroller public immutable CORE_COMPTROLLER; - /// @notice Mapping indicating whether a vault belongs to core pool mapping(address => bool) public isCoreVault; @@ -66,10 +70,17 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { 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(address coreComptroller_) { - ensureNonzeroAddress(coreComptroller_); - CORE_COMPTROLLER = IComptroller(coreComptroller_); + constructor(address coreComptroller_, address vBNB_) { + if (coreComptroller_ != address(0)) { + CORE_COMPTROLLER = IComptroller(coreComptroller_); + } + + if (vBNB_ != address(0)) { + VBNB = vBNB_; + } _disableInitializers(); } @@ -138,6 +149,11 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @custom:event CreateERC4626 is emitted when the ERC4626 wrapper is created function createERC4626(address vToken) external returns (ERC4626Upgradeable vault) { ensureNonzeroAddress(vToken); + + if (VBNB != address(0) && vToken == VBNB) { + revert VenusERC4626Factory__InvalidVToken(); + } + if (address(createdVaults[vToken]) != address(0)) revert VenusERC4626Factory__ERC4626AlreadyExists(); bool isCore = _isCoreVToken(vToken); diff --git a/deploy/020-deploy-VenusERC4626Factory.ts b/deploy/020-deploy-VenusERC4626Factory.ts index 5c6f5eb2..3c11c4f3 100644 --- a/deploy/020-deploy-VenusERC4626Factory.ts +++ b/deploy/020-deploy-VenusERC4626Factory.ts @@ -20,6 +20,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 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( @@ -70,7 +71,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }, upgradeIndex: 0, }, - args: [coreComptroller], + args: [coreComptroller, vBNB], autoMine: true, log: true, skipIfAlreadyDeployed: true, diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index 27087cca..79e21957 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -60,6 +60,7 @@ export const preconfiguredAddresses = { 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/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 7746a271..a7ecc276 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -31,6 +31,7 @@ describe("VenusERC4626Factory", () => { let coreVToken: FakeContract; let isolatedVToken: FakeContract; let invalidVToken: FakeContract; + let vBNB: FakeContract; let coreComptroller: FakeContract; let poolRegistry: FakeContract; let accessControl: FakeContract; @@ -45,6 +46,7 @@ describe("VenusERC4626Factory", () => { 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"); @@ -88,7 +90,7 @@ describe("VenusERC4626Factory", () => { ], { initializer: "initialize", - constructorArgs: [coreComptroller.address], + constructorArgs: [coreComptroller.address, vBNB.address], }, ); @@ -97,14 +99,6 @@ describe("VenusERC4626Factory", () => { }); describe("Initialization", () => { - it("should revert if coreComptroller is address(0) in constructor", async () => { - const Factory = await ethers.getContractFactory("VenusERC4626Factory"); - await expect(Factory.deploy(constants.AddressZero)).to.be.revertedWithCustomError( - factory, - "ZeroAddressNotAllowed", - ); - }); - it("should set correct initial values", async () => { expect(await factory.accessControlManager()).to.equal(accessControl.address); expect(await factory.poolRegistry()).to.equal(poolRegistry.address); @@ -142,6 +136,13 @@ describe("VenusERC4626Factory", () => { expect(event?.args?.isCore).to.be.false; }); + it("should revert for vBNB vToken", async () => { + await expect(factory.createERC4626(vBNB.address)).to.be.revertedWithCustomError( + factory, + "VenusERC4626Factory__InvalidVToken", + ); + }); + 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( From 4668f44cb93323fe85fff0c4c0bd64d8ca77acf5 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 12:46:44 +0530 Subject: [PATCH 31/41] feat: vew-05 --- contracts/ERC4626/VenusERC4626Factory.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 62d6b545..bff87d5a 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -211,6 +211,9 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @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; } From 18e5f42ad4dd04dc8b15ac25432f7816d0927d14 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 12:58:59 +0530 Subject: [PATCH 32/41] feat: vew-06 and vew-08 --- contracts/ERC4626/Base/VenusERC4626.sol | 2 +- contracts/ERC4626/VenusERC4626Core.sol | 1 + contracts/ERC4626/VenusERC4626Factory.sol | 2 ++ contracts/ERC4626/VenusERC4626Isolated.sol | 3 +++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index 3093a50a..ce48a111 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -391,7 +391,7 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// 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)); diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index e7f52baf..ff79c93a 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -22,6 +22,7 @@ contract VenusERC4626Core is VenusERC4626 { } /// @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 override initializer { super.initialize(vToken_); diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index bff87d5a..c43fe73d 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -47,9 +47,11 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { mapping(address vToken => ERC4626Upgradeable vault) public createdVaults; /// @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 a new vault is created diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 3af41396..0afce02b 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -13,9 +13,12 @@ import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; /// @title VenusERC4626Isolated /// @notice ERC4626 wrapper for Venus Isolated Pool vTokens contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { + /// @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 = 100; /// @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 override initializer { super.initialize(vToken_); From 74f607f10a836847f5a1c03327c98f41d656ba04 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 13:09:30 +0530 Subject: [PATCH 33/41] feat: vew-09 --- contracts/ERC4626/Base/VenusERC4626.sol | 37 +++++++++------------- contracts/ERC4626/VenusERC4626Core.sol | 4 +-- contracts/ERC4626/VenusERC4626Isolated.sol | 11 +++++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index ce48a111..288c1720 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -76,28 +76,6 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr /// @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 virtual initializer { - 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 Sets a new reward recipient address /// @param newRecipient The address of the new reward recipient /// @custom:access Controlled by ACM @@ -307,6 +285,21 @@ abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, Reentr } } + /// @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 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. diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index ff79c93a..c277452c 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -24,8 +24,8 @@ contract VenusERC4626Core is VenusERC4626 { /// @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 override initializer { - super.initialize(vToken_); + function initialize(address vToken_) public virtual initializer { + __VenusERC4626_init(vToken_); } /// @inheritdoc VenusERC4626 diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 0afce02b..2ef1eb93 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -17,11 +17,18 @@ contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { /// @dev This constant is used to prevent excessive gas consumption by limiting the number of loop iterations. uint256 public constant LOOPS_LIMIT = 100; + /// @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 override initializer { - super.initialize(vToken_); + function initialize(address vToken_) public virtual initializer { + __VenusERC4626_init(vToken_); } /// @notice Sets the maximum loops limit From f5dd69bced9b9ff4cb7c95635341f7dbc831905b Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 13:13:47 +0530 Subject: [PATCH 34/41] feat: vew-07 and vew-12 --- contracts/ERC4626/VenusERC4626Factory.sol | 4 ++-- contracts/ERC4626/VenusERC4626Isolated.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index c43fe73d..5c4ee4cc 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -146,8 +146,8 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Creates an ERC4626 vault for the given vToken /// @param vToken Address of the vToken /// @return vault The deployed ERC4626 vault - /// @custom:error VaultAlreadyExists if a vault already exists for the vToken - /// @custom:error InvalidVToken if the vToken is invalid or unlisted + /// @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); diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 2ef1eb93..1095bb16 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -15,7 +15,7 @@ import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { /// @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 = 100; + uint256 public constant LOOPS_LIMIT = 10; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { From f2e1457c043de07e064f27ac35be14f100ce058e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 14:04:23 +0530 Subject: [PATCH 35/41] feat: vew-01 and vew-10 --- contracts/ERC4626/Interfaces/IComptroller.sol | 8 +++++++- contracts/ERC4626/VenusERC4626Core.sol | 19 +++++++++++++------ tests/hardhat/ERC4626/VenusERC4626Core.ts | 18 ++++++++---------- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 2 +- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/contracts/ERC4626/Interfaces/IComptroller.sol b/contracts/ERC4626/Interfaces/IComptroller.sol index 5bbcedf3..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,7 +11,12 @@ 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) external; + function claimVenus( + address[] calldata holders, + VTokenInterface[] calldata vTokens, + bool borrowers, + bool suppliers + ) external; function actionPaused(address market, Action action) external view returns (bool); diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index c277452c..1bf24022 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -5,6 +5,7 @@ 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 @@ -30,7 +31,11 @@ contract VenusERC4626Core is VenusERC4626 { /// @inheritdoc VenusERC4626 function claimRewards() external override { - comptroller.claimVenus(address(this)); + 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)); @@ -38,11 +43,13 @@ contract VenusERC4626Core is VenusERC4626 { if (rewardAmount > 0) { SafeERC20Upgradeable.safeTransfer(xvs, rewardRecipient, rewardAmount); - bytes memory data = abi.encodeCall( - IProtocolShareReserve.updateAssetsState, - (address(comptroller), XVS_ADDRESS, IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS) - ); - rewardRecipient.call(data); + try + IProtocolShareReserve(rewardRecipient).updateAssetsState( + address(comptroller), + XVS_ADDRESS, + IProtocolShareReserve.IncomeType.ERC4626_WRAPPER_REWARDS + ) + {} catch {} emit ClaimRewards(rewardAmount, XVS_ADDRESS); } diff --git a/tests/hardhat/ERC4626/VenusERC4626Core.ts b/tests/hardhat/ERC4626/VenusERC4626Core.ts index 26eb1d04..81960976 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Core.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Core.ts @@ -349,18 +349,11 @@ describe("VenusERC4626Core", () => { const rewardAmount = ethers.utils.parseEther("10"); describe("When rewardRecipient is EOA", () => { - it("should claim rewards and transfer to recipient", async () => { + it("should revert the transaction", async () => { xvs.balanceOf.whenCalledWith(venusERC4626Core.address).returns(rewardAmount); xvs.transfer.returns(true); - await expect(venusERC4626Core.claimRewards()) - .to.emit(venusERC4626Core, "ClaimRewards") - .withArgs(rewardAmount, xvs.address); - - expect(comptroller.claimVenus).to.have.been.calledWith(venusERC4626Core.address); - - expect(xvs.transfer).to.have.been.calledWith(rewardRecipient, rewardAmount); - expect(rewardRecipientPSR.updateAssetsState).to.not.have.been.called; + await expect(venusERC4626Core.claimRewards()).to.be.reverted; }); }); @@ -390,7 +383,12 @@ describe("VenusERC4626Core", () => { .to.emit(venusERC4626WithPSR, "ClaimRewards") .withArgs(rewardAmount, xvs.address); - expect(comptroller.claimVenus).to.have.been.calledWith(venusERC4626WithPSR.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 diff --git a/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index a7ecc276..60129f26 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -86,7 +86,7 @@ describe("VenusERC4626Factory", () => { venusERC4626CoreImpl.address, poolRegistry.address, rewardRecipient, - 100, + 10, ], { initializer: "initialize", From 9827cccc2b77efc6829c6ed9f596033865a999d6 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 14:18:07 +0530 Subject: [PATCH 36/41] feat: vew-02 and vew-03 --- contracts/ERC4626/VenusERC4626Factory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 5c4ee4cc..cbed7f9f 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -101,7 +101,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { address poolRegistry_, address rewardRecipient_, uint256 loopsLimitNumber_ - ) external initializer { + ) external reinitializer(2) { ensureNonzeroAddress(isolatedImplementation_); ensureNonzeroAddress(coreImplementation_); ensureNonzeroAddress(poolRegistry_); @@ -159,9 +159,9 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { if (address(createdVaults[vToken]) != address(0)) revert VenusERC4626Factory__ERC4626AlreadyExists(); bool isCore = _isCoreVToken(vToken); - isCoreVault[vToken] = isCore; if (isCore) { + isCoreVault[vToken] = isCore; vault = _deployCoreVault(vToken); } else { address underlying = VTokenInterface(vToken).underlying(); From 2f7dd00d4f07d302299b32fd288dda12698b28cf Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 14:40:45 +0530 Subject: [PATCH 37/41] feat: vew-04 --- contracts/ERC4626/Base/VenusERC4626.sol | 8 +++++++- contracts/ERC4626/VenusERC4626Isolated.sol | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index 288c1720..769062ca 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -8,6 +8,7 @@ import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC2 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 { IComptroller, Action } from "../Interfaces/IComptroller.sol"; @@ -17,7 +18,12 @@ uint256 constant EXP_SCALE = 1e18; /// @title VenusERC4626 /// @notice Abstract ERC4626 wrapper for Venus vTokens, enabling standard ERC4626 vault interactions with Venus Protocol. -abstract contract VenusERC4626 is ERC4626Upgradeable, AccessControlledV8, ReentrancyGuardUpgradeable { +abstract contract VenusERC4626 is + ERC4626Upgradeable, + AccessControlledV8, + MaxLoopsLimitHelper, + ReentrancyGuardUpgradeable +{ using MathUpgradeable for uint256; using SafeERC20Upgradeable for ERC20Upgradeable; diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 1095bb16..2c530c80 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -7,12 +7,11 @@ import { VToken } from "@venusprotocol/isolated-pools/contracts/VToken.sol"; import { IERC20Upgradeable, SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewards/RewardsDistributor.sol"; -import { MaxLoopsLimitHelper } from "@venusprotocol/isolated-pools/contracts/MaxLoopsLimitHelper.sol"; import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; /// @title VenusERC4626Isolated /// @notice ERC4626 wrapper for Venus Isolated Pool vTokens -contract VenusERC4626Isolated is VenusERC4626, MaxLoopsLimitHelper { +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; From 8d561e6815537c77e53e03d5e029c1671fbdb141 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 21 Jul 2025 16:47:25 +0530 Subject: [PATCH 38/41] feat: vew-14 --- contracts/ERC4626/Interfaces/VTokenInterface.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ERC4626/Interfaces/VTokenInterface.sol b/contracts/ERC4626/Interfaces/VTokenInterface.sol index 9b630038..70ba7338 100644 --- a/contracts/ERC4626/Interfaces/VTokenInterface.sol +++ b/contracts/ERC4626/Interfaces/VTokenInterface.sol @@ -9,6 +9,8 @@ interface VTokenInterface { 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); @@ -21,7 +23,5 @@ interface VTokenInterface { function exchangeRateStored() external view returns (uint256); - function accrueInterest() external view returns (uint256); - function totalReserves() external view returns (uint256); } From 2500fabedf01dc62619dababfc37d0c191692348 Mon Sep 17 00:00:00 2001 From: Jesus Lanchas Date: Fri, 25 Jul 2025 18:49:31 +0200 Subject: [PATCH 39/41] fix: vew-03 --- contracts/ERC4626/VenusERC4626Factory.sol | 20 ++++++++++++++------ deploy/020-deploy-VenusERC4626Factory.ts | 9 +++++++-- tests/hardhat/ERC4626/VenusERC4626Factory.ts | 11 +++-------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index cbed7f9f..5b7c6be1 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -90,20 +90,17 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { /// @notice Initializes the factory contract /// @param accessControlManager_ Access control manager address /// @param isolatedImplementation_ Implementation address for isolated vaults - /// @param coreImplementation_ Implementation address for core vaults /// @param poolRegistry_ Pool registry address /// @param rewardRecipient_ Initial reward recipient address /// @param loopsLimitNumber_ Maximum number of loops function initialize( address accessControlManager_, address isolatedImplementation_, - address coreImplementation_, address poolRegistry_, address rewardRecipient_, uint256 loopsLimitNumber_ - ) external reinitializer(2) { + ) external initializer { ensureNonzeroAddress(isolatedImplementation_); - ensureNonzeroAddress(coreImplementation_); ensureNonzeroAddress(poolRegistry_); ensureNonzeroAddress(rewardRecipient_); @@ -111,14 +108,25 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { _setMaxLoopsLimit(loopsLimitNumber_); isolatedBeacon = new UpgradeableBeacon(isolatedImplementation_); - coreBeacon = new UpgradeableBeacon(coreImplementation_); poolRegistry = PoolRegistryInterface(poolRegistry_); rewardRecipient = rewardRecipient_; // The owner of the beacon will initially be the owner of the factory isolatedBeacon.transferOwnership(owner()); - coreBeacon.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 diff --git a/deploy/020-deploy-VenusERC4626Factory.ts b/deploy/020-deploy-VenusERC4626Factory.ts index 3c11c4f3..6e4e3faa 100644 --- a/deploy/020-deploy-VenusERC4626Factory.ts +++ b/deploy/020-deploy-VenusERC4626Factory.ts @@ -59,7 +59,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { args: [ accessControlManagerAddress, IsolatedImplementation.address, - CoreImplementation.address, poolRegistryAddress, rewardRecipientAddress, loopsLimit, @@ -86,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/tests/hardhat/ERC4626/VenusERC4626Factory.ts b/tests/hardhat/ERC4626/VenusERC4626Factory.ts index 60129f26..1d360c5b 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Factory.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Factory.ts @@ -80,20 +80,15 @@ describe("VenusERC4626Factory", () => { factory = await upgrades.deployProxy( Factory, - [ - accessControl.address, - venusERC4626IsolatedImpl.address, - venusERC4626CoreImpl.address, - poolRegistry.address, - rewardRecipient, - 10, - ], + [accessControl.address, venusERC4626IsolatedImpl.address, poolRegistry.address, rewardRecipient, 10], { initializer: "initialize", constructorArgs: [coreComptroller.address, vBNB.address], }, ); + await factory.initialize2(venusERC4626CoreImpl.address); + isolatedBeacon = await ethers.getContractAt("UpgradeableBeacon", await factory.isolatedBeacon()); coreBeacon = await ethers.getContractAt("UpgradeableBeacon", await factory.coreBeacon()); }); From 32e2cbc744cb14c6ad1fbb0afe6ac7bcf6e1fd21 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 29 Jul 2025 12:27:58 +0530 Subject: [PATCH 40/41] feat: I01 --- contracts/ERC4626/Base/VenusERC4626.sol | 32 +++++++++---------- contracts/ERC4626/VenusERC4626Core.sol | 12 +++++++ contracts/ERC4626/VenusERC4626Factory.sol | 2 +- contracts/ERC4626/VenusERC4626Isolated.sol | 15 ++++----- tests/hardhat/ERC4626/VenusERC4626Isolated.ts | 3 +- 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/contracts/ERC4626/Base/VenusERC4626.sol b/contracts/ERC4626/Base/VenusERC4626.sol index 769062ca..0a9b313c 100644 --- a/contracts/ERC4626/Base/VenusERC4626.sol +++ b/contracts/ERC4626/Base/VenusERC4626.sol @@ -108,22 +108,6 @@ abstract contract VenusERC4626 is /// @dev Must be implemented by child contracts function claimRewards() external virtual; - /// @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 initialize2( - address accessControlManager_, - address rewardRecipient_, - address vaultOwner_ - ) public virtual reinitializer(2) { - ensureNonzeroAddress(vaultOwner_); - - __AccessControlled_init(accessControlManager_); - _setRewardRecipient(rewardRecipient_); - _transferOwnership(vaultOwner_); - } - /// @inheritdoc ERC4626Upgradeable function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256) { ensureNonzeroAddress(receiver); @@ -306,6 +290,22 @@ abstract contract VenusERC4626 is __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. diff --git a/contracts/ERC4626/VenusERC4626Core.sol b/contracts/ERC4626/VenusERC4626Core.sol index 1bf24022..0ad6dc24 100644 --- a/contracts/ERC4626/VenusERC4626Core.sol +++ b/contracts/ERC4626/VenusERC4626Core.sol @@ -54,4 +54,16 @@ contract VenusERC4626Core is VenusERC4626 { 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 5b7c6be1..289a109c 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -240,7 +240,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { ) ) ); - vault.initialize2(address(_accessControlManager), rewardRecipient, owner()); + vault.initialize2(address(_accessControlManager), rewardRecipient, owner(), maxLoopsLimit); return ERC4626Upgradeable(address(vault)); } diff --git a/contracts/ERC4626/VenusERC4626Isolated.sol b/contracts/ERC4626/VenusERC4626Isolated.sol index 2c530c80..90b08c6f 100644 --- a/contracts/ERC4626/VenusERC4626Isolated.sol +++ b/contracts/ERC4626/VenusERC4626Isolated.sol @@ -5,7 +5,6 @@ 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 { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; import { RewardsDistributor } from "@venusprotocol/isolated-pools/contracts/Rewards/RewardsDistributor.sol"; import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol"; @@ -76,16 +75,14 @@ contract VenusERC4626Isolated is VenusERC4626 { /// @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_ - ) public override reinitializer(2) { - ensureNonzeroAddress(vaultOwner_); - - __AccessControlled_init(accessControlManager_); - _setMaxLoopsLimit(LOOPS_LIMIT); - _setRewardRecipient(rewardRecipient_); - _transferOwnership(vaultOwner_); + address vaultOwner_, + uint256 maxLoopsLimit_ + ) public virtual reinitializer(2) { + __VenusERC4626_init2(accessControlManager_, rewardRecipient_, vaultOwner_); + _setMaxLoopsLimit(maxLoopsLimit_); } } diff --git a/tests/hardhat/ERC4626/VenusERC4626Isolated.ts b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts index b76377a0..e432a076 100644 --- a/tests/hardhat/ERC4626/VenusERC4626Isolated.ts +++ b/tests/hardhat/ERC4626/VenusERC4626Isolated.ts @@ -57,7 +57,7 @@ describe("VenusERC4626Isolated", () => { venusERC4626Isolated = await upgrades.deployProxy(VenusERC4626Factory, [vToken.address], { initializer: "initialize", }); - await venusERC4626Isolated.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address); + await venusERC4626Isolated.initialize2(accessControlManager.address, rewardRecipient, vaultOwner.address, 100); }); describe("Initialization", () => { @@ -386,6 +386,7 @@ describe("VenusERC4626Isolated", () => { accessControlManager.address, rewardRecipientPSR.address, vaultOwner.address, + 100, ); comptroller.getRewardDistributors.returns([rewardDistributor.address]); rewardDistributor.rewardToken.returns(xvs.address); From 31d358fa3f0581ca455ba8f272d302d5b6ed07cb Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 29 Jul 2025 12:48:17 +0530 Subject: [PATCH 41/41] fix: vew-08 --- contracts/ERC4626/VenusERC4626Factory.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/ERC4626/VenusERC4626Factory.sol b/contracts/ERC4626/VenusERC4626Factory.sol index 289a109c..8fdcaf21 100644 --- a/contracts/ERC4626/VenusERC4626Factory.sol +++ b/contracts/ERC4626/VenusERC4626Factory.sol @@ -88,6 +88,7 @@ contract VenusERC4626Factory is AccessControlledV8, MaxLoopsLimitHelper { } /// @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