diff --git a/contracts/LimitOutflowModifier.sol b/contracts/LimitOutflowModifier.sol deleted file mode 100644 index 7a49329..0000000 --- a/contracts/LimitOutflowModifier.sol +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -/** - * @title LimitOutflowModifier - * @dev Implementation contract that sits in the middle between the proxy and the implementation contract of - * an ERC-4626 contract and checks if the net outflows in a given timeframe exceeded a given threshold. - * - * The check is executed before any withdraw/redeem operation, and the outflows are recorded on each - * withdraw/redeem/mint/deposit methods. The amounts are computed from the first parameter of these methods - * calls (expressed in assets or shares), without considering the actual assets transferred. - * - * This means, this assumes the underlying ERC-4626 works well. - * - * The limit is applied for TWO `slotSize` periods. So for example if slotSize=1 day and limit=100K, this means - * that up to 100K of outflows every two calendar days are acceptable. - * - * @custom:security-contact security@ensuro.co - * @author Ensuro - */ -contract LimitOutflowModifier is Proxy { - using Address for address; - using SafeCast for uint256; - - address public immutable VAULT; - - type SlotIndex is uint256; // slotSize << 128 + block.timestamp / slotSize - - // @custom:storage-location erc7201:ensuro.storage.LimitOutflowModifier - struct LOMStorage { - uint128 slotSize; // Duration in seconds of the time slots - uint128 limit; // Limit of outflows in a given slot + the previous one - mapping(SlotIndex => int256) assetsDelta; // Variation in assets in a given slot - } - - enum MethodType { - other, - enter, - exit - } - - event LimitChanged(uint256 slotSize, uint256 newLimit); - event DeltaManuallySet(SlotIndex slot, int256 oldDelta, int256 newDelta); - - error LimitReached(int256 assetsDelta, uint256 limit); - - // keccak256(abi.encode(uint256(keccak256("ensuro.storage.LimitOutflowModifier")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant STORAGE_LOCATION = 0x0c147463594770c228964940e3a69f233ddf647014df668242e963bf18053800; - - function _getLOMStorage() private pure returns (LOMStorage storage $) { - // solhint-disable-next-line no-inline-assembly - assembly { - $.slot := STORAGE_LOCATION - } - } - - constructor(address implementation) { - VAULT = implementation; - } - - // I don't define initialize method, because I want to pass it through to the vault. - - /** - * @dev Changes the limit and the timeframe used to track it. - * - * @notice This method doesn't have built-in access control. The access control validation is supposed to be - * implemented by the proxy. But this SHOULDN'T be publicly available. - * - * @param slotSize The duration in seconds of the timeframe used to limit the amount of outflows. - * @param limit The max amount of outflows that will be allowed in a given time slot. - */ - // solhint-disable-next-line func-name-mixedcase - function LOM__setLimit(uint256 slotSize, uint256 limit) external { - _getLOMStorage().limit = limit.toUint128(); - _getLOMStorage().slotSize = slotSize.toUint128(); - emit LimitChanged(slotSize, limit); - } - - // solhint-disable-next-line func-name-mixedcase - function LOM__getSlotSize() external view returns (uint256) { - return _getLOMStorage().slotSize; - } - - // solhint-disable-next-line func-name-mixedcase - function LOM__getLimit() external view returns (uint256) { - return _getLOMStorage().limit; - } - - // solhint-disable-next-line func-name-mixedcase - function LOM__getAssetsDelta(SlotIndex slot) external view returns (int256) { - return _getLOMStorage().assetsDelta[slot]; - } - - // solhint-disable-next-line func-name-mixedcase - function LOM__makeSlot(uint256 slotSize, uint40 timestamp) external pure returns (SlotIndex) { - return SlotIndex.wrap((slotSize << 128) + timestamp / slotSize); - } - - /** - * @dev Manually changes the delta in a given slot. Used to exceptionally allow or disallow limits different than - * the configured ones or to reset the limit when a valid operation is verified. - * - * @notice This method doesn't have built-in access control. The access control validation is supposed to be - * implemented by the proxy. But this SHOULDN'T be publicly available. - * - * @param slot Identification of the slot to modify. - * The slot is computed as `slotSize << 128 + block.timestamp / slotSize` - * @param newDelta The delta in assets to store in a given slot - */ - // solhint-disable-next-line func-name-mixedcase - function LOM__changeDelta(SlotIndex slot, int256 deltaChange) external returns (int256 newDelta) { - int256 oldDelta = _getLOMStorage().assetsDelta[slot]; - newDelta = _getLOMStorage().assetsDelta[slot] += deltaChange; - emit DeltaManuallySet(slot, oldDelta, newDelta); - } - - function _implementation() internal view virtual override returns (address) { - return VAULT; - } - - function _slotIndex() internal view returns (SlotIndex) { - uint256 slotSize = _getLOMStorage().slotSize; - return SlotIndex.wrap((slotSize << 128) + block.timestamp / slotSize); - } - - function _methodType(bytes4 selector) internal pure returns (MethodType) { - if (selector == IERC4626.withdraw.selector || selector == IERC4626.redeem.selector) return MethodType.exit; - if (selector == IERC4626.mint.selector || selector == IERC4626.deposit.selector) return MethodType.enter; - return MethodType.other; - } - - function _convertToAssets(uint256 shares) internal returns (uint256) { - bytes memory result = VAULT.functionDelegateCall(abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares)); - return abi.decode(result, (uint256)); - } - - function _computeAssetsDelta() internal returns (int256) { - bytes4 selector = bytes4(msg.data[0:4]); - uint256 amount = abi.decode(msg.data[4:36], (uint256)); - if (selector == IERC4626.withdraw.selector) return -int256(amount); - if (selector == IERC4626.redeem.selector) return -int256(_convertToAssets(amount)); - if (selector == IERC4626.mint.selector) return int256(_convertToAssets(amount)); - // then --> (selector == IERC4626.deposit.selector) - return int256(amount); - } - - function _fallback() internal override { - MethodType methodType = _methodType(bytes4(msg.data[0:4])); - // If some of the enter/exit methods called, updates - if (methodType != MethodType.other) { - // Computes how much assets will change and if the change exceeds the threshold fails before calling - // the implementation - SlotIndex slot = _slotIndex(); - int256 assetsDelta = _computeAssetsDelta(); - if (assetsDelta < 0) { - // Checks limit not reached - SlotIndex prevSlot = SlotIndex.wrap(SlotIndex.unwrap(slot) - 1); - int256 deltaLastTwoSlots = assetsDelta + - _getLOMStorage().assetsDelta[slot] + - _getLOMStorage().assetsDelta[prevSlot]; - // To check the limit, uses TWO slots, the current one and the previous one. This is to avoid someone doing - // several operations in the slot limit, like withdrawal at 11:59PM and another withdrawal at 12:01 AM. - if (deltaLastTwoSlots < 0 && uint256(-deltaLastTwoSlots) > _getLOMStorage().limit) - revert LimitReached(deltaLastTwoSlots, _getLOMStorage().limit); - } - _getLOMStorage().assetsDelta[slot] += assetsDelta; - } - super._fallback(); - } -} diff --git a/contracts/OutflowLimitedAMMSV.sol b/contracts/OutflowLimitedAMMSV.sol new file mode 100644 index 0000000..cb7009b --- /dev/null +++ b/contracts/OutflowLimitedAMMSV.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {AccessManagedMSV} from "./AccessManagedMSV.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/** + * @title OutflowLimitedAMMSV + * @dev Variant of the AccessManagedMSV that has protection to limit the amount of outflows in a given timeframe. + * + * Reverts if net outflows in a given timeframe exceeded a given threshold. + * + * The check is executed before any withdraw/redeem operation, and the outflows are recorded on each + * withdraw/redeem/mint/deposit methods. + * + * The limit is applied for TWO `slotSize` periods. So for example if slotSize=1 day and limit=100K, this means + * that up to 100K of outflows every two calendar days are acceptable. + * + * @custom:security-contact security@ensuro.co + * @author Ensuro + */ +contract OutflowLimitedAMMSV is AccessManagedMSV { + using SafeCast for uint256; + + type SlotIndex is uint256; // slotSize << 128 + block.timestamp / slotSize + + // @custom:storage-location erc7201:ensuro.storage.OutflowLimitedAMMSV + struct LOMStorage { + uint128 slotSize; // Duration in seconds of the time slots + uint128 limit; // Limit of outflows in a given slot + the previous one + mapping(SlotIndex => int256) assetsDelta; // Variation in assets in a given slot + } + + enum MethodType { + other, + enter, + exit + } + + event LimitChanged(uint256 slotSize, uint256 newLimit); + event DeltaManuallySet(SlotIndex slot, int256 oldDelta, int256 newDelta); + + error LimitReached(int256 assetsDelta, uint256 limit); + + // keccak256(abi.encode(uint256(keccak256("ensuro.storage.OutflowLimitedAMMSV")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant STORAGE_LOCATION = 0xa2ada5d673dba5eecea7c7503ee87e29913d0d36ae093e950d632f7b86891f00; + + function _getLOMStorage() private pure returns (LOMStorage storage $) { + // solhint-disable-next-line no-inline-assembly + assembly { + $.slot := STORAGE_LOCATION + } + } + + /** + * @dev Changes the limit and the timeframe used to track it. + * + * @notice This method doesn't have built-in access control. The access control validation is supposed to be + * implemented by the proxy. But this SHOULDN'T be publicly available. + * + * @param slotSize The duration in seconds of the timeframe used to limit the amount of outflows. + * @param limit The max amount of outflows that will be allowed in a given time slot. + */ + function setupOutflowLimit(uint256 slotSize, uint256 limit) external { + LOMStorage storage $ = _getLOMStorage(); + $.limit = limit.toUint128(); + $.slotSize = slotSize.toUint128(); + emit LimitChanged(slotSize, limit); + } + + function getOutflowLimitSlotSize() external view returns (uint256) { + return _getLOMStorage().slotSize; + } + + function getOutflowLimit() external view returns (uint256) { + return _getLOMStorage().limit; + } + + function getAssetsDelta(SlotIndex slot) external view returns (int256) { + return _getLOMStorage().assetsDelta[slot]; + } + + function makeOutflowSlot(uint256 slotSize, uint40 timestamp) external pure returns (SlotIndex) { + return SlotIndex.wrap((slotSize << 128) + timestamp / slotSize); + } + + /** + * @dev Manually changes the delta in a given slot. Used to exceptionally allow or disallow limits different than + * the configured ones or to reset the limit when a valid operation is verified. + * + * @notice This method doesn't have built-in access control. The access control validation is supposed to be + * implemented by the proxy. But this SHOULDN'T be publicly available. + * + * @param slot Identification of the slot to modify. + * The slot is computed as `slotSize << 128 + block.timestamp / slotSize` + * @param newDelta The delta in assets to store in a given slot + */ + // solhint-disable-next-line func-name-mixedcase + function changeDelta(SlotIndex slot, int256 deltaChange) external returns (int256 newDelta) { + int256 oldDelta = _getLOMStorage().assetsDelta[slot]; + newDelta = _getLOMStorage().assetsDelta[slot] += deltaChange; + emit DeltaManuallySet(slot, oldDelta, newDelta); + } + + function _slotIndex() internal view returns (SlotIndex) { + uint256 slotSize = _getLOMStorage().slotSize; + return SlotIndex.wrap((slotSize << 128) + block.timestamp / slotSize); + } + + /// @inheritdoc ERC4626Upgradeable + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + SlotIndex slot = _slotIndex(); + + // Check delta doesn't exceed the threshold + SlotIndex prevSlot = SlotIndex.wrap(SlotIndex.unwrap(slot) - 1); + LOMStorage storage $ = _getLOMStorage(); + int256 deltaLastTwoSlots = -int256(assets) + $.assetsDelta[slot] + $.assetsDelta[prevSlot]; + // To check the limit, uses TWO slots, the current one and the previous one. This is to avoid someone doing + // several operations in the slot limit, like withdrawal at 11:59PM and another withdrawal at 12:01 AM. + if (deltaLastTwoSlots < 0 && uint256(-deltaLastTwoSlots) > $.limit) revert LimitReached(deltaLastTwoSlots, $.limit); + + // Update the delta and pass the message to parent contract + $.assetsDelta[slot] -= assets.toInt256(); + super._withdraw(caller, receiver, owner, assets, shares); + } + + /// @inheritdoc ERC4626Upgradeable + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + // Just update the delta and pass the message to parent contract + SlotIndex slot = _slotIndex(); + _getLOMStorage().assetsDelta[slot] += assets.toInt256(); + super._deposit(caller, receiver, assets, shares); + } +} diff --git a/test/test-limit-outflow.js b/test/test-limit-outflow.js index 6f20d83..5eeb2a3 100644 --- a/test/test-limit-outflow.js +++ b/test/test-limit-outflow.js @@ -2,7 +2,7 @@ const { expect } = require("chai"); const { amountFunction, getRole } = require("@ensuro/utils/js/utils"); const { WEEK, DAY } = require("@ensuro/utils/js/constants"); const { initCurrency } = require("@ensuro/utils/js/test-utils"); -const { encodeDummyStorage, tagit, makeAllViewsPublic } = require("./utils"); +const { encodeDummyStorage, tagit, makeAllViewsPublic, mergeFragments, setupAMRole } = require("./utils"); const hre = require("hardhat"); const helpers = require("@nomicfoundation/hardhat-network-helpers"); @@ -31,11 +31,8 @@ async function setUp() { .fill(0) .map(() => DummyInvestStrategy.deploy(currency)) ); - const MultiStrategyERC4626 = await ethers.getContractFactory("MultiStrategyERC4626"); - const LimitOutflowModifier = await ethers.getContractFactory("LimitOutflowModifier"); const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); const AccessManagedProxy = await ethers.getContractFactory("AccessManagedProxy"); - const AccessManagedMSV = await ethers.getContractFactory("AccessManagedMSV"); return { currency, @@ -46,115 +43,39 @@ async function setUp() { anon, guardian, admin, - MultiStrategyERC4626, - LimitOutflowModifier, ERC1967Proxy, AccessManagedProxy, - AccessManagedMSV, }; } const variants = [ { - name: "LOM+MultiStrategyERC4626", + name: "AMProxy+OutflowLimitedAMMSV", tagit: tagit, + accessManaged: true, + accessError: "revertedWithAMError", fixture: async () => { const ret = await setUp(); - - const { - MultiStrategyERC4626, - ERC1967Proxy, - LimitOutflowModifier, - strategies, - adminAddr, - currency, - admin, - lp, - lp2, - } = ret; - - async function deployVault(strategies_, initStrategyDatas, depositQueue, withdrawQueue) { - const msv = await MultiStrategyERC4626.deploy(); - - if (strategies_ === undefined) { - strategies_ = strategies; - } else if (typeof strategies_ == "number") { - strategies_ = strategies.slice(0, strategies_); - } - if (initStrategyDatas === undefined) { - initStrategyDatas = strategies_.map(() => encodeDummyStorage({})); - } - if (depositQueue === undefined) { - depositQueue = strategies_.map((_, i) => i); - } - if (withdrawQueue === undefined) { - withdrawQueue = strategies_.map((_, i) => i); - } - - const initializeData = msv.interface.encodeFunctionData("initialize", [ - NAME, - SYMB, - adminAddr, - await ethers.resolveAddress(currency), - await Promise.all(strategies_.map(ethers.resolveAddress)), - initStrategyDatas, - depositQueue, - withdrawQueue, - ]); - - const lom = await LimitOutflowModifier.deploy(msv); - const proxy = await ERC1967Proxy.deploy(lom, initializeData); - const deploymentTransaction = proxy.deploymentTransaction(); - const vault = await ethers.getContractAt("MultiStrategyERC4626", await ethers.resolveAddress(proxy)); - vault.deploymentTransaction = () => deploymentTransaction; - - // Use getContractAt instead of attach to avoid errors with emit assertions - const vaultAsLOM = await ethers.getContractAt("LimitOutflowModifier", await ethers.resolveAddress(proxy)); - await vaultAsLOM.LOM__setLimit(DAY, _A(1000)); - - return { - vault, - lom: vaultAsLOM, - }; - } - - async function grantRole(vault, role, user) { - await vault.connect(admin).grantRole(getRole(role), user); - } - - return { - deployVault, - grantRole, - ...ret, - }; - }, - }, - { - name: "AccessManagedProxy+LOM+AccessManagedMSV", - tagit: tagit, - fixture: async () => { - const ret = await setUp(); - - const { - AccessManagedProxy, - AccessManagedMSV, - ERC1967Proxy, - LimitOutflowModifier, - strategies, - adminAddr, - currency, - admin, - lp, - lp2, - } = ret; + const { strategies, admin, currency } = ret; + const OutflowLimitedAMMSV = await ethers.getContractFactory("OutflowLimitedAMMSV"); + const AccessManagedProxy = await ethers.getContractFactory("AccessManagedProxy"); const AccessManager = await ethers.getContractFactory("AccessManager"); const acMgr = await AccessManager.deploy(admin); - const LP_ROLE = 1; - const LOM_ADMIN = 2; + const msv = await OutflowLimitedAMMSV.deploy(); + const combinedABI = mergeFragments( + OutflowLimitedAMMSV.interface.fragments, + AccessManagedProxy.interface.fragments + ); + const roles = { + LP_ROLE: 1, + LOM_ADMIN: 2, + REBALANCER_ROLE: 3, + STRATEGY_ADMIN_ROLE: 4, + QUEUE_ADMIN_ROLE: 5, + FORWARD_TO_STRATEGY_ROLE: 6, + }; async function deployVault(strategies_, initStrategyDatas, depositQueue, withdrawQueue) { - const msv = await AccessManagedMSV.deploy(); - if (strategies_ === undefined) { strategies_ = strategies; } else if (typeof strategies_ == "number") { @@ -169,7 +90,6 @@ const variants = [ if (withdrawQueue === undefined) { withdrawQueue = strategies_.map((_, i) => i); } - const initializeData = msv.interface.encodeFunctionData("initialize", [ NAME, SYMB, @@ -179,45 +99,60 @@ const variants = [ depositQueue, withdrawQueue, ]); - - const lom = await LimitOutflowModifier.deploy(msv); - const proxy = await AccessManagedProxy.deploy(lom, initializeData, acMgr); + const proxy = await AccessManagedProxy.deploy(msv, initializeData, acMgr); const deploymentTransaction = proxy.deploymentTransaction(); - const vault = await ethers.getContractAt("AccessManagedMSV", await ethers.resolveAddress(proxy)); + const vault = await ethers.getContractAt(combinedABI, await ethers.resolveAddress(proxy)); vault.deploymentTransaction = () => deploymentTransaction; - await makeAllViewsPublic(acMgr.connect(admin), vault); - // Use getContractAt instead of attach to avoid errors with emit assertions - const vaultAsLOM = await ethers.getContractAt("LimitOutflowModifier", await ethers.resolveAddress(proxy)); - await vaultAsLOM.connect(admin).LOM__setLimit(DAY, _A(1000)); + await setupAMRole(acMgr.connect(admin), vault, roles, "LP_ROLE", [ + "withdraw", + "deposit", + "mint", + "redeem", + "transfer", + ]); + await setupAMRole(acMgr.connect(admin), vault, roles, "STRATEGY_ADMIN_ROLE", [ + "addStrategy", + "replaceStrategy", + "removeStrategy", + ]); + + await setupAMRole(acMgr.connect(admin), vault, roles, "QUEUE_ADMIN_ROLE", [ + "changeDepositQueue", + "changeWithdrawQueue", + ]); - await makeAllViewsPublic(acMgr.connect(admin), vaultAsLOM); + await setupAMRole(acMgr.connect(admin), vault, roles, "REBALANCER_ROLE", ["rebalance"]); - // Setup LP role - await acMgr.connect(admin).labelRole(LP_ROLE, "LP_ROLE"); - for (const method of ["withdraw", "deposit", "mint", "redeem", "transfer"]) { - await acMgr - .connect(admin) - .setTargetFunctionRole(vault, [vault.interface.getFunction(method).selector], LP_ROLE); - } + await setupAMRole(acMgr.connect(admin), vault, roles, "FORWARD_TO_STRATEGY_ROLE", ["forwardToStrategy"]); + + await vault.connect(admin).setupOutflowLimit(3600 * 24, _A(1000)); return { vault, - lom: vaultAsLOM, }; } async function grantRole(_, role, user) { - if (role === "LP_ROLE") role = LP_ROLE; - if (role === "LOM_ADMIN") role = LOM_ADMIN; - await acMgr.connect(admin).grantRole(role, user, 0); + const roleId = role.startsWith("0x") ? role : roles[role]; + if (roleId === undefined) throw new Error(`Unknown role ${role}`); + await acMgr.connect(admin).grantRole(roleId, user, 0); + } + + async function grantForwardToStrategy(vault, strategyIndex, method, user) { + await acMgr.connect(admin).grantRole(roles.FORWARD_TO_STRATEGY_ROLE, user, 0); + const specificSelector = await vault.getForwardToStrategySelector(strategyIndex, method); + await acMgr.connect(admin).setTargetFunctionRole(vault, [specificSelector], specificSelector); + await acMgr.connect(admin).grantRole(specificSelector, user, 0); } return { deployVault, - acMgr, grantRole, + grantForwardToStrategy, + acMgr, + OutflowLimitedAMMSV, ...ret, }; }, @@ -228,7 +163,7 @@ variants.forEach((variant) => { describe(`${variant.name} contract tests`, function () { it("Initializes the vault correctly", async () => { const { deployVault, currency, strategies } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(1); + const { vault } = await deployVault(1); expect(await vault.name()).to.equal(NAME); expect(await vault.symbol()).to.equal(SYMB); expect(await vault.withdrawQueue()).to.deep.equal([1].concat(Array(MAX_STRATEGIES - 1).fill(0))); @@ -238,49 +173,50 @@ variants.forEach((variant) => { ); expect(await vault.asset()).to.equal(currency); expect(await vault.totalAssets()).to.equal(0); - expect(await lom.LOM__getLimit()).to.equal(_A(1000)); + expect(await vault.getOutflowLimit()).to.equal(_A(1000)); }); it("Handles withdrawal limits correctly for multiple LPs and ensures limits are respected across time periods", async () => { const { deployVault, lp, lp2, currency, grantRole } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); - await expect(vault.connect(lp).deposit(_A(5000), lp)).not.to.be.reverted; + await expect(vault.connect(lp).deposit(_A(4000), lp)).not.to.be.reverted; + + await helpers.time.increase(WEEK); + await expect(vault.connect(lp).deposit(_A(2000), lp)).not.to.be.reverted; await expect(vault.connect(lp).withdraw(_A(800), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp).withdraw(_A(300), lp, lp)).not.to.be.reverted; - - // Limit happens before the underlying vault limits, so in this case fails with LimitReached even when - // it will fail anyway because lack of funds - await expect(vault.connect(lp).withdraw(_A(5600), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + // So far: +900 Flow / +4900 total + await expect(vault.connect(lp).withdraw(_A(2000), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await currency.connect(lp2).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp2); await expect(vault.connect(lp2).deposit(_A(2000), lp2)).not.to.be.reverted; await expect(vault.connect(lp2).withdraw(_A(400), lp2, lp2)).not.to.be.reverted; - - await expect(vault.connect(lp2).withdraw(_A(6700), lp2, lp2)).to.be.revertedWithCustomError(lom, "LimitReached"); + // So far: +2500 Flow / +6500 + await expect(vault.connect(lp).withdraw(_A(3501), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(WEEK); // Balance Of and other methods work as always expect(await vault.balanceOf(lp2)).to.equal(_A(1600)); - expect(await vault.convertToAssets(vault.balanceOf(lp))).to.equal(_A(3900)); + expect(await vault.convertToAssets(vault.balanceOf(lp))).to.equal(_A(4900)); await expect(vault.connect(lp).withdraw(_A(320), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp2).withdraw(_A(680), lp2, lp2)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(700), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp2).withdraw(_A(800), lp2, lp2)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(700), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp2).withdraw(_A(800), lp2, lp2)).to.be.revertedWithCustomError(vault, "LimitReached"); }); it("Respects withdrawal limits and resets daily limit after time advancement", async () => { const { deployVault, lp, currency, grantRole } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -291,15 +227,15 @@ variants.forEach((variant) => { await expect(vault.connect(lp).withdraw(_A(100), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(970), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(970), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).withdraw(_A(830), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(80), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(80), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); - await expect(vault.connect(lp).withdraw(_A(80), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(80), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); // After two times the time slot, the first day withdrawals disapear @@ -308,12 +244,12 @@ variants.forEach((variant) => { await expect(vault.connect(lp).withdraw(_A(920), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(65), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(65), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); }); it("Prevents withdrawal when combined daily limits from consecutive slots are surpassed", async () => { const { deployVault, lp, currency, grantRole } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -322,14 +258,14 @@ variants.forEach((variant) => { await helpers.time.increase(WEEK); - await expect(vault.connect(lp).withdraw(_A(1001), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1001), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).withdraw(_A(1000), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(165), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(165), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); - await expect(vault.connect(lp).withdraw(_A(165), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(165), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); @@ -341,14 +277,14 @@ variants.forEach((variant) => { await helpers.time.increase(DAY); - await expect(vault.connect(lp).withdraw(_A(275), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(275), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).withdraw(_A(255), lp, lp)).not.to.be.reverted; }); it("Prevents withdrawal for consecutive daily limits, but forgets two days ago withdrawals", async () => { const { deployVault, lp, currency, grantRole } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -359,20 +295,20 @@ variants.forEach((variant) => { await expect(vault.connect(lp).withdraw(_A(400), lp, lp)).not.to.be.reverted; await helpers.time.increase(DAY); - await expect(vault.connect(lp).withdraw(_A(800), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(800), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).withdraw(_A(600), lp, lp)).not.to.be.reverted; // Fails because it reached the limit in the last two days - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); await expect(vault.connect(lp).withdraw(_A(300), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp).withdraw(_A(100), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); }); it("Checks that change in slot size resets the limits", async () => { const { deployVault, lp, currency, grantRole, admin } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -385,24 +321,23 @@ variants.forEach((variant) => { await helpers.time.increase(DAY); await expect(vault.connect(lp).withdraw(_A(600), lp, lp)).not.to.be.reverted; // Fails because it reached the limit in the last two days - - await expect(lom.connect(admin).LOM__setLimit(DAY / 2, _A(500))) - .to.emit(lom, "LimitChanged") + await expect(vault.connect(admin).setupOutflowLimit(DAY / 2, _A(500))) + .to.emit(vault, "LimitChanged") .withArgs(DAY / 2, _A(500)); - expect(await lom.LOM__getLimit()).to.equal(_A(500)); - expect(await lom.LOM__getSlotSize()).to.equal(DAY / 2); + expect(await vault.getOutflowLimit()).to.equal(_A(500)); + expect(await vault.getOutflowLimitSlotSize()).to.equal(DAY / 2); // Changing the slotSize resets the limits, so now we can withdraw await expect(vault.connect(lp).withdraw(_A(500), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY / 2); - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY / 2); await expect(vault.connect(lp).withdraw(_A(5), lp, lp)).not.to.be.reverted; }); it("Allows accumulated withdrawals up to the daily limit and prevents exceeding it", async () => { const { deployVault, lp, currency, grantRole } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -410,27 +345,27 @@ variants.forEach((variant) => { const slotSize = DAY; let now = await helpers.time.latest(); await expect(vault.connect(lp).deposit(_A(5000), lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(5000)); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(5000)); await helpers.time.increase(DAY * 15); now = await helpers.time.latest(); - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(0); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(0); await expect(vault.connect(lp).withdraw(_A(300), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp).withdraw(_A(400), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp).withdraw(_A(299), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-999)); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-999)); await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-1000)); - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-1000)); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); // This fails because the limit extends for TWO slots await helpers.time.increase(DAY); - await expect(vault.connect(lp).withdraw(_A(500), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(500), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); @@ -441,7 +376,7 @@ variants.forEach((variant) => { it("Allows accumulated withdrawals up to the daily limit and prevents exceeding it - mint/redeem", async () => { const { deployVault, lp, currency, grantRole, strategies } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -449,47 +384,43 @@ variants.forEach((variant) => { const slotSize = DAY; let now = await helpers.time.latest(); await expect(vault.connect(lp).mint(_A(5000), lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(5000)); - + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(5000)); await currency.connect(lp).transfer(await strategies[0].other(), _A(2500)); // Give 2500 for free to the vault, now 1 share = 1.5 assets await helpers.time.increase(DAY * 15); now = await helpers.time.latest(); - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(0); - + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(0); await expect(vault.connect(lp).redeem(_A(300), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-450), 1n); - await expect(vault.connect(lp).redeem(_A(400), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-450), 1n); + await expect(vault.connect(lp).redeem(_A(400), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).redeem(_A(200), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp).redeem(_A(166), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-999), 1n); - - await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-999), 1n); + await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); // This fails because the limit extends for TWO slots await helpers.time.increase(DAY); - await expect(vault.connect(lp).redeem(_A(500), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).redeem(_A(500), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); now = await helpers.time.latest(); - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(0), 1n); - + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(0), 1n); await helpers.time.increase(DAY); now = await helpers.time.latest(); await expect(vault.connect(lp).redeem(_A(500), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-750)); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-750)); await expect(vault.connect(lp).redeem(_A(165), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-997.5), 1n); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-997.5), 1n); await expect(vault.connect(lp).redeem(_A(1), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-999), 1n); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-999), 1n); }); it("Allows accumulated withdrawals up to the daily limit and prevents exceeding it - mint/redeem/deposit/withdraw", async () => { const { deployVault, lp, currency, grantRole, strategies } = await helpers.loadFixture(variant.fixture); - const { vault, lom } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); + const { vault } = await deployVault(4, undefined, [3, 2, 1, 0], [2, 0, 3, 1]); await currency.connect(lp).approve(vault, MaxUint256); await grantRole(vault, "LP_ROLE", lp); @@ -497,90 +428,87 @@ variants.forEach((variant) => { const slotSize = DAY; let now = await helpers.time.latest(); await expect(vault.connect(lp).deposit(_A(3000), lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(3000)); - + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(3000)); await helpers.time.increase(DAY * 15); now = await helpers.time.latest(); - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(0); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(0); // Mixing withdraws and redeems when ratio is 1 share = 1 asset await expect(vault.connect(lp).redeem(_A(300), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-300)); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-300)); await expect(vault.connect(lp).withdraw(_A(400), lp, lp)).not.to.be.reverted; await expect(vault.connect(lp).redeem(_A(299), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-999)); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-999)); await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-1000)); - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-1000)); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).mint(_A(3000), lp)).not.to.be.reverted; // Minted _A(3000), actual slot is _A(3000) + (_A(-1000)) = _A(2000) - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(2000)); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(2000)); await currency.connect(lp).transfer(await strategies[0].other(), _A(2500)); // Give 2500 for free to the vault, now 1 share = 1.5 assets await helpers.time.increase(DAY * 7); now = await helpers.time.latest(); - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(0); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(0); await expect(vault.connect(lp).redeem(_A(300), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-450), 1n); - await expect(vault.connect(lp).redeem(_A(400), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-450), 1n); + await expect(vault.connect(lp).redeem(_A(400), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).withdraw(_A(549), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-999), 1n); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-999), 1n); await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-1000), 1n); - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-1000), 1n); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); // This fails because the limit extends for TWO slots await helpers.time.increase(DAY); - await expect(vault.connect(lp).redeem(_A(500), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp).withdraw(_A(500), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); - await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).redeem(_A(500), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(500), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); + await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await helpers.time.increase(DAY); now = await helpers.time.latest(); await expect(vault.connect(lp).withdraw(_A(300), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.equal(_A(-300)); - await expect(vault.connect(lp).redeem(_A(500), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.equal(_A(-300)); + await expect(vault.connect(lp).redeem(_A(500), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); await expect(vault.connect(lp).redeem(_A(300), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-750), 1n); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-750), 1n); await expect(vault.connect(lp).withdraw(_A(249), lp, lp)).not.to.be.reverted; - await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(lom, "LimitReached"); + await expect(vault.connect(lp).redeem(_A(1), lp, lp)).to.be.revertedWithCustomError(vault, "LimitReached"); - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-999), 1n); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-999), 1n); await expect(vault.connect(lp).withdraw(_A(1), lp, lp)).not.to.be.reverted; - expect(await lom.LOM__getAssetsDelta(await lom.LOM__makeSlot(slotSize, now))).to.closeTo(_A(-1000), 1n); + expect(await vault.getAssetsDelta(await vault.makeOutflowSlot(slotSize, now))).to.closeTo(_A(-1000), 1n); }); it("Sets and resets delta using LOM__changeDelta correctly", async () => { const { deployVault, admin } = await helpers.loadFixture(variant.fixture); - const { lom } = await deployVault(2); - const lomContract = await ethers.getContractAt("LimitOutflowModifier", lom); + const { vault } = await deployVault(2); const slotSize = DAY; const currentTimestamp = await helpers.time.latest(); + const slotIndex = await vault.makeOutflowSlot(slotSize, currentTimestamp); - const slotIndex = await lom.LOM__makeSlot(slotSize, currentTimestamp); - - await expect(lomContract.connect(admin).LOM__changeDelta(slotIndex, _A(1500))) - .to.emit(lomContract, "DeltaManuallySet") + await expect(vault.connect(admin).changeDelta(slotIndex, _A(1500))) + .to.emit(vault, "DeltaManuallySet") .withArgs(slotIndex, 0, _A(1500)); - await expect(lomContract.connect(admin).LOM__changeDelta(slotIndex, _A(-250))) - .to.emit(lomContract, "DeltaManuallySet") + await expect(vault.connect(admin).changeDelta(slotIndex, _A(-250))) + .to.emit(vault, "DeltaManuallySet") .withArgs(slotIndex, _A(1500), _A(1250)); }); }); diff --git a/test/test-multi-strategy-erc4626.js b/test/test-multi-strategy-erc4626.js index 8e70ac9..14c1da0 100644 --- a/test/test-multi-strategy-erc4626.js +++ b/test/test-multi-strategy-erc4626.js @@ -1,9 +1,10 @@ const { expect } = require("chai"); const { _A, getRole } = require("@ensuro/utils/js/utils"); const { initCurrency } = require("@ensuro/utils/js/test-utils"); -const { encodeDummyStorage, dummyStorage, tagit, makeAllViewsPublic, mergeFragments, setupAMRole } = require("./utils"); +const { encodeDummyStorage, dummyStorage, tagit, makeAllViewsPublic, setupAMRole } = require("./utils"); const hre = require("hardhat"); const helpers = require("@nomicfoundation/hardhat-network-helpers"); +const { deploy: ozUpgradesDeploy } = require("@openzeppelin/hardhat-upgrades/dist/utils"); const { ethers } = hre; const { ZeroAddress, MaxUint256 } = hre.ethers; @@ -105,7 +106,7 @@ const variants = [ }, }, { - name: "AMProxy+LOM-AccessManagedMSV", + name: "AMProxy+AccessManagedMSV", tagit: tagit, accessManaged: true, accessError: "revertedWithAMError", @@ -113,16 +114,9 @@ const variants = [ const ret = await setUp(); const { strategies, admin, currency } = ret; const AccessManagedMSV = await ethers.getContractFactory("AccessManagedMSV"); - const LimitOutflowModifier = await ethers.getContractFactory("LimitOutflowModifier"); const AccessManagedProxy = await ethers.getContractFactory("AccessManagedProxy"); const AccessManager = await ethers.getContractFactory("AccessManager"); const acMgr = await AccessManager.deploy(admin); - const msv = await AccessManagedMSV.deploy(); - const lom = await LimitOutflowModifier.deploy(msv); - const combinedABI = mergeFragments( - AccessManagedMSV.interface.fragments, - mergeFragments(LimitOutflowModifier.interface.fragments, AccessManagedProxy.interface.fragments) - ); const roles = { LP_ROLE: 1, LOM_ADMIN: 2, @@ -147,19 +141,128 @@ const variants = [ if (withdrawQueue === undefined) { withdrawQueue = strategies_.map((_, i) => i); } - const initializeData = msv.interface.encodeFunctionData("initialize", [ - NAME, - SYMB, - await ethers.resolveAddress(currency), - await Promise.all(strategies_.map(ethers.resolveAddress)), - initStrategyDatas, - depositQueue, - withdrawQueue, + const vault = await hre.upgrades.deployProxy( + AccessManagedMSV, + [ + NAME, + SYMB, + await ethers.resolveAddress(currency), + await Promise.all(strategies_.map(ethers.resolveAddress)), + initStrategyDatas, + depositQueue, + withdrawQueue, + ], + { + kind: "uups", + unsafeAllow: ["delegatecall"], + proxyFactory: AccessManagedProxy, + deployFunction: async (hre, opts, factory, ...args) => ozUpgradesDeploy(hre, opts, factory, ...args, acMgr), + } + ); + await makeAllViewsPublic(acMgr.connect(admin), vault); + + await setupAMRole(acMgr.connect(admin), vault, roles, "LP_ROLE", [ + "withdraw", + "deposit", + "mint", + "redeem", + "transfer", + ]); + await setupAMRole(acMgr.connect(admin), vault, roles, "STRATEGY_ADMIN_ROLE", [ + "addStrategy", + "replaceStrategy", + "removeStrategy", ]); - const proxy = await AccessManagedProxy.deploy(lom, initializeData, acMgr); - const deploymentTransaction = proxy.deploymentTransaction(); - const vault = await ethers.getContractAt(combinedABI, await ethers.resolveAddress(proxy)); - vault.deploymentTransaction = () => deploymentTransaction; + + await setupAMRole(acMgr.connect(admin), vault, roles, "QUEUE_ADMIN_ROLE", [ + "changeDepositQueue", + "changeWithdrawQueue", + ]); + + await setupAMRole(acMgr.connect(admin), vault, roles, "REBALANCER_ROLE", ["rebalance"]); + + await setupAMRole(acMgr.connect(admin), vault, roles, "FORWARD_TO_STRATEGY_ROLE", ["forwardToStrategy"]); + + return vault; + } + + async function grantRole(_, role, user) { + const roleId = role.startsWith("0x") ? role : roles[role]; + if (roleId === undefined) throw new Error(`Unknown role ${role}`); + await acMgr.connect(admin).grantRole(roleId, user, 0); + } + + async function grantForwardToStrategy(vault, strategyIndex, method, user) { + await acMgr.connect(admin).grantRole(roles.FORWARD_TO_STRATEGY_ROLE, user, 0); + const specificSelector = await vault.getForwardToStrategySelector(strategyIndex, method); + await acMgr.connect(admin).setTargetFunctionRole(vault, [specificSelector], specificSelector); + await acMgr.connect(admin).grantRole(specificSelector, user, 0); + } + + return { + deployVault, + grantRole, + grantForwardToStrategy, + acMgr, + AccessManagedMSV, + ...ret, + }; + }, + }, + { + name: "AMProxy+OutflowLimitedAMMSV", + tagit: tagit, + accessManaged: true, + accessError: "revertedWithAMError", + fixture: async () => { + const ret = await setUp(); + const { strategies, admin, currency } = ret; + const OutflowLimitedAMMSV = await ethers.getContractFactory("OutflowLimitedAMMSV"); + const AccessManagedProxy = await ethers.getContractFactory("AccessManagedProxy"); + const AccessManager = await ethers.getContractFactory("AccessManager"); + const acMgr = await AccessManager.deploy(admin); + const roles = { + LP_ROLE: 1, + LOM_ADMIN: 2, + REBALANCER_ROLE: 3, + STRATEGY_ADMIN_ROLE: 4, + QUEUE_ADMIN_ROLE: 5, + FORWARD_TO_STRATEGY_ROLE: 6, + }; + + async function deployVault(strategies_, initStrategyDatas, depositQueue, withdrawQueue) { + if (strategies_ === undefined) { + strategies_ = strategies; + } else if (typeof strategies_ == "number") { + strategies_ = strategies.slice(0, strategies_); + } + if (initStrategyDatas === undefined) { + initStrategyDatas = strategies_.map(() => encodeDummyStorage({})); + } + if (depositQueue === undefined) { + depositQueue = strategies_.map((_, i) => i); + } + if (withdrawQueue === undefined) { + withdrawQueue = strategies_.map((_, i) => i); + } + const vault = await hre.upgrades.deployProxy( + OutflowLimitedAMMSV, + [ + NAME, + SYMB, + await ethers.resolveAddress(currency), + await Promise.all(strategies_.map(ethers.resolveAddress)), + initStrategyDatas, + depositQueue, + withdrawQueue, + ], + { + kind: "uups", + unsafeAllow: ["delegatecall"], + proxyFactory: AccessManagedProxy, + deployFunction: async (hre, opts, factory, ...args) => ozUpgradesDeploy(hre, opts, factory, ...args, acMgr), + } + ); await makeAllViewsPublic(acMgr.connect(admin), vault); await setupAMRole(acMgr.connect(admin), vault, roles, "LP_ROLE", [ @@ -184,7 +287,7 @@ const variants = [ await setupAMRole(acMgr.connect(admin), vault, roles, "FORWARD_TO_STRATEGY_ROLE", ["forwardToStrategy"]); - await vault.connect(admin).LOM__setLimit(3600 * 24, _A(1)); + await vault.connect(admin).setupOutflowLimit(3600 * 24, _A(1)); return vault; } @@ -207,7 +310,7 @@ const variants = [ grantRole, grantForwardToStrategy, acMgr, - AccessManagedMSV, + OutflowLimitedAMMSV, ...ret, }; }, @@ -260,8 +363,11 @@ variants.forEach((variant) => { }); variant.tagit("Checks vault constructs with disabled initializer [!MultiStrategyERC4626]", async () => { - const { AccessManagedMSV, currency, strategies } = await helpers.loadFixture(variant.fixture); - const newVault = await AccessManagedMSV.deploy(); + const { AccessManagedMSV, OutflowLimitedAMMSV, currency, strategies } = await helpers.loadFixture( + variant.fixture + ); + const factory = AccessManagedMSV || OutflowLimitedAMMSV; + const newVault = await factory.deploy(); await expect(newVault.deploymentTransaction()).to.emit(newVault, "Initialized"); await expect( newVault.initialize( @@ -273,7 +379,7 @@ variants.forEach((variant) => { [0], [0] ) - ).to.be.revertedWithCustomError(AccessManagedMSV, "InvalidInitialization"); + ).to.be.revertedWithCustomError(factory, "InvalidInitialization"); }); variant.tagit("Initializes the vault correctly", async () => {