From 91119b740aa2671effffc1a600dd612b25401ea9 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Dec 2025 09:33:17 -0500 Subject: [PATCH 01/18] feat: duration vaults --- .../interfaces/IDurationVaultStrategy.sol | 113 +++++++++ src/contracts/interfaces/IStrategyFactory.sol | 44 ++++ .../strategies/DurationVaultStrategy.sol | 232 ++++++++++++++++++ src/contracts/strategies/StrategyFactory.sol | 68 +++++ .../strategies/StrategyFactoryStorage.sol | 10 +- src/test/unit/DurationVaultStrategyUnit.t.sol | 106 ++++++++ src/test/unit/StrategyFactoryUnit.t.sol | 76 ++++++ .../unit/StrategyManagerDurationUnit.t.sol | 154 ++++++++++++ 8 files changed, 801 insertions(+), 2 deletions(-) create mode 100644 src/contracts/interfaces/IDurationVaultStrategy.sol create mode 100644 src/contracts/strategies/DurationVaultStrategy.sol create mode 100644 src/test/unit/DurationVaultStrategyUnit.t.sol create mode 100644 src/test/unit/StrategyManagerDurationUnit.t.sol diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol new file mode 100644 index 0000000000..001bbf2352 --- /dev/null +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./IStrategy.sol"; + +/** + * @title Interface for time-bound EigenLayer vault strategies. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +interface IDurationVaultStrategy is IStrategy { + struct VaultConfig { + IERC20 underlyingToken; + address vaultAdmin; + uint64 depositWindowStart; + uint64 depositWindowEnd; + uint64 duration; + uint256 maxPerDeposit; + uint256 stakeCap; + string metadataURI; + } + + /// @dev Thrown when attempting to use a zero-address vault admin. + error InvalidVaultAdmin(); + /// @dev Thrown when attempting to configure a zero duration. + error InvalidDuration(); + /// @dev Thrown when attempting to configure an invalid deposit window. + error InvalidDepositWindow(); + /// @dev Thrown when attempting to mutate configuration from a non-admin. + error OnlyVaultAdmin(); + /// @dev Thrown when attempting to lock an already locked vault. + error VaultAlreadyLocked(); + /// @dev Thrown when attempting to unlock or update a vault that has not been locked yet. + error VaultNotLocked(); + /// @dev Thrown when attempting to deposit before the window opens. + error DepositWindowNotStarted(); + /// @dev Thrown when attempting to deposit after the window closes (either manually or via lock). + error DepositWindowClosed(); + /// @dev Thrown when attempting to withdraw while funds remain locked. + error WithdrawalsLocked(); + /// @dev Thrown when attempting to mark the vault as matured before duration elapses. + error DurationNotElapsed(); + + event VaultInitialized( + address indexed vaultAdmin, + IERC20 indexed underlyingToken, + uint64 depositWindowStart, + uint64 depositWindowEnd, + uint64 duration, + uint256 maxPerDeposit, + uint256 stakeCap, + string metadataURI + ); + + event VaultLocked(uint64 lockedAt, uint64 unlockAt); + + event VaultMatured(uint64 maturedAt); + + event VaultAdminUpdated(address indexed previousAdmin, address indexed newAdmin); + + event DepositWindowUpdated(uint64 newStart, uint64 newEnd); + + event MetadataURIUpdated(string newMetadataURI); + + /** + * @notice Locks the vault, preventing further deposits / withdrawals until maturity. + */ + function lock() external; + + /** + * @notice Marks the vault as matured once the configured duration has elapsed. + * @dev After maturation, withdrawals are permitted while deposits remain disabled. + */ + function markMatured() external; + + /** + * @notice Updates the vault metadata URI. + */ + function updateMetadataURI( + string calldata newMetadataURI + ) external; + + /** + * @notice Updates the deposit window bounds. Only callable before the vault is locked. + */ + function updateDepositWindow( + uint64 newStart, + uint64 newEnd + ) external; + + /** + * @notice Transfers vault admin privileges to a new address. + */ + function transferVaultAdmin( + address newVaultAdmin + ) external; + + function vaultAdmin() external view returns (address); + function depositWindowStart() external view returns (uint64); + function depositWindowEnd() external view returns (uint64); + function duration() external view returns (uint64); + function lockedAt() external view returns (uint64); + function unlockTimestamp() external view returns (uint64); + function isLocked() external view returns (bool); + function isMatured() external view returns (bool); + function metadataURI() external view returns (string memory); + function stakeCap() external view returns (uint256); + function depositsOpen() external view returns (bool); + function withdrawalsOpen() external view returns (bool); +} + diff --git a/src/contracts/interfaces/IStrategyFactory.sol b/src/contracts/interfaces/IStrategyFactory.sol index c019695b4a..c367615199 100644 --- a/src/contracts/interfaces/IStrategyFactory.sol +++ b/src/contracts/interfaces/IStrategyFactory.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IStrategy.sol"; +import "./IDurationVaultStrategy.sol"; import "./ISemVerMixin.sol"; /** @@ -19,12 +20,17 @@ interface IStrategyFactory is ISemVerMixin { error StrategyAlreadyExists(); /// @dev Thrown when attempting to blacklist a token that is already blacklisted error AlreadyBlacklisted(); + /// @dev Thrown when attempting to deploy a duration vault before its beacon has been configured. + error DurationVaultBeaconNotSet(); event TokenBlacklisted(IERC20 token); /// @notice Upgradeable beacon which new Strategies deployed by this contract point to function strategyBeacon() external view returns (IBeacon); + /// @notice Upgradeable beacon which duration vault strategies deployed by this contract point to + function durationVaultBeacon() external view returns (IBeacon); + /// @notice Mapping token => Strategy contract for the token /// The strategies in this mapping are deployed by the StrategyFactory. /// The factory can only deploy a single strategy per token address @@ -47,6 +53,21 @@ interface IStrategyFactory is ISemVerMixin { IERC20 token ) external returns (IStrategy newStrategy); + /** + * @notice Deploys a new duration-bound vault strategy contract. + * @dev Enforces the same blacklist semantics as vanilla strategies. + */ + function deployDurationVaultStrategy( + IDurationVaultStrategy.VaultConfig calldata config + ) external returns (IDurationVaultStrategy newVault); + + /** + * @notice Returns all duration vaults that have ever been deployed for a given token. + */ + function getDurationVaults( + IERC20 token + ) external view returns (IDurationVaultStrategy[] memory); + /** * @notice Owner-only function to pass through a call to `StrategyManager.addStrategiesToDepositWhitelist` */ @@ -61,9 +82,32 @@ interface IStrategyFactory is ISemVerMixin { IStrategy[] calldata strategiesToRemoveFromWhitelist ) external; + /** + * @notice Owner-only function to update the beacon used for deploying duration vault strategies. + */ + function setDurationVaultBeacon( + IBeacon newDurationVaultBeacon + ) external; + /// @notice Emitted when the `strategyBeacon` is changed event StrategyBeaconModified(IBeacon previousBeacon, IBeacon newBeacon); + /// @notice Emitted when the `durationVaultBeacon` is changed + event DurationVaultBeaconModified(IBeacon previousBeacon, IBeacon newBeacon); + /// @notice Emitted whenever a slot is set in the `tokenStrategy` mapping event StrategySetForToken(IERC20 token, IStrategy strategy); + + /// @notice Emitted whenever a duration vault is deployed. + event DurationVaultDeployed( + IDurationVaultStrategy vault, + IERC20 indexed underlyingToken, + address indexed vaultAdmin, + uint64 depositWindowStart, + uint64 depositWindowEnd, + uint64 duration, + uint256 maxPerDeposit, + uint256 stakeCap, + string metadataURI + ); } diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol new file mode 100644 index 0000000000..cc7bf2efeb --- /dev/null +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "./StrategyBaseTVLLimits.sol"; +import "../interfaces/IDurationVaultStrategy.sol"; + +/** + * @title Duration-bound EigenLayer vault strategy with configurable deposit caps and windows. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy { + /// @notice Address empowered to configure and lock the vault. + address public vaultAdmin; + + /// @notice Timestamp when the deposit window opens. Zero means "immediately open". + uint64 public depositWindowStart; + + /// @notice Timestamp when the deposit window closes. Zero means "no enforced close" until lock(). + uint64 public depositWindowEnd; + + /// @notice The enforced lock duration once `lock` is called. + uint64 public duration; + + /// @notice Timestamp when the vault was locked. Zero indicates the vault is not yet locked. + uint64 public lockedAt; + + /// @notice Timestamp when the vault unlocks (set at lock time). + uint64 public unlockAt; + + /// @notice Timestamp when the vault was marked as matured (purely informational). + uint64 public maturedAt; + + /// @notice Optional metadata URI describing the vault configuration. + string public metadataURI; + + modifier onlyVaultAdmin() { + if (msg.sender != vaultAdmin) revert OnlyVaultAdmin(); + _; + } + + constructor( + IStrategyManager _strategyManager, + IPauserRegistry _pauserRegistry, + string memory _version + ) StrategyBaseTVLLimits(_strategyManager, _pauserRegistry, _version) {} + + /** + * @notice Initializes the vault configuration. + */ + function initialize( + VaultConfig memory config + ) public initializer { + if (config.vaultAdmin == address(0)) revert InvalidVaultAdmin(); + if (config.duration == 0) revert InvalidDuration(); + if (config.depositWindowEnd != 0 && config.depositWindowEnd <= config.depositWindowStart) { + revert InvalidDepositWindow(); + } + + _setTVLLimits(config.maxPerDeposit, config.stakeCap); + _initializeStrategyBase(config.underlyingToken); + + vaultAdmin = config.vaultAdmin; + depositWindowStart = config.depositWindowStart; + depositWindowEnd = config.depositWindowEnd; + duration = config.duration; + metadataURI = config.metadataURI; + + emit VaultInitialized( + vaultAdmin, + config.underlyingToken, + depositWindowStart, + depositWindowEnd, + duration, + config.maxPerDeposit, + config.stakeCap, + metadataURI + ); + } + + /** + * @notice Locks the vault, preventing new deposits and withdrawals until maturity. + */ + function lock() external override onlyVaultAdmin { + if (lockedAt != 0) revert VaultAlreadyLocked(); + if (depositWindowStart != 0 && block.timestamp < depositWindowStart) revert DepositWindowNotStarted(); + + lockedAt = uint64(block.timestamp); + uint256 rawUnlockTimestamp = uint256(lockedAt) + uint256(duration); + require(rawUnlockTimestamp <= type(uint64).max, InvalidDuration()); + unlockAt = uint64(rawUnlockTimestamp); + + // Closing the deposit window at the lock time ensures future deposits revert even if no explicit end was set. + depositWindowEnd = lockedAt; + + emit VaultLocked(lockedAt, unlockAt); + } + + /** + * @notice Marks the vault as matured once the configured duration elapses. Callable by anyone. + */ + function markMatured() external override { + if (!isMatured()) revert DurationNotElapsed(); + if (maturedAt != 0) { + // already recorded; noop + return; + } + maturedAt = uint64(block.timestamp); + emit VaultMatured(maturedAt); + } + + /** + * @notice Updates the metadata URI describing the vault. + */ + function updateMetadataURI( + string calldata newMetadataURI + ) external override onlyVaultAdmin { + metadataURI = newMetadataURI; + emit MetadataURIUpdated(newMetadataURI); + } + + /** + * @notice Updates the deposit window bounds. Cannot be called after locking. + */ + function updateDepositWindow( + uint64 newStart, + uint64 newEnd + ) external override onlyVaultAdmin { + if (lockedAt != 0) revert VaultAlreadyLocked(); + if (newEnd != 0 && newEnd <= newStart) revert InvalidDepositWindow(); + depositWindowStart = newStart; + depositWindowEnd = newEnd; + emit DepositWindowUpdated(newStart, newEnd); + } + + /** + * @notice Transfers admin privileges to a new address. + */ + function transferVaultAdmin( + address newVaultAdmin + ) external override onlyVaultAdmin { + if (newVaultAdmin == address(0)) revert InvalidVaultAdmin(); + emit VaultAdminUpdated(vaultAdmin, newVaultAdmin); + vaultAdmin = newVaultAdmin; + } + + /// @inheritdoc IDurationVaultStrategy + function unlockTimestamp() public view override returns (uint64) { + return unlockAt; + } + + /// @inheritdoc IDurationVaultStrategy + function isLocked() public view override returns (bool) { + return lockedAt != 0; + } + + /// @inheritdoc IDurationVaultStrategy + function isMatured() public view override returns (bool) { + uint64 lockTimestamp = lockedAt; + if (lockTimestamp == 0) { + return false; + } + return block.timestamp >= unlockAt && unlockAt != 0; + } + + /// @inheritdoc IDurationVaultStrategy + function stakeCap() external view override returns (uint256) { + return maxTotalDeposits; + } + + /// @inheritdoc IDurationVaultStrategy + function depositsOpen() public view override returns (bool) { + return _depositsWithinWindow() && !isLocked(); + } + + /// @inheritdoc IDurationVaultStrategy + function withdrawalsOpen() public view override returns (bool) { + if (!isLocked()) { + return true; + } + return isMatured(); + } + + /** + * @notice Internal helper verifying deposit timing constraints. + */ + function _depositsWithinWindow() internal view returns (bool) { + uint64 start = depositWindowStart; + uint64 end = depositWindowEnd; + if (start != 0 && block.timestamp < start) { + return false; + } + if (end != 0 && block.timestamp > end) { + return false; + } + return true; + } + + function _beforeDeposit( + IERC20 token, + uint256 amount + ) internal virtual override { + if (!isLocked()) { + uint64 start = depositWindowStart; + if (start != 0 && block.timestamp < start) revert DepositWindowNotStarted(); + uint64 end = depositWindowEnd; + if (end != 0 && block.timestamp > end) revert DepositWindowClosed(); + } else { + revert DepositWindowClosed(); + } + + super._beforeDeposit(token, amount); + } + + function _beforeWithdrawal( + address recipient, + IERC20 token, + uint256 amountShares + ) internal virtual override { + if (isLocked() && !isMatured()) { + revert WithdrawalsLocked(); + } + super._beforeWithdrawal(recipient, token, amountShares); + } + + /** + * @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[40] private __gap; +} + diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index 385bdcd839..dc7ae912f5 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -6,6 +6,8 @@ import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; import "../mixins/SemVerMixin.sol"; import "./StrategyFactoryStorage.sol"; import "./StrategyBase.sol"; +import "./DurationVaultStrategy.sol"; +import "../interfaces/IDurationVaultStrategy.sol"; import "../permissions/Pausable.sol"; /** @@ -110,6 +112,42 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist); } + /** + * @notice Deploys a new duration vault strategy backed by the configured beacon. + */ + function deployDurationVaultStrategy( + IDurationVaultStrategy.VaultConfig calldata config + ) external onlyWhenNotPaused(PAUSED_NEW_STRATEGIES) returns (IDurationVaultStrategy newVault) { + IERC20 underlyingToken = config.underlyingToken; + require(!isBlacklisted[underlyingToken], BlacklistedToken()); + require(address(durationVaultBeacon) != address(0), DurationVaultBeaconNotSet()); + + newVault = IDurationVaultStrategy( + address( + new BeaconProxy( + address(durationVaultBeacon), abi.encodeWithSelector(DurationVaultStrategy.initialize.selector, config) + ) + ) + ); + + _registerDurationVault(underlyingToken, newVault); + IStrategy[] memory strategiesToWhitelist = new IStrategy[](1); + strategiesToWhitelist[0] = newVault; + strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist); + + emit DurationVaultDeployed( + newVault, + underlyingToken, + config.vaultAdmin, + config.depositWindowStart, + config.depositWindowEnd, + config.duration, + config.maxPerDeposit, + config.stakeCap, + config.metadataURI + ); + } + /** * @notice Owner-only function to pass through a call to `StrategyManager.removeStrategiesFromDepositWhitelist` */ @@ -130,4 +168,34 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable emit StrategyBeaconModified(strategyBeacon, _strategyBeacon); strategyBeacon = _strategyBeacon; } + + /** + * @notice Owner-only function to update the duration vault beacon. + */ + function setDurationVaultBeacon( + IBeacon newDurationVaultBeacon + ) external onlyOwner { + _setDurationVaultBeacon(newDurationVaultBeacon); + } + + /// @inheritdoc IStrategyFactory + function getDurationVaults( + IERC20 token + ) external view returns (IDurationVaultStrategy[] memory) { + return durationVaultsByToken[token]; + } + + function _setDurationVaultBeacon( + IBeacon newDurationVaultBeacon + ) internal { + emit DurationVaultBeaconModified(durationVaultBeacon, newDurationVaultBeacon); + durationVaultBeacon = newDurationVaultBeacon; + } + + function _registerDurationVault( + IERC20 token, + IDurationVaultStrategy vault + ) internal { + durationVaultsByToken[token].push(vault); + } } diff --git a/src/contracts/strategies/StrategyFactoryStorage.sol b/src/contracts/strategies/StrategyFactoryStorage.sol index c74cf7eb23..58c4789b13 100644 --- a/src/contracts/strategies/StrategyFactoryStorage.sol +++ b/src/contracts/strategies/StrategyFactoryStorage.sol @@ -9,9 +9,12 @@ import "../interfaces/IStrategyFactory.sol"; * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service */ abstract contract StrategyFactoryStorage is IStrategyFactory { - /// @notice Upgradeable beacon which new Strategies deployed by this contract point to + /// @notice Upgradeable beacon used for baseline strategies deployed by this contract. IBeacon public strategyBeacon; + /// @notice Upgradeable beacon used for duration vault strategies deployed by this contract. + IBeacon public durationVaultBeacon; + /// @notice Mapping token => Strategy contract for the token /// The strategies in this mapping are deployed by the StrategyFactory. /// The factory can only deploy a single strategy per token address @@ -24,10 +27,13 @@ abstract contract StrategyFactoryStorage is IStrategyFactory { /// @notice Mapping token => Whether or not a strategy can be deployed for the token mapping(IERC20 => bool) public isBlacklisted; + /// @notice Mapping token => all duration vault strategies deployed for the token. + mapping(IERC20 => IDurationVaultStrategy[]) internal durationVaultsByToken; + /** * @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. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[48] private __gap; + uint256[46] private __gap; } diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol new file mode 100644 index 0000000000..ffcc690f46 --- /dev/null +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "./StrategyBaseTVLLimitsUnit.sol"; +import "../../contracts/strategies/DurationVaultStrategy.sol"; +import "../../contracts/interfaces/IDurationVaultStrategy.sol"; + +contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { + DurationVaultStrategy public durationVaultImplementation; + DurationVaultStrategy public durationVault; + + uint64 internal defaultDuration = 30 days; + uint64 internal defaultDepositWindowLength = 7 days; + + function setUp() public virtual override { + StrategyBaseUnitTests.setUp(); + + durationVaultImplementation = new DurationVaultStrategy(strategyManager, pauserRegistry, "9.9.9"); + + IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ + underlyingToken: underlyingToken, + vaultAdmin: address(this), + depositWindowStart: 0, + depositWindowEnd: uint64(block.timestamp + defaultDepositWindowLength), + duration: defaultDuration, + maxPerDeposit: maxPerDeposit, + stakeCap: maxTotalDeposits, + metadataURI: "ipfs://duration-vault" + }); + + durationVault = DurationVaultStrategy( + address( + new TransparentUpgradeableProxy( + address(durationVaultImplementation), + address(proxyAdmin), + abi.encodeWithSelector(DurationVaultStrategy.initialize.selector, config) + ) + ) + ); + + strategy = StrategyBase(address(durationVault)); + strategyWithTVLLimits = StrategyBaseTVLLimits(address(durationVault)); + } + + function testDepositWindowNotStarted() public { + // reconfigure deposit window to start in future + durationVault.updateDepositWindow(uint64(block.timestamp + 1 hours), uint64(block.timestamp + 2 hours)); + + uint256 depositAmount = 1e18; + underlyingToken.transfer(address(durationVault), depositAmount); + + cheats.prank(address(strategyManager)); + cheats.expectRevert(IDurationVaultStrategy.DepositWindowNotStarted.selector); + durationVault.deposit(underlyingToken, depositAmount); + } + + function testDepositWindowClosedAfterEnd() public { + cheats.warp(block.timestamp + defaultDepositWindowLength + 1); + + uint256 depositAmount = 1e18; + underlyingToken.transfer(address(durationVault), depositAmount); + + cheats.prank(address(strategyManager)); + cheats.expectRevert(IDurationVaultStrategy.DepositWindowClosed.selector); + durationVault.deposit(underlyingToken, depositAmount); + } + + function testDepositsBlockedAfterManualLock() public { + durationVault.lock(); + + uint256 depositAmount = 1e18; + + underlyingToken.transfer(address(durationVault), depositAmount); + cheats.prank(address(strategyManager)); + cheats.expectRevert(IDurationVaultStrategy.DepositWindowClosed.selector); + durationVault.deposit(underlyingToken, depositAmount); + } + + function testWithdrawalsBlockedUntilMaturity() public { + // prepare deposit + uint256 depositAmount = 10 ether; + underlyingToken.transfer(address(durationVault), depositAmount); + cheats.prank(address(strategyManager)); + durationVault.deposit(underlyingToken, depositAmount); + + durationVault.lock(); + + assertTrue(durationVault.isLocked(), "vault should be locked"); + assertFalse(durationVault.withdrawalsOpen(), "withdrawals should be closed before maturity"); + + uint256 shares = durationVault.totalShares(); + + // Attempt withdrawal before maturity + cheats.startPrank(address(strategyManager)); + cheats.expectRevert(IDurationVaultStrategy.WithdrawalsLocked.selector); + durationVault.withdraw(address(this), underlyingToken, shares); + cheats.stopPrank(); + + cheats.warp(block.timestamp + defaultDuration + 1); + + cheats.startPrank(address(strategyManager)); + durationVault.withdraw(address(this), underlyingToken, durationVault.totalShares()); + cheats.stopPrank(); + } +} + diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index 65f47e783d..49e4be7e8d 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -5,6 +5,8 @@ import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "src/contracts/strategies/StrategyFactory.sol"; +import "src/contracts/strategies/DurationVaultStrategy.sol"; +import "../../contracts/interfaces/IDurationVaultStrategy.sol"; import "src/test/utils/EigenLayerUnitTestSetup.sol"; import "../../contracts/permissions/PauserRegistry.sol"; @@ -19,7 +21,9 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { // Contract dependencies StrategyBase public strategyImplementation; + DurationVaultStrategy public durationVaultImplementation; UpgradeableBeacon public strategyBeacon; + UpgradeableBeacon public durationVaultBeacon; ERC20PresetFixedSupply public underlyingToken; uint initialSupply = 1e36; @@ -51,6 +55,11 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { strategyBeacon = new UpgradeableBeacon(address(strategyImplementation)); strategyBeacon.transferOwnership(beaconProxyOwner); + durationVaultImplementation = + new DurationVaultStrategy(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "9.9.9"); + durationVaultBeacon = new UpgradeableBeacon(address(durationVaultImplementation)); + durationVaultBeacon.transferOwnership(beaconProxyOwner); + strategyFactoryImplementation = new StrategyFactory(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "9.9.9"); strategyFactory = StrategyFactory( @@ -62,6 +71,8 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { ) ) ); + + strategyFactory.setDurationVaultBeacon(durationVaultBeacon); } function test_initialization() public view { @@ -114,6 +125,71 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { strategyFactory.deployNewStrategy(underlyingToken); } + function test_deployDurationVaultStrategy() public { + IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ + underlyingToken: underlyingToken, + vaultAdmin: address(this), + depositWindowStart: 0, + depositWindowEnd: 0, + duration: 30 days, + maxPerDeposit: 10 ether, + stakeCap: 100 ether, + metadataURI: "ipfs://duration" + }); + + DurationVaultStrategy vault = DurationVaultStrategy( + address(strategyFactory.deployDurationVaultStrategy(config)) + ); + + IDurationVaultStrategy[] memory deployedVaults = strategyFactory.getDurationVaults(underlyingToken); + require(deployedVaults.length == 1, "vault not tracked"); + require(address(deployedVaults[0]) == address(vault), "vault mismatch"); + assertTrue(strategyManagerMock.strategyIsWhitelistedForDeposit(vault), "duration vault not whitelisted"); + } + + function test_deployDurationVaultStrategy_revertBeaconNotSet() public { + strategyFactory.setDurationVaultBeacon(IBeacon(address(0))); + + IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ + underlyingToken: underlyingToken, + vaultAdmin: address(this), + depositWindowStart: 0, + depositWindowEnd: 0, + duration: 30 days, + maxPerDeposit: 10 ether, + stakeCap: 100 ether, + metadataURI: "ipfs://duration" + }); + + cheats.expectRevert(IStrategyFactory.DurationVaultBeaconNotSet.selector); + strategyFactory.deployDurationVaultStrategy(config); + } + + function test_deployDurationVaultStrategy_withExistingStrategy() public { + StrategyBase base = StrategyBase(address(strategyFactory.deployNewStrategy(underlyingToken))); + require(strategyFactory.deployedStrategies(underlyingToken) == base, "base strategy missing"); + + IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ + underlyingToken: underlyingToken, + vaultAdmin: address(this), + depositWindowStart: 0, + depositWindowEnd: 0, + duration: 7 days, + maxPerDeposit: 5 ether, + stakeCap: 50 ether, + metadataURI: "ipfs://duration" + }); + + strategyFactory.deployDurationVaultStrategy(config); + + IDurationVaultStrategy[] memory deployedVaults = strategyFactory.getDurationVaults(underlyingToken); + require(deployedVaults.length == 1, "duration vault missing"); + assertTrue(strategyManagerMock.strategyIsWhitelistedForDeposit(IDurationVaultStrategy(address(deployedVaults[0]))), "vault not whitelisted"); + + // Base mapping should remain untouched. + require(strategyFactory.deployedStrategies(underlyingToken) == base, "base strategy overwritten"); + } + function test_blacklistTokens(IERC20 token) public { IERC20[] memory tokens = new IERC20[](1); tokens[0] = token; diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol new file mode 100644 index 0000000000..7e72dd5b6f --- /dev/null +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + +import "src/contracts/core/StrategyManager.sol"; +import "src/contracts/strategies/DurationVaultStrategy.sol"; +import "src/contracts/interfaces/IDurationVaultStrategy.sol"; +import "src/contracts/interfaces/IStrategy.sol"; +import "src/test/utils/EigenLayerUnitTestSetup.sol"; + +contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyManagerEvents { + StrategyManager public strategyManagerImplementation; + StrategyManager public strategyManager; + + DurationVaultStrategy public durationVaultImplementation; + IDurationVaultStrategy public durationVault; + + ERC20PresetFixedSupply public underlyingToken; + + address internal constant STAKER = address(0xBEEF); + uint256 internal constant INITIAL_SUPPLY = 1e36; + + function setUp() public override { + EigenLayerUnitTestSetup.setUp(); + + strategyManagerImplementation = new StrategyManager( + IAllocationManager(address(allocationManagerMock)), IDelegationManager(address(delegationManagerMock)), pauserRegistry, "9.9.9" + ); + + strategyManager = StrategyManager( + address( + new TransparentUpgradeableProxy( + address(strategyManagerImplementation), + address(eigenLayerProxyAdmin), + abi.encodeWithSelector(StrategyManager.initialize.selector, address(this), address(this), 0) + ) + ) + ); + + underlyingToken = new ERC20PresetFixedSupply("Mock Token", "MOCK", INITIAL_SUPPLY, address(this)); + + durationVaultImplementation = new DurationVaultStrategy( + IStrategyManager(address(strategyManager)), pauserRegistry, "9.9.9" + ); + + IDurationVaultStrategy.VaultConfig memory cfg = IDurationVaultStrategy.VaultConfig({ + underlyingToken: IERC20(address(underlyingToken)), + vaultAdmin: address(this), + depositWindowStart: 0, + depositWindowEnd: 0, + duration: 30 days, + maxPerDeposit: 1_000_000 ether, + stakeCap: 10_000_000 ether, + metadataURI: "ipfs://duration-vault-test" + }); + + durationVault = IDurationVaultStrategy( + address( + new TransparentUpgradeableProxy( + address(durationVaultImplementation), + address(eigenLayerProxyAdmin), + abi.encodeWithSelector(DurationVaultStrategy.initialize.selector, cfg) + ) + ) + ); + + IStrategy[] memory whitelist = new IStrategy[](1); + whitelist[0] = IStrategy(address(durationVault)); + + cheats.prank(strategyManager.owner()); + strategyManager.addStrategiesToDepositWhitelist(whitelist); + } + + function testDepositIntoDurationVaultViaStrategyManager() public { + uint256 amount = 10 ether; + underlyingToken.transfer(STAKER, amount); + + cheats.startPrank(STAKER); + underlyingToken.approve(address(strategyManager), amount); + cheats.expectEmit(true, true, true, true, address(strategyManager)); + emit Deposit(STAKER, IStrategy(address(durationVault)), amount); + strategyManager.depositIntoStrategy(IStrategy(address(durationVault)), IERC20(address(underlyingToken)), amount); + cheats.stopPrank(); + + uint256 shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); + assertEq(shares, amount, "staker shares mismatch"); + } + + function testDepositRevertsAfterVaultLock() public { + durationVault.lock(); + + uint256 amount = 5 ether; + underlyingToken.transfer(STAKER, amount); + + cheats.startPrank(STAKER); + underlyingToken.approve(address(strategyManager), amount); + cheats.expectRevert(IDurationVaultStrategy.DepositWindowClosed.selector); + strategyManager.depositIntoStrategy(IStrategy(address(durationVault)), IERC20(address(underlyingToken)), amount); + cheats.stopPrank(); + } + + function _depositFor(address staker, uint256 amount) internal { + underlyingToken.transfer(staker, amount); + cheats.startPrank(staker); + underlyingToken.approve(address(strategyManager), amount); + strategyManager.depositIntoStrategy(IStrategy(address(durationVault)), IERC20(address(underlyingToken)), amount); + cheats.stopPrank(); + } + + function testWithdrawalsBlockedViaStrategyManagerBeforeMaturity() public { + uint256 amount = 8 ether; + _depositFor(STAKER, amount); + durationVault.lock(); + + uint256 shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); + + cheats.prank(address(delegationManagerMock)); + cheats.expectRevert(IDurationVaultStrategy.WithdrawalsLocked.selector); + strategyManager.withdrawSharesAsTokens( + STAKER, IStrategy(address(durationVault)), IERC20(address(underlyingToken)), shares + ); + } + + function testWithdrawalsAllowedAfterMaturity() public { + uint256 amount = 6 ether; + _depositFor(STAKER, amount); + durationVault.lock(); + + cheats.warp(block.timestamp + durationVault.duration() + 1); + + uint256 shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); + + cheats.prank(address(delegationManagerMock)); + strategyManager.removeDepositShares(STAKER, IStrategy(address(durationVault)), shares); + + uint256 balanceBefore = underlyingToken.balanceOf(STAKER); + + cheats.prank(address(delegationManagerMock)); + strategyManager.withdrawSharesAsTokens( + STAKER, IStrategy(address(durationVault)), IERC20(address(underlyingToken)), shares + ); + + assertEq(strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))), 0, "shares should be zero after removal"); + assertEq( + underlyingToken.balanceOf(STAKER), + balanceBefore + amount, + "staker did not receive withdrawn tokens" + ); + } +} + From 66c3316e2678a31faf306d21c87212b4be0d1937 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Dec 2025 12:56:17 -0500 Subject: [PATCH 02/18] chore: fmt --- .../interfaces/IDurationVaultStrategy.sol | 6 +----- .../strategies/DurationVaultStrategy.sol | 17 +++-------------- src/contracts/strategies/StrategyFactory.sol | 8 +++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index 001bbf2352..d0149d7131 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -85,10 +85,7 @@ interface IDurationVaultStrategy is IStrategy { /** * @notice Updates the deposit window bounds. Only callable before the vault is locked. */ - function updateDepositWindow( - uint64 newStart, - uint64 newEnd - ) external; + function updateDepositWindow(uint64 newStart, uint64 newEnd) external; /** * @notice Transfers vault admin privileges to a new address. @@ -110,4 +107,3 @@ interface IDurationVaultStrategy is IStrategy { function depositsOpen() external view returns (bool); function withdrawalsOpen() external view returns (bool); } - diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index cc7bf2efeb..1421e45f04 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -122,10 +122,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy /** * @notice Updates the deposit window bounds. Cannot be called after locking. */ - function updateDepositWindow( - uint64 newStart, - uint64 newEnd - ) external override onlyVaultAdmin { + function updateDepositWindow(uint64 newStart, uint64 newEnd) external override onlyVaultAdmin { if (lockedAt != 0) revert VaultAlreadyLocked(); if (newEnd != 0 && newEnd <= newStart) revert InvalidDepositWindow(); depositWindowStart = newStart; @@ -196,10 +193,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy return true; } - function _beforeDeposit( - IERC20 token, - uint256 amount - ) internal virtual override { + function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override { if (!isLocked()) { uint64 start = depositWindowStart; if (start != 0 && block.timestamp < start) revert DepositWindowNotStarted(); @@ -212,11 +206,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy super._beforeDeposit(token, amount); } - function _beforeWithdrawal( - address recipient, - IERC20 token, - uint256 amountShares - ) internal virtual override { + function _beforeWithdrawal(address recipient, IERC20 token, uint256 amountShares) internal virtual override { if (isLocked() && !isMatured()) { revert WithdrawalsLocked(); } @@ -229,4 +219,3 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy */ uint256[40] private __gap; } - diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index dc7ae912f5..abf7c83299 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -125,7 +125,8 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable newVault = IDurationVaultStrategy( address( new BeaconProxy( - address(durationVaultBeacon), abi.encodeWithSelector(DurationVaultStrategy.initialize.selector, config) + address(durationVaultBeacon), + abi.encodeWithSelector(DurationVaultStrategy.initialize.selector, config) ) ) ); @@ -192,10 +193,7 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable durationVaultBeacon = newDurationVaultBeacon; } - function _registerDurationVault( - IERC20 token, - IDurationVaultStrategy vault - ) internal { + function _registerDurationVault(IERC20 token, IDurationVaultStrategy vault) internal { durationVaultsByToken[token].push(vault); } } From f18ac880ecbf94e94fbe2b3c60233660c66fbfc1 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Dec 2025 12:57:00 -0500 Subject: [PATCH 03/18] chore: fmt --- src/test/unit/DurationVaultStrategyUnit.t.sol | 11 +++--- src/test/unit/StrategyFactoryUnit.t.sol | 11 +++--- .../unit/StrategyManagerDurationUnit.t.sol | 39 +++++++------------ 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol index ffcc690f46..5b121f00cb 100644 --- a/src/test/unit/DurationVaultStrategyUnit.t.sol +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -46,7 +46,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { // reconfigure deposit window to start in future durationVault.updateDepositWindow(uint64(block.timestamp + 1 hours), uint64(block.timestamp + 2 hours)); - uint256 depositAmount = 1e18; + uint depositAmount = 1e18; underlyingToken.transfer(address(durationVault), depositAmount); cheats.prank(address(strategyManager)); @@ -57,7 +57,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { function testDepositWindowClosedAfterEnd() public { cheats.warp(block.timestamp + defaultDepositWindowLength + 1); - uint256 depositAmount = 1e18; + uint depositAmount = 1e18; underlyingToken.transfer(address(durationVault), depositAmount); cheats.prank(address(strategyManager)); @@ -68,7 +68,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { function testDepositsBlockedAfterManualLock() public { durationVault.lock(); - uint256 depositAmount = 1e18; + uint depositAmount = 1e18; underlyingToken.transfer(address(durationVault), depositAmount); cheats.prank(address(strategyManager)); @@ -78,7 +78,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { function testWithdrawalsBlockedUntilMaturity() public { // prepare deposit - uint256 depositAmount = 10 ether; + uint depositAmount = 10 ether; underlyingToken.transfer(address(durationVault), depositAmount); cheats.prank(address(strategyManager)); durationVault.deposit(underlyingToken, depositAmount); @@ -88,7 +88,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { assertTrue(durationVault.isLocked(), "vault should be locked"); assertFalse(durationVault.withdrawalsOpen(), "withdrawals should be closed before maturity"); - uint256 shares = durationVault.totalShares(); + uint shares = durationVault.totalShares(); // Attempt withdrawal before maturity cheats.startPrank(address(strategyManager)); @@ -103,4 +103,3 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { cheats.stopPrank(); } } - diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index 49e4be7e8d..7007257ab5 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -55,8 +55,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { strategyBeacon = new UpgradeableBeacon(address(strategyImplementation)); strategyBeacon.transferOwnership(beaconProxyOwner); - durationVaultImplementation = - new DurationVaultStrategy(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "9.9.9"); + durationVaultImplementation = new DurationVaultStrategy(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "9.9.9"); durationVaultBeacon = new UpgradeableBeacon(address(durationVaultImplementation)); durationVaultBeacon.transferOwnership(beaconProxyOwner); @@ -137,9 +136,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { metadataURI: "ipfs://duration" }); - DurationVaultStrategy vault = DurationVaultStrategy( - address(strategyFactory.deployDurationVaultStrategy(config)) - ); + DurationVaultStrategy vault = DurationVaultStrategy(address(strategyFactory.deployDurationVaultStrategy(config))); IDurationVaultStrategy[] memory deployedVaults = strategyFactory.getDurationVaults(underlyingToken); require(deployedVaults.length == 1, "vault not tracked"); @@ -184,7 +181,9 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy[] memory deployedVaults = strategyFactory.getDurationVaults(underlyingToken); require(deployedVaults.length == 1, "duration vault missing"); - assertTrue(strategyManagerMock.strategyIsWhitelistedForDeposit(IDurationVaultStrategy(address(deployedVaults[0]))), "vault not whitelisted"); + assertTrue( + strategyManagerMock.strategyIsWhitelistedForDeposit(IDurationVaultStrategy(address(deployedVaults[0]))), "vault not whitelisted" + ); // Base mapping should remain untouched. require(strategyFactory.deployedStrategies(underlyingToken) == base, "base strategy overwritten"); diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index 7e72dd5b6f..02fc5edf4e 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -21,7 +21,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM ERC20PresetFixedSupply public underlyingToken; address internal constant STAKER = address(0xBEEF); - uint256 internal constant INITIAL_SUPPLY = 1e36; + uint internal constant INITIAL_SUPPLY = 1e36; function setUp() public override { EigenLayerUnitTestSetup.setUp(); @@ -42,9 +42,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM underlyingToken = new ERC20PresetFixedSupply("Mock Token", "MOCK", INITIAL_SUPPLY, address(this)); - durationVaultImplementation = new DurationVaultStrategy( - IStrategyManager(address(strategyManager)), pauserRegistry, "9.9.9" - ); + durationVaultImplementation = new DurationVaultStrategy(IStrategyManager(address(strategyManager)), pauserRegistry, "9.9.9"); IDurationVaultStrategy.VaultConfig memory cfg = IDurationVaultStrategy.VaultConfig({ underlyingToken: IERC20(address(underlyingToken)), @@ -75,7 +73,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM } function testDepositIntoDurationVaultViaStrategyManager() public { - uint256 amount = 10 ether; + uint amount = 10 ether; underlyingToken.transfer(STAKER, amount); cheats.startPrank(STAKER); @@ -85,14 +83,14 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM strategyManager.depositIntoStrategy(IStrategy(address(durationVault)), IERC20(address(underlyingToken)), amount); cheats.stopPrank(); - uint256 shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); + uint shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); assertEq(shares, amount, "staker shares mismatch"); } function testDepositRevertsAfterVaultLock() public { durationVault.lock(); - uint256 amount = 5 ether; + uint amount = 5 ether; underlyingToken.transfer(STAKER, amount); cheats.startPrank(STAKER); @@ -102,7 +100,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM cheats.stopPrank(); } - function _depositFor(address staker, uint256 amount) internal { + function _depositFor(address staker, uint amount) internal { underlyingToken.transfer(staker, amount); cheats.startPrank(staker); underlyingToken.approve(address(strategyManager), amount); @@ -111,44 +109,35 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM } function testWithdrawalsBlockedViaStrategyManagerBeforeMaturity() public { - uint256 amount = 8 ether; + uint amount = 8 ether; _depositFor(STAKER, amount); durationVault.lock(); - uint256 shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); + uint shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); cheats.prank(address(delegationManagerMock)); cheats.expectRevert(IDurationVaultStrategy.WithdrawalsLocked.selector); - strategyManager.withdrawSharesAsTokens( - STAKER, IStrategy(address(durationVault)), IERC20(address(underlyingToken)), shares - ); + strategyManager.withdrawSharesAsTokens(STAKER, IStrategy(address(durationVault)), IERC20(address(underlyingToken)), shares); } function testWithdrawalsAllowedAfterMaturity() public { - uint256 amount = 6 ether; + uint amount = 6 ether; _depositFor(STAKER, amount); durationVault.lock(); cheats.warp(block.timestamp + durationVault.duration() + 1); - uint256 shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); + uint shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); cheats.prank(address(delegationManagerMock)); strategyManager.removeDepositShares(STAKER, IStrategy(address(durationVault)), shares); - uint256 balanceBefore = underlyingToken.balanceOf(STAKER); + uint balanceBefore = underlyingToken.balanceOf(STAKER); cheats.prank(address(delegationManagerMock)); - strategyManager.withdrawSharesAsTokens( - STAKER, IStrategy(address(durationVault)), IERC20(address(underlyingToken)), shares - ); + strategyManager.withdrawSharesAsTokens(STAKER, IStrategy(address(durationVault)), IERC20(address(underlyingToken)), shares); assertEq(strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))), 0, "shares should be zero after removal"); - assertEq( - underlyingToken.balanceOf(STAKER), - balanceBefore + amount, - "staker did not receive withdrawn tokens" - ); + assertEq(underlyingToken.balanceOf(STAKER), balanceBefore + amount, "staker did not receive withdrawn tokens"); } } - From a1fa83ba68f2df5b0b3adc8b7d59aa51008c8364 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Dec 2025 13:16:38 -0500 Subject: [PATCH 04/18] fix: fix storage layout --- src/contracts/strategies/StrategyFactoryStorage.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/strategies/StrategyFactoryStorage.sol b/src/contracts/strategies/StrategyFactoryStorage.sol index 58c4789b13..a631e4f3ec 100644 --- a/src/contracts/strategies/StrategyFactoryStorage.sol +++ b/src/contracts/strategies/StrategyFactoryStorage.sol @@ -12,9 +12,6 @@ abstract contract StrategyFactoryStorage is IStrategyFactory { /// @notice Upgradeable beacon used for baseline strategies deployed by this contract. IBeacon public strategyBeacon; - /// @notice Upgradeable beacon used for duration vault strategies deployed by this contract. - IBeacon public durationVaultBeacon; - /// @notice Mapping token => Strategy contract for the token /// The strategies in this mapping are deployed by the StrategyFactory. /// The factory can only deploy a single strategy per token address @@ -27,6 +24,9 @@ abstract contract StrategyFactoryStorage is IStrategyFactory { /// @notice Mapping token => Whether or not a strategy can be deployed for the token mapping(IERC20 => bool) public isBlacklisted; + /// @notice Upgradeable beacon used for duration vault strategies deployed by this contract. + IBeacon public durationVaultBeacon; + /// @notice Mapping token => all duration vault strategies deployed for the token. mapping(IERC20 => IDurationVaultStrategy[]) internal durationVaultsByToken; From 80efea9e02a943a33b252e3d047d7c9e7aa7b65b Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Dec 2025 20:08:45 -0500 Subject: [PATCH 05/18] refactor: AM integration --- .../interfaces/IDurationVaultStrategy.sol | 37 ++-- src/contracts/interfaces/IStrategyFactory.sol | 7 +- .../strategies/DurationVaultStrategy.sol | 161 ++++++++++++------ src/contracts/strategies/StrategyFactory.sol | 37 ++-- src/test/mocks/AllocationManagerMock.sol | 81 +++++++++ src/test/mocks/DelegationManagerMock.sol | 25 +++ src/test/unit/DurationVaultStrategyUnit.t.sol | 98 ++++++++--- src/test/unit/StrategyFactoryUnit.t.sol | 44 ++++- .../unit/StrategyManagerDurationUnit.t.sol | 22 ++- 9 files changed, 394 insertions(+), 118 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index d0149d7131..3796d6f569 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IStrategy.sol"; +import "./IDelegationManager.sol"; +import "./IAllocationManager.sol"; /** * @title Interface for time-bound EigenLayer vault strategies. @@ -14,30 +16,32 @@ interface IDurationVaultStrategy is IStrategy { struct VaultConfig { IERC20 underlyingToken; address vaultAdmin; - uint64 depositWindowStart; - uint64 depositWindowEnd; uint64 duration; uint256 maxPerDeposit; uint256 stakeCap; string metadataURI; + IDelegationManager delegationManager; + IAllocationManager allocationManager; + address operatorSetAVS; + uint32 operatorSetId; + bytes operatorSetRegistrationData; + address delegationApprover; + uint32 operatorAllocationDelay; + string operatorMetadataURI; } /// @dev Thrown when attempting to use a zero-address vault admin. error InvalidVaultAdmin(); /// @dev Thrown when attempting to configure a zero duration. error InvalidDuration(); - /// @dev Thrown when attempting to configure an invalid deposit window. - error InvalidDepositWindow(); /// @dev Thrown when attempting to mutate configuration from a non-admin. error OnlyVaultAdmin(); /// @dev Thrown when attempting to lock an already locked vault. error VaultAlreadyLocked(); /// @dev Thrown when attempting to unlock or update a vault that has not been locked yet. error VaultNotLocked(); - /// @dev Thrown when attempting to deposit before the window opens. - error DepositWindowNotStarted(); - /// @dev Thrown when attempting to deposit after the window closes (either manually or via lock). - error DepositWindowClosed(); + /// @dev Thrown when attempting to deposit after the vault has been locked. + error DepositsLocked(); /// @dev Thrown when attempting to withdraw while funds remain locked. error WithdrawalsLocked(); /// @dev Thrown when attempting to mark the vault as matured before duration elapses. @@ -46,8 +50,6 @@ interface IDurationVaultStrategy is IStrategy { event VaultInitialized( address indexed vaultAdmin, IERC20 indexed underlyingToken, - uint64 depositWindowStart, - uint64 depositWindowEnd, uint64 duration, uint256 maxPerDeposit, uint256 stakeCap, @@ -60,8 +62,6 @@ interface IDurationVaultStrategy is IStrategy { event VaultAdminUpdated(address indexed previousAdmin, address indexed newAdmin); - event DepositWindowUpdated(uint64 newStart, uint64 newEnd); - event MetadataURIUpdated(string newMetadataURI); /** @@ -82,11 +82,6 @@ interface IDurationVaultStrategy is IStrategy { string calldata newMetadataURI ) external; - /** - * @notice Updates the deposit window bounds. Only callable before the vault is locked. - */ - function updateDepositWindow(uint64 newStart, uint64 newEnd) external; - /** * @notice Transfers vault admin privileges to a new address. */ @@ -95,8 +90,6 @@ interface IDurationVaultStrategy is IStrategy { ) external; function vaultAdmin() external view returns (address); - function depositWindowStart() external view returns (uint64); - function depositWindowEnd() external view returns (uint64); function duration() external view returns (uint64); function lockedAt() external view returns (uint64); function unlockTimestamp() external view returns (uint64); @@ -106,4 +99,10 @@ interface IDurationVaultStrategy is IStrategy { function stakeCap() external view returns (uint256); function depositsOpen() external view returns (bool); function withdrawalsOpen() external view returns (bool); + function delegationManager() external view returns (IDelegationManager); + function allocationManager() external view returns (IAllocationManager); + function operatorIntegrationConfigured() external view returns (bool); + function operatorSetRegistered() external view returns (bool); + function allocationsActive() external view returns (bool); + function operatorSetInfo() external view returns (address avs, uint32 operatorSetId); } diff --git a/src/contracts/interfaces/IStrategyFactory.sol b/src/contracts/interfaces/IStrategyFactory.sol index c367615199..b709877e58 100644 --- a/src/contracts/interfaces/IStrategyFactory.sol +++ b/src/contracts/interfaces/IStrategyFactory.sol @@ -103,11 +103,12 @@ interface IStrategyFactory is ISemVerMixin { IDurationVaultStrategy vault, IERC20 indexed underlyingToken, address indexed vaultAdmin, - uint64 depositWindowStart, - uint64 depositWindowEnd, uint64 duration, uint256 maxPerDeposit, uint256 stakeCap, - string metadataURI + string metadataURI, + address operatorSetAVS, + uint32 operatorSetId, + bool operatorIntegrationEnabled ); } diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index 1421e45f04..72732254c7 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.27; import "./StrategyBaseTVLLimits.sol"; import "../interfaces/IDurationVaultStrategy.sol"; +import "../interfaces/IDelegationManager.sol"; +import "../interfaces/IAllocationManager.sol"; +import "../libraries/OperatorSetLib.sol"; /** * @title Duration-bound EigenLayer vault strategy with configurable deposit caps and windows. @@ -10,15 +13,10 @@ import "../interfaces/IDurationVaultStrategy.sol"; * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service */ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy { + using OperatorSetLib for OperatorSet; /// @notice Address empowered to configure and lock the vault. address public vaultAdmin; - /// @notice Timestamp when the deposit window opens. Zero means "immediately open". - uint64 public depositWindowStart; - - /// @notice Timestamp when the deposit window closes. Zero means "no enforced close" until lock(). - uint64 public depositWindowEnd; - /// @notice The enforced lock duration once `lock` is called. uint64 public duration; @@ -34,6 +32,26 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy /// @notice Optional metadata URI describing the vault configuration. string public metadataURI; + /// @notice Delegation manager reference used to register the vault as an operator. + IDelegationManager public delegationManager; + + /// @notice Allocation manager reference used to register/allocate operator sets. + IAllocationManager public allocationManager; + + /// @notice Stored operator set metadata for integration with the allocation manager. + OperatorSet internal _operatorSet; + + /// @notice True when allocations are currently active (i.e. slashable) for the configured operator set. + bool public allocationsActive; + + /// @notice True when the vault remains registered for the operator set. + bool public operatorSetRegistered; + + /// @notice Constant representing the full allocation magnitude (1 WAD) for allocation manager calls. + uint64 internal constant FULL_ALLOCATION = 1e18; + + error OperatorIntegrationInvalid(); + modifier onlyVaultAdmin() { if (msg.sender != vaultAdmin) revert OnlyVaultAdmin(); _; @@ -53,24 +71,18 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy ) public initializer { if (config.vaultAdmin == address(0)) revert InvalidVaultAdmin(); if (config.duration == 0) revert InvalidDuration(); - if (config.depositWindowEnd != 0 && config.depositWindowEnd <= config.depositWindowStart) { - revert InvalidDepositWindow(); - } - _setTVLLimits(config.maxPerDeposit, config.stakeCap); _initializeStrategyBase(config.underlyingToken); vaultAdmin = config.vaultAdmin; - depositWindowStart = config.depositWindowStart; - depositWindowEnd = config.depositWindowEnd; duration = config.duration; metadataURI = config.metadataURI; + _configureOperatorIntegration(config); + emit VaultInitialized( vaultAdmin, config.underlyingToken, - depositWindowStart, - depositWindowEnd, duration, config.maxPerDeposit, config.stakeCap, @@ -83,17 +95,17 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy */ function lock() external override onlyVaultAdmin { if (lockedAt != 0) revert VaultAlreadyLocked(); - if (depositWindowStart != 0 && block.timestamp < depositWindowStart) revert DepositWindowNotStarted(); lockedAt = uint64(block.timestamp); uint256 rawUnlockTimestamp = uint256(lockedAt) + uint256(duration); require(rawUnlockTimestamp <= type(uint64).max, InvalidDuration()); unlockAt = uint64(rawUnlockTimestamp); - // Closing the deposit window at the lock time ensures future deposits revert even if no explicit end was set. - depositWindowEnd = lockedAt; - emit VaultLocked(lockedAt, unlockAt); + + if (!allocationsActive) { + _allocateFullMagnitude(); + } } /** @@ -107,6 +119,14 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy } maturedAt = uint64(block.timestamp); emit VaultMatured(maturedAt); + + if (allocationsActive) { + _deallocateAll(); + allocationsActive = false; + } + if (operatorSetRegistered) { + _deregisterFromOperatorSet(); + } } /** @@ -119,17 +139,6 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy emit MetadataURIUpdated(newMetadataURI); } - /** - * @notice Updates the deposit window bounds. Cannot be called after locking. - */ - function updateDepositWindow(uint64 newStart, uint64 newEnd) external override onlyVaultAdmin { - if (lockedAt != 0) revert VaultAlreadyLocked(); - if (newEnd != 0 && newEnd <= newStart) revert InvalidDepositWindow(); - depositWindowStart = newStart; - depositWindowEnd = newEnd; - emit DepositWindowUpdated(newStart, newEnd); - } - /** * @notice Transfers admin privileges to a new address. */ @@ -167,7 +176,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy /// @inheritdoc IDurationVaultStrategy function depositsOpen() public view override returns (bool) { - return _depositsWithinWindow() && !isLocked(); + return !isLocked(); } /// @inheritdoc IDurationVaultStrategy @@ -178,31 +187,20 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy return isMatured(); } - /** - * @notice Internal helper verifying deposit timing constraints. - */ - function _depositsWithinWindow() internal view returns (bool) { - uint64 start = depositWindowStart; - uint64 end = depositWindowEnd; - if (start != 0 && block.timestamp < start) { - return false; - } - if (end != 0 && block.timestamp > end) { - return false; - } + /// @inheritdoc IDurationVaultStrategy + function operatorIntegrationConfigured() public pure override returns (bool) { return true; } + /// @inheritdoc IDurationVaultStrategy + function operatorSetInfo() external view override returns (address avs, uint32 operatorSetId) { + return (_operatorSet.avs, _operatorSet.id); + } + function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override { - if (!isLocked()) { - uint64 start = depositWindowStart; - if (start != 0 && block.timestamp < start) revert DepositWindowNotStarted(); - uint64 end = depositWindowEnd; - if (end != 0 && block.timestamp > end) revert DepositWindowClosed(); - } else { - revert DepositWindowClosed(); + if (isLocked()) { + revert DepositsLocked(); } - super._beforeDeposit(token, amount); } @@ -213,9 +211,70 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy super._beforeWithdrawal(recipient, token, amountShares); } + function _configureOperatorIntegration( + VaultConfig memory config + ) internal { + bool hasDelegation = address(config.delegationManager) != address(0); + bool hasAllocation = address(config.allocationManager) != address(0); + bool hasAVS = config.operatorSetAVS != address(0); + bool hasOperatorSetId = config.operatorSetId != 0; + + if (!(hasDelegation && hasAllocation && hasAVS && hasOperatorSetId)) { + revert OperatorIntegrationInvalid(); + } + + delegationManager = config.delegationManager; + allocationManager = config.allocationManager; + _operatorSet = OperatorSet({avs: config.operatorSetAVS, id: config.operatorSetId}); + + delegationManager.registerAsOperator( + config.delegationApprover, config.operatorAllocationDelay, config.operatorMetadataURI + ); + + IAllocationManager.RegisterParams memory params; + params.avs = config.operatorSetAVS; + params.operatorSetIds = new uint32[](1); + params.operatorSetIds[0] = config.operatorSetId; + params.data = config.operatorSetRegistrationData; + allocationManager.registerForOperatorSets(address(this), params); + + operatorSetRegistered = true; + } + + function _allocateFullMagnitude() internal { + IAllocationManager.AllocateParams[] memory params = new IAllocationManager.AllocateParams[](1); + params[0].operatorSet = _operatorSet; + params[0].strategies = new IStrategy[](1); + params[0].strategies[0] = IStrategy(address(this)); + params[0].newMagnitudes = new uint64[](1); + params[0].newMagnitudes[0] = FULL_ALLOCATION; + allocationManager.modifyAllocations(address(this), params); + allocationsActive = true; + } + + function _deallocateAll() internal { + IAllocationManager.AllocateParams[] memory params = new IAllocationManager.AllocateParams[](1); + params[0].operatorSet = _operatorSet; + params[0].strategies = new IStrategy[](1); + params[0].strategies[0] = IStrategy(address(this)); + params[0].newMagnitudes = new uint64[](1); + params[0].newMagnitudes[0] = 0; + allocationManager.modifyAllocations(address(this), params); + } + + function _deregisterFromOperatorSet() internal { + IAllocationManager.DeregisterParams memory params; + params.operator = address(this); + params.avs = _operatorSet.avs; + params.operatorSetIds = new uint32[](1); + params.operatorSetIds[0] = _operatorSet.id; + allocationManager.deregisterFromOperatorSets(params); + operatorSetRegistered = false; + } + /** * @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[40] private __gap; + uint256[36] private __gap; } diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index abf7c83299..d080181cb6 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -136,17 +136,7 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable strategiesToWhitelist[0] = newVault; strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist); - emit DurationVaultDeployed( - newVault, - underlyingToken, - config.vaultAdmin, - config.depositWindowStart, - config.depositWindowEnd, - config.duration, - config.maxPerDeposit, - config.stakeCap, - config.metadataURI - ); + _emitDurationVaultDeployed(newVault, underlyingToken, config); } /** @@ -196,4 +186,29 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable function _registerDurationVault(IERC20 token, IDurationVaultStrategy vault) internal { durationVaultsByToken[token].push(vault); } + + function _emitDurationVaultDeployed( + IDurationVaultStrategy vault, + IERC20 underlyingToken, + IDurationVaultStrategy.VaultConfig calldata config + ) internal { + bool operatorIntegrationEnabled = + address(config.delegationManager) != address(0) && + address(config.allocationManager) != address(0) && + config.operatorSetAVS != address(0) && + config.operatorSetId != 0; + + emit DurationVaultDeployed( + vault, + underlyingToken, + config.vaultAdmin, + config.duration, + config.maxPerDeposit, + config.stakeCap, + config.metadataURI, + config.operatorSetAVS, + config.operatorSetId, + operatorIntegrationEnabled + ); + } } diff --git a/src/test/mocks/AllocationManagerMock.sol b/src/test/mocks/AllocationManagerMock.sol index 5425bb4ed0..42a34156e8 100644 --- a/src/test/mocks/AllocationManagerMock.sol +++ b/src/test/mocks/AllocationManagerMock.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.9; import "forge-std/Test.sol"; import "src/contracts/interfaces/IStrategy.sol"; +import "src/contracts/interfaces/IAllocationManager.sol"; import "src/contracts/libraries/Snapshots.sol"; import "src/contracts/libraries/OperatorSetLib.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -28,6 +29,35 @@ contract AllocationManagerMock is Test { _minimumSlashableStake; mapping(bytes32 operatorSetKey => mapping(address operator => bool)) internal _isOperatorSlashable; + struct RegisterCall { + address operator; + address avs; + uint32[] operatorSetIds; + bytes data; + } + + struct AllocateCall { + address operator; + address avs; + uint32 operatorSetId; + IStrategy strategy; + uint64 magnitude; + } + + struct DeregisterCall { + address operator; + address avs; + uint32[] operatorSetIds; + } + + RegisterCall internal _lastRegisterCall; + AllocateCall internal _lastAllocateCall; + DeregisterCall internal _lastDeregisterCall; + + uint256 public registerForOperatorSetsCallCount; + uint256 public modifyAllocationsCallCount; + uint256 public deregisterFromOperatorSetsCallCount; + function getSlashCount(OperatorSet memory operatorSet) external view returns (uint) { return _getSlashCount[operatorSet.key()]; } @@ -158,4 +188,55 @@ contract AllocationManagerMock is Test { function setIsOperatorSlashable(address operator, OperatorSet memory operatorSet, bool isSlashable) external { _isOperatorSlashable[operatorSet.key()][operator] = isSlashable; } + + function registerForOperatorSets(address operator, IAllocationManager.RegisterParams calldata params) external { + registerForOperatorSetsCallCount++; + _lastRegisterCall.operator = operator; + _lastRegisterCall.avs = params.avs; + delete _lastRegisterCall.operatorSetIds; + for (uint256 i = 0; i < params.operatorSetIds.length; ++i) { + _lastRegisterCall.operatorSetIds.push(params.operatorSetIds[i]); + } + _lastRegisterCall.data = params.data; + } + + function lastRegisterForOperatorSetsCall() external view returns (RegisterCall memory) { + return _lastRegisterCall; + } + + function modifyAllocations(address operator, IAllocationManager.AllocateParams[] calldata params) external { + modifyAllocationsCallCount++; + delete _lastAllocateCall; + _lastAllocateCall.operator = operator; + + if (params.length > 0) { + IAllocationManager.AllocateParams calldata first = params[0]; + _lastAllocateCall.avs = first.operatorSet.avs; + _lastAllocateCall.operatorSetId = first.operatorSet.id; + if (first.strategies.length > 0) { + _lastAllocateCall.strategy = first.strategies[0]; + } + if (first.newMagnitudes.length > 0) { + _lastAllocateCall.magnitude = first.newMagnitudes[0]; + } + } + } + + function lastModifyAllocationsCall() external view returns (AllocateCall memory) { + return _lastAllocateCall; + } + + function deregisterFromOperatorSets(IAllocationManager.DeregisterParams calldata params) external { + deregisterFromOperatorSetsCallCount++; + _lastDeregisterCall.operator = params.operator; + _lastDeregisterCall.avs = params.avs; + delete _lastDeregisterCall.operatorSetIds; + for (uint256 i = 0; i < params.operatorSetIds.length; ++i) { + _lastDeregisterCall.operatorSetIds.push(params.operatorSetIds[i]); + } + } + + function lastDeregisterFromOperatorSetsCall() external view returns (DeregisterCall memory) { + return _lastDeregisterCall; + } } diff --git a/src/test/mocks/DelegationManagerMock.sol b/src/test/mocks/DelegationManagerMock.sol index b07775d894..a65ffc47d6 100644 --- a/src/test/mocks/DelegationManagerMock.sol +++ b/src/test/mocks/DelegationManagerMock.sol @@ -15,6 +15,16 @@ contract DelegationManagerMock is Test { mapping(address => address) public delegatedTo; mapping(address => mapping(IStrategy => uint)) public operatorShares; + struct RegisterAsOperatorCall { + address operator; + address delegationApprover; + uint32 allocationDelay; + string metadataURI; + } + + RegisterAsOperatorCall internal _lastRegisterAsOperatorCall; + uint256 public registerAsOperatorCallCount; + function getDelegatableShares(address staker) external view returns (IStrategy[] memory, uint[] memory) {} function setMinWithdrawalDelayBlocks(uint newMinWithdrawalDelayBlocks) external {} @@ -67,6 +77,21 @@ contract DelegationManagerMock is Test { delegatedTo[msg.sender] = operator; } + function registerAsOperator(address delegationApprover, uint32 allocationDelay, string calldata metadataURI) external { + registerAsOperatorCallCount++; + isOperator[msg.sender] = true; + _lastRegisterAsOperatorCall = RegisterAsOperatorCall({ + operator: msg.sender, + delegationApprover: delegationApprover, + allocationDelay: allocationDelay, + metadataURI: metadataURI + }); + } + + function lastRegisterAsOperatorCall() external view returns (RegisterAsOperatorCall memory) { + return _lastRegisterAsOperatorCall; + } + function undelegate(address staker) external returns (bytes32[] memory withdrawalRoot) { delegatedTo[staker] = address(0); return withdrawalRoot; diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol index 5b121f00cb..ee13067900 100644 --- a/src/test/unit/DurationVaultStrategyUnit.t.sol +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -4,28 +4,49 @@ pragma solidity ^0.8.27; import "./StrategyBaseTVLLimitsUnit.sol"; import "../../contracts/strategies/DurationVaultStrategy.sol"; import "../../contracts/interfaces/IDurationVaultStrategy.sol"; +import "../../contracts/interfaces/IDelegationManager.sol"; +import "../../contracts/interfaces/IAllocationManager.sol"; +import "../mocks/DelegationManagerMock.sol"; +import "../mocks/AllocationManagerMock.sol"; contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { DurationVaultStrategy public durationVaultImplementation; DurationVaultStrategy public durationVault; + DelegationManagerMock internal delegationManagerMock; + AllocationManagerMock internal allocationManagerMock; uint64 internal defaultDuration = 30 days; - uint64 internal defaultDepositWindowLength = 7 days; + address internal constant OPERATOR_SET_AVS = address(0xA11CE); + uint32 internal constant OPERATOR_SET_ID = 42; + address internal constant DELEGATION_APPROVER = address(0xB0B); + uint32 internal constant OPERATOR_ALLOCATION_DELAY = 5; + string internal constant OPERATOR_METADATA_URI = "ipfs://operator-metadata"; + bytes internal constant REGISTRATION_DATA = hex"1234"; + uint64 internal constant FULL_ALLOCATION = 1e18; function setUp() public virtual override { StrategyBaseUnitTests.setUp(); + delegationManagerMock = new DelegationManagerMock(); + allocationManagerMock = new AllocationManagerMock(); + durationVaultImplementation = new DurationVaultStrategy(strategyManager, pauserRegistry, "9.9.9"); IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - depositWindowStart: 0, - depositWindowEnd: uint64(block.timestamp + defaultDepositWindowLength), duration: defaultDuration, maxPerDeposit: maxPerDeposit, stakeCap: maxTotalDeposits, - metadataURI: "ipfs://duration-vault" + metadataURI: "ipfs://duration-vault", + delegationManager: IDelegationManager(address(delegationManagerMock)), + allocationManager: IAllocationManager(address(allocationManagerMock)), + operatorSetAVS: OPERATOR_SET_AVS, + operatorSetId: OPERATOR_SET_ID, + operatorSetRegistrationData: REGISTRATION_DATA, + delegationApprover: DELEGATION_APPROVER, + operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, + operatorMetadataURI: OPERATOR_METADATA_URI }); durationVault = DurationVaultStrategy( @@ -42,37 +63,72 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { strategyWithTVLLimits = StrategyBaseTVLLimits(address(durationVault)); } - function testDepositWindowNotStarted() public { - // reconfigure deposit window to start in future - durationVault.updateDepositWindow(uint64(block.timestamp + 1 hours), uint64(block.timestamp + 2 hours)); + function testInitializeConfiguresOperatorIntegration() public { + DelegationManagerMock.RegisterAsOperatorCall memory delegationCall = delegationManagerMock.lastRegisterAsOperatorCall(); + assertEq(delegationCall.operator, address(durationVault), "delegation operator mismatch"); + assertEq(delegationCall.delegationApprover, DELEGATION_APPROVER, "delegation approver mismatch"); + assertEq(delegationCall.allocationDelay, OPERATOR_ALLOCATION_DELAY, "allocation delay mismatch"); + assertEq(delegationCall.metadataURI, OPERATOR_METADATA_URI, "metadata mismatch"); + + AllocationManagerMock.RegisterCall memory registerCall = allocationManagerMock.lastRegisterForOperatorSetsCall(); + assertEq(registerCall.operator, address(durationVault), "register operator mismatch"); + assertEq(registerCall.avs, OPERATOR_SET_AVS, "register AVS mismatch"); + assertEq(registerCall.operatorSetIds.length, 1, "unexpected operatorSetIds length"); + assertEq(registerCall.operatorSetIds[0], OPERATOR_SET_ID, "operatorSetId mismatch"); + assertEq(registerCall.data, REGISTRATION_DATA, "registration data mismatch"); + + (address avs, uint32 operatorSetId) = durationVault.operatorSetInfo(); + assertEq(avs, OPERATOR_SET_AVS, "stored AVS mismatch"); + assertEq(operatorSetId, OPERATOR_SET_ID, "stored operatorSetId mismatch"); + assertEq(address(durationVault.delegationManager()), address(delegationManagerMock), "delegation manager mismatch"); + assertEq(address(durationVault.allocationManager()), address(allocationManagerMock), "allocation manager mismatch"); + assertTrue(durationVault.operatorSetRegistered(), "operator set should be registered"); + } - uint depositAmount = 1e18; - underlyingToken.transfer(address(durationVault), depositAmount); + function testLockAllocatesFullMagnitude() public { + assertEq(allocationManagerMock.modifyAllocationsCallCount(), 0, "precondition failed"); - cheats.prank(address(strategyManager)); - cheats.expectRevert(IDurationVaultStrategy.DepositWindowNotStarted.selector); - durationVault.deposit(underlyingToken, depositAmount); + durationVault.lock(); + + assertTrue(durationVault.allocationsActive(), "allocations should be active after lock"); + assertEq(allocationManagerMock.modifyAllocationsCallCount(), 1, "modifyAllocations not called"); + + AllocationManagerMock.AllocateCall memory allocateCall = allocationManagerMock.lastModifyAllocationsCall(); + assertEq(allocateCall.operator, address(durationVault), "allocate operator mismatch"); + assertEq(allocateCall.avs, OPERATOR_SET_AVS, "allocate AVS mismatch"); + assertEq(allocateCall.operatorSetId, OPERATOR_SET_ID, "allocate operatorSetId mismatch"); + assertEq(address(allocateCall.strategy), address(durationVault), "allocate strategy mismatch"); + assertEq(allocateCall.magnitude, FULL_ALLOCATION, "allocate magnitude mismatch"); } - function testDepositWindowClosedAfterEnd() public { - cheats.warp(block.timestamp + defaultDepositWindowLength + 1); + function testMarkMaturedDeallocatesAndDeregisters() public { + durationVault.lock(); + cheats.warp(block.timestamp + defaultDuration + 1); - uint depositAmount = 1e18; - underlyingToken.transfer(address(durationVault), depositAmount); + durationVault.markMatured(); - cheats.prank(address(strategyManager)); - cheats.expectRevert(IDurationVaultStrategy.DepositWindowClosed.selector); - durationVault.deposit(underlyingToken, depositAmount); + assertEq(allocationManagerMock.modifyAllocationsCallCount(), 2, "deallocation not invoked"); + AllocationManagerMock.AllocateCall memory allocateCall = allocationManagerMock.lastModifyAllocationsCall(); + assertEq(allocateCall.magnitude, 0, "expected zero magnitude"); + + AllocationManagerMock.DeregisterCall memory deregisterCall = allocationManagerMock.lastDeregisterFromOperatorSetsCall(); + assertEq(deregisterCall.operator, address(durationVault), "deregister operator mismatch"); + assertEq(deregisterCall.avs, OPERATOR_SET_AVS, "deregister AVS mismatch"); + assertEq(deregisterCall.operatorSetIds.length, 1, "unexpected deregister length"); + assertEq(deregisterCall.operatorSetIds[0], OPERATOR_SET_ID, "deregister operatorSetId mismatch"); + + assertFalse(durationVault.allocationsActive(), "allocations should be inactive"); + assertFalse(durationVault.operatorSetRegistered(), "operator set should be deregistered"); } - function testDepositsBlockedAfterManualLock() public { + function testDepositsBlockedAfterLock() public { durationVault.lock(); uint depositAmount = 1e18; underlyingToken.transfer(address(durationVault), depositAmount); cheats.prank(address(strategyManager)); - cheats.expectRevert(IDurationVaultStrategy.DepositWindowClosed.selector); + cheats.expectRevert(IDurationVaultStrategy.DepositsLocked.selector); durationVault.deposit(underlyingToken, depositAmount); } diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index 7007257ab5..ab85f4df78 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -7,6 +7,8 @@ import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "src/contracts/strategies/StrategyFactory.sol"; import "src/contracts/strategies/DurationVaultStrategy.sol"; import "../../contracts/interfaces/IDurationVaultStrategy.sol"; +import "../../contracts/interfaces/IDelegationManager.sol"; +import "../../contracts/interfaces/IAllocationManager.sol"; import "src/test/utils/EigenLayerUnitTestSetup.sol"; import "../../contracts/permissions/PauserRegistry.sol"; @@ -32,6 +34,12 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { address notOwner = address(7_777_777); uint initialPausedStatus = 0; + address internal constant OPERATOR_SET_AVS = address(0xABCD); + uint32 internal constant OPERATOR_SET_ID = 9; + address internal constant DELEGATION_APPROVER = address(0x5151); + uint32 internal constant OPERATOR_ALLOCATION_DELAY = 4; + string internal constant OPERATOR_METADATA_URI = "ipfs://factory-duration-vault"; + bytes internal constant REGISTRATION_DATA = hex"F00D"; /// @notice Emitted when the `strategyBeacon` is changed event StrategyBeaconModified(IBeacon previousBeacon, IBeacon newBeacon); @@ -128,12 +136,18 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - depositWindowStart: 0, - depositWindowEnd: 0, duration: 30 days, maxPerDeposit: 10 ether, stakeCap: 100 ether, - metadataURI: "ipfs://duration" + metadataURI: "ipfs://duration", + delegationManager: IDelegationManager(address(delegationManagerMock)), + allocationManager: IAllocationManager(address(allocationManagerMock)), + operatorSetAVS: OPERATOR_SET_AVS, + operatorSetId: OPERATOR_SET_ID, + operatorSetRegistrationData: REGISTRATION_DATA, + delegationApprover: DELEGATION_APPROVER, + operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, + operatorMetadataURI: OPERATOR_METADATA_URI }); DurationVaultStrategy vault = DurationVaultStrategy(address(strategyFactory.deployDurationVaultStrategy(config))); @@ -150,12 +164,18 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - depositWindowStart: 0, - depositWindowEnd: 0, duration: 30 days, maxPerDeposit: 10 ether, stakeCap: 100 ether, - metadataURI: "ipfs://duration" + metadataURI: "ipfs://duration", + delegationManager: IDelegationManager(address(delegationManagerMock)), + allocationManager: IAllocationManager(address(allocationManagerMock)), + operatorSetAVS: OPERATOR_SET_AVS, + operatorSetId: OPERATOR_SET_ID, + operatorSetRegistrationData: REGISTRATION_DATA, + delegationApprover: DELEGATION_APPROVER, + operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, + operatorMetadataURI: OPERATOR_METADATA_URI }); cheats.expectRevert(IStrategyFactory.DurationVaultBeaconNotSet.selector); @@ -169,12 +189,18 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - depositWindowStart: 0, - depositWindowEnd: 0, duration: 7 days, maxPerDeposit: 5 ether, stakeCap: 50 ether, - metadataURI: "ipfs://duration" + metadataURI: "ipfs://duration", + delegationManager: IDelegationManager(address(delegationManagerMock)), + allocationManager: IAllocationManager(address(allocationManagerMock)), + operatorSetAVS: OPERATOR_SET_AVS, + operatorSetId: OPERATOR_SET_ID, + operatorSetRegistrationData: REGISTRATION_DATA, + delegationApprover: DELEGATION_APPROVER, + operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, + operatorMetadataURI: OPERATOR_METADATA_URI }); strategyFactory.deployDurationVaultStrategy(config); diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index 02fc5edf4e..b01f694bc9 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -9,6 +9,8 @@ import "src/contracts/core/StrategyManager.sol"; import "src/contracts/strategies/DurationVaultStrategy.sol"; import "src/contracts/interfaces/IDurationVaultStrategy.sol"; import "src/contracts/interfaces/IStrategy.sol"; +import "src/contracts/interfaces/IDelegationManager.sol"; +import "src/contracts/interfaces/IAllocationManager.sol"; import "src/test/utils/EigenLayerUnitTestSetup.sol"; contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyManagerEvents { @@ -22,6 +24,12 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM address internal constant STAKER = address(0xBEEF); uint internal constant INITIAL_SUPPLY = 1e36; + address internal constant OPERATOR_SET_AVS = address(0xF00D); + uint32 internal constant OPERATOR_SET_ID = 7; + address internal constant DELEGATION_APPROVER = address(0xCAFE); + uint32 internal constant OPERATOR_ALLOCATION_DELAY = 3; + string internal constant OPERATOR_METADATA_URI = "ipfs://strategy-manager-vault"; + bytes internal constant REGISTRATION_DATA = hex"DEADBEEF"; function setUp() public override { EigenLayerUnitTestSetup.setUp(); @@ -47,12 +55,18 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM IDurationVaultStrategy.VaultConfig memory cfg = IDurationVaultStrategy.VaultConfig({ underlyingToken: IERC20(address(underlyingToken)), vaultAdmin: address(this), - depositWindowStart: 0, - depositWindowEnd: 0, duration: 30 days, maxPerDeposit: 1_000_000 ether, stakeCap: 10_000_000 ether, - metadataURI: "ipfs://duration-vault-test" + metadataURI: "ipfs://duration-vault-test", + delegationManager: IDelegationManager(address(delegationManagerMock)), + allocationManager: IAllocationManager(address(allocationManagerMock)), + operatorSetAVS: OPERATOR_SET_AVS, + operatorSetId: OPERATOR_SET_ID, + operatorSetRegistrationData: REGISTRATION_DATA, + delegationApprover: DELEGATION_APPROVER, + operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, + operatorMetadataURI: OPERATOR_METADATA_URI }); durationVault = IDurationVaultStrategy( @@ -95,7 +109,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM cheats.startPrank(STAKER); underlyingToken.approve(address(strategyManager), amount); - cheats.expectRevert(IDurationVaultStrategy.DepositWindowClosed.selector); + cheats.expectRevert(IDurationVaultStrategy.DepositsLocked.selector); strategyManager.depositIntoStrategy(IStrategy(address(durationVault)), IERC20(address(underlyingToken)), amount); cheats.stopPrank(); } From daf709d61dfd5ef46d52068df494c54cf5746bbd Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 1 Dec 2025 20:33:30 -0500 Subject: [PATCH 06/18] chore: fmt --- .../strategies/DurationVaultStrategy.sol | 8 ++------ src/contracts/strategies/StrategyFactory.sol | 8 +++----- src/test/mocks/AllocationManagerMock.sol | 18 +++++++----------- src/test/mocks/DelegationManagerMock.sol | 2 +- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index 72732254c7..dd64d6472e 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -15,6 +15,7 @@ import "../libraries/OperatorSetLib.sol"; contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy { using OperatorSetLib for OperatorSet; /// @notice Address empowered to configure and lock the vault. + address public vaultAdmin; /// @notice The enforced lock duration once `lock` is called. @@ -81,12 +82,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy _configureOperatorIntegration(config); emit VaultInitialized( - vaultAdmin, - config.underlyingToken, - duration, - config.maxPerDeposit, - config.stakeCap, - metadataURI + vaultAdmin, config.underlyingToken, duration, config.maxPerDeposit, config.stakeCap, metadataURI ); } diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index d080181cb6..2fd1b1773e 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -192,11 +192,9 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable IERC20 underlyingToken, IDurationVaultStrategy.VaultConfig calldata config ) internal { - bool operatorIntegrationEnabled = - address(config.delegationManager) != address(0) && - address(config.allocationManager) != address(0) && - config.operatorSetAVS != address(0) && - config.operatorSetId != 0; + bool operatorIntegrationEnabled = address(config.delegationManager) != address(0) + && address(config.allocationManager) != address(0) && config.operatorSetAVS != address(0) + && config.operatorSetId != 0; emit DurationVaultDeployed( vault, diff --git a/src/test/mocks/AllocationManagerMock.sol b/src/test/mocks/AllocationManagerMock.sol index 42a34156e8..f48a17de83 100644 --- a/src/test/mocks/AllocationManagerMock.sol +++ b/src/test/mocks/AllocationManagerMock.sol @@ -54,9 +54,9 @@ contract AllocationManagerMock is Test { AllocateCall internal _lastAllocateCall; DeregisterCall internal _lastDeregisterCall; - uint256 public registerForOperatorSetsCallCount; - uint256 public modifyAllocationsCallCount; - uint256 public deregisterFromOperatorSetsCallCount; + uint public registerForOperatorSetsCallCount; + uint public modifyAllocationsCallCount; + uint public deregisterFromOperatorSetsCallCount; function getSlashCount(OperatorSet memory operatorSet) external view returns (uint) { return _getSlashCount[operatorSet.key()]; @@ -194,7 +194,7 @@ contract AllocationManagerMock is Test { _lastRegisterCall.operator = operator; _lastRegisterCall.avs = params.avs; delete _lastRegisterCall.operatorSetIds; - for (uint256 i = 0; i < params.operatorSetIds.length; ++i) { + for (uint i = 0; i < params.operatorSetIds.length; ++i) { _lastRegisterCall.operatorSetIds.push(params.operatorSetIds[i]); } _lastRegisterCall.data = params.data; @@ -213,12 +213,8 @@ contract AllocationManagerMock is Test { IAllocationManager.AllocateParams calldata first = params[0]; _lastAllocateCall.avs = first.operatorSet.avs; _lastAllocateCall.operatorSetId = first.operatorSet.id; - if (first.strategies.length > 0) { - _lastAllocateCall.strategy = first.strategies[0]; - } - if (first.newMagnitudes.length > 0) { - _lastAllocateCall.magnitude = first.newMagnitudes[0]; - } + if (first.strategies.length > 0) _lastAllocateCall.strategy = first.strategies[0]; + if (first.newMagnitudes.length > 0) _lastAllocateCall.magnitude = first.newMagnitudes[0]; } } @@ -231,7 +227,7 @@ contract AllocationManagerMock is Test { _lastDeregisterCall.operator = params.operator; _lastDeregisterCall.avs = params.avs; delete _lastDeregisterCall.operatorSetIds; - for (uint256 i = 0; i < params.operatorSetIds.length; ++i) { + for (uint i = 0; i < params.operatorSetIds.length; ++i) { _lastDeregisterCall.operatorSetIds.push(params.operatorSetIds[i]); } } diff --git a/src/test/mocks/DelegationManagerMock.sol b/src/test/mocks/DelegationManagerMock.sol index a65ffc47d6..7cb8afb21c 100644 --- a/src/test/mocks/DelegationManagerMock.sol +++ b/src/test/mocks/DelegationManagerMock.sol @@ -23,7 +23,7 @@ contract DelegationManagerMock is Test { } RegisterAsOperatorCall internal _lastRegisterAsOperatorCall; - uint256 public registerAsOperatorCallCount; + uint public registerAsOperatorCallCount; function getDelegatableShares(address staker) external view returns (IStrategy[] memory, uint[] memory) {} From 3c5d1724df1edc9d80b00254b81c02e5eb04e457 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Dec 2025 17:49:16 -0500 Subject: [PATCH 07/18] refactor: storage contract --- .../interfaces/IDurationVaultStrategy.sol | 25 ++-- src/contracts/interfaces/IStrategyFactory.sol | 9 +- .../strategies/DurationVaultStrategy.sol | 133 ++++++++---------- .../DurationVaultStrategyStorage.sol | 50 +++++++ src/contracts/strategies/StrategyFactory.sol | 7 +- src/test/unit/DurationVaultStrategyUnit.t.sol | 14 +- src/test/unit/StrategyFactoryUnit.t.sol | 20 +-- .../unit/StrategyManagerDurationUnit.t.sol | 13 +- 8 files changed, 155 insertions(+), 116 deletions(-) create mode 100644 src/contracts/strategies/DurationVaultStrategyStorage.sol diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index 3796d6f569..c3534dd947 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -13,15 +13,19 @@ import "./IAllocationManager.sol"; * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service */ interface IDurationVaultStrategy is IStrategy { + enum VaultState { + Deposits, + Allocations, + Withdrawals + } + struct VaultConfig { IERC20 underlyingToken; address vaultAdmin; - uint64 duration; + uint32 duration; uint256 maxPerDeposit; uint256 stakeCap; string metadataURI; - IDelegationManager delegationManager; - IAllocationManager allocationManager; address operatorSetAVS; uint32 operatorSetId; bytes operatorSetRegistrationData; @@ -46,19 +50,21 @@ interface IDurationVaultStrategy is IStrategy { error WithdrawalsLocked(); /// @dev Thrown when attempting to mark the vault as matured before duration elapses. error DurationNotElapsed(); + /// @dev Thrown when attempting to convert a timestamp that exceeds uint32 bounds. + error TimestampOverflow(); event VaultInitialized( address indexed vaultAdmin, IERC20 indexed underlyingToken, - uint64 duration, + uint32 duration, uint256 maxPerDeposit, uint256 stakeCap, string metadataURI ); - event VaultLocked(uint64 lockedAt, uint64 unlockAt); + event VaultLocked(uint32 lockedAt, uint32 unlockAt); - event VaultMatured(uint64 maturedAt); + event VaultMatured(uint32 maturedAt); event VaultAdminUpdated(address indexed previousAdmin, address indexed newAdmin); @@ -90,11 +96,12 @@ interface IDurationVaultStrategy is IStrategy { ) external; function vaultAdmin() external view returns (address); - function duration() external view returns (uint64); - function lockedAt() external view returns (uint64); - function unlockTimestamp() external view returns (uint64); + function duration() external view returns (uint32); + function lockedAt() external view returns (uint32); + function unlockTimestamp() external view returns (uint32); function isLocked() external view returns (bool); function isMatured() external view returns (bool); + function state() external view returns (VaultState); function metadataURI() external view returns (string memory); function stakeCap() external view returns (uint256); function depositsOpen() external view returns (bool); diff --git a/src/contracts/interfaces/IStrategyFactory.sol b/src/contracts/interfaces/IStrategyFactory.sol index b709877e58..219aaf74b3 100644 --- a/src/contracts/interfaces/IStrategyFactory.sol +++ b/src/contracts/interfaces/IStrategyFactory.sol @@ -98,17 +98,16 @@ interface IStrategyFactory is ISemVerMixin { /// @notice Emitted whenever a slot is set in the `tokenStrategy` mapping event StrategySetForToken(IERC20 token, IStrategy strategy); - /// @notice Emitted whenever a duration vault is deployed. + /// @notice Emitted whenever a duration vault is deployed. The vault address uniquely identifies the deployment. event DurationVaultDeployed( - IDurationVaultStrategy vault, + IDurationVaultStrategy indexed vault, IERC20 indexed underlyingToken, address indexed vaultAdmin, - uint64 duration, + uint32 duration, uint256 maxPerDeposit, uint256 stakeCap, string metadataURI, address operatorSetAVS, - uint32 operatorSetId, - bool operatorIntegrationEnabled + uint32 operatorSetId ); } diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index dd64d6472e..b7de69729f 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; import "./StrategyBaseTVLLimits.sol"; -import "../interfaces/IDurationVaultStrategy.sol"; +import "./DurationVaultStrategyStorage.sol"; import "../interfaces/IDelegationManager.sol"; import "../interfaces/IAllocationManager.sol"; import "../libraries/OperatorSetLib.sol"; @@ -12,44 +12,20 @@ import "../libraries/OperatorSetLib.sol"; * @author Layr Labs, Inc. * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service */ -contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy { +contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLLimits { using OperatorSetLib for OperatorSet; - /// @notice Address empowered to configure and lock the vault. - address public vaultAdmin; - - /// @notice The enforced lock duration once `lock` is called. - uint64 public duration; - - /// @notice Timestamp when the vault was locked. Zero indicates the vault is not yet locked. - uint64 public lockedAt; - - /// @notice Timestamp when the vault unlocks (set at lock time). - uint64 public unlockAt; - - /// @notice Timestamp when the vault was marked as matured (purely informational). - uint64 public maturedAt; + /// @notice Constant representing the full allocation magnitude (1 WAD) for allocation manager calls. + uint64 internal constant FULL_ALLOCATION = 1e18; - /// @notice Optional metadata URI describing the vault configuration. - string public metadataURI; + /// @notice Maximum allowable duration (approximately 2 years). + uint32 internal constant MAX_DURATION = uint32(2 * 365 days); /// @notice Delegation manager reference used to register the vault as an operator. - IDelegationManager public delegationManager; + IDelegationManager public immutable override delegationManager; /// @notice Allocation manager reference used to register/allocate operator sets. - IAllocationManager public allocationManager; - - /// @notice Stored operator set metadata for integration with the allocation manager. - OperatorSet internal _operatorSet; - - /// @notice True when allocations are currently active (i.e. slashable) for the configured operator set. - bool public allocationsActive; - - /// @notice True when the vault remains registered for the operator set. - bool public operatorSetRegistered; - - /// @notice Constant representing the full allocation magnitude (1 WAD) for allocation manager calls. - uint64 internal constant FULL_ALLOCATION = 1e18; + IAllocationManager public immutable override allocationManager; error OperatorIntegrationInvalid(); @@ -61,8 +37,16 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy constructor( IStrategyManager _strategyManager, IPauserRegistry _pauserRegistry, - string memory _version - ) StrategyBaseTVLLimits(_strategyManager, _pauserRegistry, _version) {} + string memory _version, + IDelegationManager _delegationManager, + IAllocationManager _allocationManager + ) StrategyBaseTVLLimits(_strategyManager, _pauserRegistry, _version) { + if (address(_delegationManager) == address(0) || address(_allocationManager) == address(0)) { + revert OperatorIntegrationInvalid(); + } + delegationManager = _delegationManager; + allocationManager = _allocationManager; + } /** * @notice Initializes the vault configuration. @@ -71,7 +55,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy VaultConfig memory config ) public initializer { if (config.vaultAdmin == address(0)) revert InvalidVaultAdmin(); - if (config.duration == 0) revert InvalidDuration(); + if (config.duration == 0 || config.duration > MAX_DURATION) revert InvalidDuration(); _setTVLLimits(config.maxPerDeposit, config.stakeCap); _initializeStrategyBase(config.underlyingToken); @@ -80,6 +64,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy metadataURI = config.metadataURI; _configureOperatorIntegration(config); + _state = VaultState.Deposits; emit VaultInitialized( vaultAdmin, config.underlyingToken, duration, config.maxPerDeposit, config.stakeCap, metadataURI @@ -90,39 +75,37 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy * @notice Locks the vault, preventing new deposits and withdrawals until maturity. */ function lock() external override onlyVaultAdmin { - if (lockedAt != 0) revert VaultAlreadyLocked(); + if (_state != VaultState.Deposits) revert VaultAlreadyLocked(); - lockedAt = uint64(block.timestamp); - uint256 rawUnlockTimestamp = uint256(lockedAt) + uint256(duration); - require(rawUnlockTimestamp <= type(uint64).max, InvalidDuration()); - unlockAt = uint64(rawUnlockTimestamp); + uint32 currentTimestamp = _currentTimestamp(); + lockedAt = currentTimestamp; + uint32 newUnlockAt = currentTimestamp + duration; + if (newUnlockAt < currentTimestamp) revert InvalidDuration(); + unlockAt = newUnlockAt; + + _state = VaultState.Allocations; emit VaultLocked(lockedAt, unlockAt); - if (!allocationsActive) { - _allocateFullMagnitude(); - } + _allocateFullMagnitude(); } /** * @notice Marks the vault as matured once the configured duration elapses. Callable by anyone. */ function markMatured() external override { - if (!isMatured()) revert DurationNotElapsed(); - if (maturedAt != 0) { + if (_state == VaultState.Withdrawals) { // already recorded; noop return; } - maturedAt = uint64(block.timestamp); + if (_state != VaultState.Allocations || block.timestamp < unlockAt) revert DurationNotElapsed(); + + _state = VaultState.Withdrawals; + maturedAt = _currentTimestamp(); emit VaultMatured(maturedAt); - if (allocationsActive) { - _deallocateAll(); - allocationsActive = false; - } - if (operatorSetRegistered) { - _deregisterFromOperatorSet(); - } + _deallocateAll(); + _deregisterFromOperatorSet(); } /** @@ -147,22 +130,23 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy } /// @inheritdoc IDurationVaultStrategy - function unlockTimestamp() public view override returns (uint64) { + function unlockTimestamp() public view override returns (uint32) { return unlockAt; } /// @inheritdoc IDurationVaultStrategy function isLocked() public view override returns (bool) { - return lockedAt != 0; + return _state != VaultState.Deposits; } /// @inheritdoc IDurationVaultStrategy function isMatured() public view override returns (bool) { - uint64 lockTimestamp = lockedAt; - if (lockTimestamp == 0) { - return false; - } - return block.timestamp >= unlockAt && unlockAt != 0; + return _state == VaultState.Withdrawals; + } + + /// @inheritdoc IDurationVaultStrategy + function state() public view override returns (VaultState) { + return _state; } /// @inheritdoc IDurationVaultStrategy @@ -172,15 +156,12 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy /// @inheritdoc IDurationVaultStrategy function depositsOpen() public view override returns (bool) { - return !isLocked(); + return _state == VaultState.Deposits; } /// @inheritdoc IDurationVaultStrategy function withdrawalsOpen() public view override returns (bool) { - if (!isLocked()) { - return true; - } - return isMatured(); + return _state != VaultState.Allocations; } /// @inheritdoc IDurationVaultStrategy @@ -194,14 +175,14 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy } function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override { - if (isLocked()) { + if (!depositsOpen()) { revert DepositsLocked(); } super._beforeDeposit(token, amount); } function _beforeWithdrawal(address recipient, IERC20 token, uint256 amountShares) internal virtual override { - if (isLocked() && !isMatured()) { + if (!withdrawalsOpen()) { revert WithdrawalsLocked(); } super._beforeWithdrawal(recipient, token, amountShares); @@ -210,17 +191,9 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy function _configureOperatorIntegration( VaultConfig memory config ) internal { - bool hasDelegation = address(config.delegationManager) != address(0); - bool hasAllocation = address(config.allocationManager) != address(0); - bool hasAVS = config.operatorSetAVS != address(0); - bool hasOperatorSetId = config.operatorSetId != 0; - - if (!(hasDelegation && hasAllocation && hasAVS && hasOperatorSetId)) { + if (config.operatorSetAVS == address(0) || config.operatorSetId == 0) { revert OperatorIntegrationInvalid(); } - - delegationManager = config.delegationManager; - allocationManager = config.allocationManager; _operatorSet = OperatorSet({avs: config.operatorSetAVS, id: config.operatorSetId}); delegationManager.registerAsOperator( @@ -233,7 +206,6 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy params.operatorSetIds[0] = config.operatorSetId; params.data = config.operatorSetRegistrationData; allocationManager.registerForOperatorSets(address(this), params); - operatorSetRegistered = true; } @@ -256,6 +228,7 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy params[0].newMagnitudes = new uint64[](1); params[0].newMagnitudes[0] = 0; allocationManager.modifyAllocations(address(this), params); + allocationsActive = false; } function _deregisterFromOperatorSet() internal { @@ -272,5 +245,9 @@ contract DurationVaultStrategy is StrategyBaseTVLLimits, IDurationVaultStrategy * @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[36] private __gap; + function _currentTimestamp() internal view returns (uint32) { + uint256 ts = block.timestamp; + if (ts > type(uint32).max) revert TimestampOverflow(); + return uint32(ts); + } } diff --git a/src/contracts/strategies/DurationVaultStrategyStorage.sol b/src/contracts/strategies/DurationVaultStrategyStorage.sol new file mode 100644 index 0000000000..d946e48c1e --- /dev/null +++ b/src/contracts/strategies/DurationVaultStrategyStorage.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "../interfaces/IDurationVaultStrategy.sol"; +import "../libraries/OperatorSetLib.sol"; + +/** + * @title Storage layout for DurationVaultStrategy. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +abstract contract DurationVaultStrategyStorage is IDurationVaultStrategy { + /// @notice Address empowered to configure and lock the vault. + address public vaultAdmin; + + /// @notice The enforced lock duration once `lock` is called. + uint32 public duration; + + /// @notice Timestamp when the vault was locked. Zero indicates the vault is not yet locked. + uint32 public lockedAt; + + /// @notice Timestamp when the vault unlocks (set at lock time). + uint32 public unlockAt; + + /// @notice Timestamp when the vault was marked as matured (purely informational). + uint32 public maturedAt; + + /// @notice Optional metadata URI describing the vault configuration. + string public metadataURI; + + /// @notice Stored operator set metadata for integration with the allocation manager. + OperatorSet internal _operatorSet; + + /// @notice Tracks the lifecycle of the vault (deposits -> allocations -> withdrawals). + VaultState internal _state; + + /// @notice True when allocations are currently active (i.e. slashable) for the configured operator set. + bool public allocationsActive; + + /// @notice True when the vault remains registered for the operator set. + bool public operatorSetRegistered; + + /** + * @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[41] private __gap; +} + + diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index 2fd1b1773e..bab7ba1c3f 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -192,10 +192,6 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable IERC20 underlyingToken, IDurationVaultStrategy.VaultConfig calldata config ) internal { - bool operatorIntegrationEnabled = address(config.delegationManager) != address(0) - && address(config.allocationManager) != address(0) && config.operatorSetAVS != address(0) - && config.operatorSetId != 0; - emit DurationVaultDeployed( vault, underlyingToken, @@ -205,8 +201,7 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable config.stakeCap, config.metadataURI, config.operatorSetAVS, - config.operatorSetId, - operatorIntegrationEnabled + config.operatorSetId ); } } diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol index ee13067900..ac311130d4 100644 --- a/src/test/unit/DurationVaultStrategyUnit.t.sol +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -15,7 +15,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { DelegationManagerMock internal delegationManagerMock; AllocationManagerMock internal allocationManagerMock; - uint64 internal defaultDuration = 30 days; + uint32 internal defaultDuration = uint32(30 days); address internal constant OPERATOR_SET_AVS = address(0xA11CE); uint32 internal constant OPERATOR_SET_ID = 42; address internal constant DELEGATION_APPROVER = address(0xB0B); @@ -30,7 +30,13 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { delegationManagerMock = new DelegationManagerMock(); allocationManagerMock = new AllocationManagerMock(); - durationVaultImplementation = new DurationVaultStrategy(strategyManager, pauserRegistry, "9.9.9"); + durationVaultImplementation = new DurationVaultStrategy( + strategyManager, + pauserRegistry, + "9.9.9", + IDelegationManager(address(delegationManagerMock)), + IAllocationManager(address(allocationManagerMock)) + ); IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, @@ -39,8 +45,6 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { maxPerDeposit: maxPerDeposit, stakeCap: maxTotalDeposits, metadataURI: "ipfs://duration-vault", - delegationManager: IDelegationManager(address(delegationManagerMock)), - allocationManager: IAllocationManager(address(allocationManagerMock)), operatorSetAVS: OPERATOR_SET_AVS, operatorSetId: OPERATOR_SET_ID, operatorSetRegistrationData: REGISTRATION_DATA, @@ -153,6 +157,8 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { cheats.stopPrank(); cheats.warp(block.timestamp + defaultDuration + 1); + durationVault.markMatured(); + assertTrue(durationVault.withdrawalsOpen(), "withdrawals should open after maturity"); cheats.startPrank(address(strategyManager)); durationVault.withdraw(address(this), underlyingToken, durationVault.totalShares()); diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index ab85f4df78..31dcf5df42 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -63,7 +63,13 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { strategyBeacon = new UpgradeableBeacon(address(strategyImplementation)); strategyBeacon.transferOwnership(beaconProxyOwner); - durationVaultImplementation = new DurationVaultStrategy(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "9.9.9"); + durationVaultImplementation = new DurationVaultStrategy( + IStrategyManager(address(strategyManagerMock)), + pauserRegistry, + "9.9.9", + IDelegationManager(address(delegationManagerMock)), + IAllocationManager(address(allocationManagerMock)) + ); durationVaultBeacon = new UpgradeableBeacon(address(durationVaultImplementation)); durationVaultBeacon.transferOwnership(beaconProxyOwner); @@ -136,12 +142,10 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - duration: 30 days, + duration: uint32(30 days), maxPerDeposit: 10 ether, stakeCap: 100 ether, metadataURI: "ipfs://duration", - delegationManager: IDelegationManager(address(delegationManagerMock)), - allocationManager: IAllocationManager(address(allocationManagerMock)), operatorSetAVS: OPERATOR_SET_AVS, operatorSetId: OPERATOR_SET_ID, operatorSetRegistrationData: REGISTRATION_DATA, @@ -164,12 +168,10 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - duration: 30 days, + duration: uint32(30 days), maxPerDeposit: 10 ether, stakeCap: 100 ether, metadataURI: "ipfs://duration", - delegationManager: IDelegationManager(address(delegationManagerMock)), - allocationManager: IAllocationManager(address(allocationManagerMock)), operatorSetAVS: OPERATOR_SET_AVS, operatorSetId: OPERATOR_SET_ID, operatorSetRegistrationData: REGISTRATION_DATA, @@ -189,12 +191,10 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { IDurationVaultStrategy.VaultConfig memory config = IDurationVaultStrategy.VaultConfig({ underlyingToken: underlyingToken, vaultAdmin: address(this), - duration: 7 days, + duration: uint32(7 days), maxPerDeposit: 5 ether, stakeCap: 50 ether, metadataURI: "ipfs://duration", - delegationManager: IDelegationManager(address(delegationManagerMock)), - allocationManager: IAllocationManager(address(allocationManagerMock)), operatorSetAVS: OPERATOR_SET_AVS, operatorSetId: OPERATOR_SET_ID, operatorSetRegistrationData: REGISTRATION_DATA, diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index b01f694bc9..288d452c0a 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -50,17 +50,21 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM underlyingToken = new ERC20PresetFixedSupply("Mock Token", "MOCK", INITIAL_SUPPLY, address(this)); - durationVaultImplementation = new DurationVaultStrategy(IStrategyManager(address(strategyManager)), pauserRegistry, "9.9.9"); + durationVaultImplementation = new DurationVaultStrategy( + IStrategyManager(address(strategyManager)), + pauserRegistry, + "9.9.9", + IDelegationManager(address(delegationManagerMock)), + IAllocationManager(address(allocationManagerMock)) + ); IDurationVaultStrategy.VaultConfig memory cfg = IDurationVaultStrategy.VaultConfig({ underlyingToken: IERC20(address(underlyingToken)), vaultAdmin: address(this), - duration: 30 days, + duration: uint32(30 days), maxPerDeposit: 1_000_000 ether, stakeCap: 10_000_000 ether, metadataURI: "ipfs://duration-vault-test", - delegationManager: IDelegationManager(address(delegationManagerMock)), - allocationManager: IAllocationManager(address(allocationManagerMock)), operatorSetAVS: OPERATOR_SET_AVS, operatorSetId: OPERATOR_SET_ID, operatorSetRegistrationData: REGISTRATION_DATA, @@ -140,6 +144,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM durationVault.lock(); cheats.warp(block.timestamp + durationVault.duration() + 1); + durationVault.markMatured(); uint shares = strategyManager.stakerDepositShares(STAKER, IStrategy(address(durationVault))); From c3b46f692f9c595076c466edef3f575edc9d3842 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Dec 2025 18:04:57 -0500 Subject: [PATCH 08/18] refactor: opset ds --- src/contracts/interfaces/IDurationVaultStrategy.sol | 4 ++-- src/contracts/strategies/DurationVaultStrategy.sol | 8 ++++---- src/contracts/strategies/StrategyFactory.sol | 4 ++-- src/test/unit/DurationVaultStrategyUnit.t.sol | 4 ++-- src/test/unit/StrategyFactoryUnit.t.sol | 10 ++++------ src/test/unit/StrategyManagerDurationUnit.t.sol | 4 ++-- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index c3534dd947..ba0a0bd9ce 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IStrategy.sol"; import "./IDelegationManager.sol"; import "./IAllocationManager.sol"; +import "../libraries/OperatorSetLib.sol"; /** * @title Interface for time-bound EigenLayer vault strategies. @@ -26,8 +27,7 @@ interface IDurationVaultStrategy is IStrategy { uint256 maxPerDeposit; uint256 stakeCap; string metadataURI; - address operatorSetAVS; - uint32 operatorSetId; + OperatorSet operatorSet; bytes operatorSetRegistrationData; address delegationApprover; uint32 operatorAllocationDelay; diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index b7de69729f..9b0c238682 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -191,19 +191,19 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL function _configureOperatorIntegration( VaultConfig memory config ) internal { - if (config.operatorSetAVS == address(0) || config.operatorSetId == 0) { + if (config.operatorSet.avs == address(0) || config.operatorSet.id == 0) { revert OperatorIntegrationInvalid(); } - _operatorSet = OperatorSet({avs: config.operatorSetAVS, id: config.operatorSetId}); + _operatorSet = config.operatorSet; delegationManager.registerAsOperator( config.delegationApprover, config.operatorAllocationDelay, config.operatorMetadataURI ); IAllocationManager.RegisterParams memory params; - params.avs = config.operatorSetAVS; + params.avs = config.operatorSet.avs; params.operatorSetIds = new uint32[](1); - params.operatorSetIds[0] = config.operatorSetId; + params.operatorSetIds[0] = config.operatorSet.id; params.data = config.operatorSetRegistrationData; allocationManager.registerForOperatorSets(address(this), params); operatorSetRegistered = true; diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index bab7ba1c3f..f1c3867391 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -200,8 +200,8 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable config.maxPerDeposit, config.stakeCap, config.metadataURI, - config.operatorSetAVS, - config.operatorSetId + config.operatorSet.avs, + config.operatorSet.id ); } } diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol index ac311130d4..7d0f74536c 100644 --- a/src/test/unit/DurationVaultStrategyUnit.t.sol +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -6,6 +6,7 @@ import "../../contracts/strategies/DurationVaultStrategy.sol"; import "../../contracts/interfaces/IDurationVaultStrategy.sol"; import "../../contracts/interfaces/IDelegationManager.sol"; import "../../contracts/interfaces/IAllocationManager.sol"; +import "../../contracts/libraries/OperatorSetLib.sol"; import "../mocks/DelegationManagerMock.sol"; import "../mocks/AllocationManagerMock.sol"; @@ -45,8 +46,7 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { maxPerDeposit: maxPerDeposit, stakeCap: maxTotalDeposits, metadataURI: "ipfs://duration-vault", - operatorSetAVS: OPERATOR_SET_AVS, - operatorSetId: OPERATOR_SET_ID, + operatorSet: OperatorSet({avs: OPERATOR_SET_AVS, id: OPERATOR_SET_ID}), operatorSetRegistrationData: REGISTRATION_DATA, delegationApprover: DELEGATION_APPROVER, operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index 31dcf5df42..3fea414bb6 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -9,6 +9,7 @@ import "src/contracts/strategies/DurationVaultStrategy.sol"; import "../../contracts/interfaces/IDurationVaultStrategy.sol"; import "../../contracts/interfaces/IDelegationManager.sol"; import "../../contracts/interfaces/IAllocationManager.sol"; +import "../../contracts/libraries/OperatorSetLib.sol"; import "src/test/utils/EigenLayerUnitTestSetup.sol"; import "../../contracts/permissions/PauserRegistry.sol"; @@ -146,8 +147,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { maxPerDeposit: 10 ether, stakeCap: 100 ether, metadataURI: "ipfs://duration", - operatorSetAVS: OPERATOR_SET_AVS, - operatorSetId: OPERATOR_SET_ID, + operatorSet: OperatorSet({avs: OPERATOR_SET_AVS, id: OPERATOR_SET_ID}), operatorSetRegistrationData: REGISTRATION_DATA, delegationApprover: DELEGATION_APPROVER, operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, @@ -172,8 +172,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { maxPerDeposit: 10 ether, stakeCap: 100 ether, metadataURI: "ipfs://duration", - operatorSetAVS: OPERATOR_SET_AVS, - operatorSetId: OPERATOR_SET_ID, + operatorSet: OperatorSet({avs: OPERATOR_SET_AVS, id: OPERATOR_SET_ID}), operatorSetRegistrationData: REGISTRATION_DATA, delegationApprover: DELEGATION_APPROVER, operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, @@ -195,8 +194,7 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { maxPerDeposit: 5 ether, stakeCap: 50 ether, metadataURI: "ipfs://duration", - operatorSetAVS: OPERATOR_SET_AVS, - operatorSetId: OPERATOR_SET_ID, + operatorSet: OperatorSet({avs: OPERATOR_SET_AVS, id: OPERATOR_SET_ID}), operatorSetRegistrationData: REGISTRATION_DATA, delegationApprover: DELEGATION_APPROVER, operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index 288d452c0a..09502eff98 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -11,6 +11,7 @@ import "src/contracts/interfaces/IDurationVaultStrategy.sol"; import "src/contracts/interfaces/IStrategy.sol"; import "src/contracts/interfaces/IDelegationManager.sol"; import "src/contracts/interfaces/IAllocationManager.sol"; +import "src/contracts/libraries/OperatorSetLib.sol"; import "src/test/utils/EigenLayerUnitTestSetup.sol"; contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyManagerEvents { @@ -65,8 +66,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM maxPerDeposit: 1_000_000 ether, stakeCap: 10_000_000 ether, metadataURI: "ipfs://duration-vault-test", - operatorSetAVS: OPERATOR_SET_AVS, - operatorSetId: OPERATOR_SET_ID, + operatorSet: OperatorSet({avs: OPERATOR_SET_AVS, id: OPERATOR_SET_ID}), operatorSetRegistrationData: REGISTRATION_DATA, delegationApprover: DELEGATION_APPROVER, operatorAllocationDelay: OPERATOR_ALLOCATION_DELAY, From f42059cb66afb848ac5866c69b51acad6d2098b0 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Dec 2025 20:01:48 -0500 Subject: [PATCH 09/18] chore: error messages --- .../interfaces/IDurationVaultStrategy.sol | 2 + .../strategies/DurationVaultStrategy.sol | 39 ++++++++----------- .../unit/StrategyManagerDurationUnit.t.sol | 1 - 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index ba0a0bd9ce..8673e90fd8 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -52,6 +52,8 @@ interface IDurationVaultStrategy is IStrategy { error DurationNotElapsed(); /// @dev Thrown when attempting to convert a timestamp that exceeds uint32 bounds. error TimestampOverflow(); + /// @dev Thrown when operator integration inputs are missing or invalid. + error OperatorIntegrationInvalid(); event VaultInitialized( address indexed vaultAdmin, diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index 9b0c238682..d34ccccc45 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.27; import "./StrategyBaseTVLLimits.sol"; import "./DurationVaultStrategyStorage.sol"; +import "../interfaces/IDurationVaultStrategy.sol"; import "../interfaces/IDelegationManager.sol"; import "../interfaces/IAllocationManager.sol"; import "../libraries/OperatorSetLib.sol"; @@ -27,10 +28,8 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL /// @notice Allocation manager reference used to register/allocate operator sets. IAllocationManager public immutable override allocationManager; - error OperatorIntegrationInvalid(); - modifier onlyVaultAdmin() { - if (msg.sender != vaultAdmin) revert OnlyVaultAdmin(); + require(msg.sender == vaultAdmin, OnlyVaultAdmin()); _; } @@ -41,9 +40,10 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL IDelegationManager _delegationManager, IAllocationManager _allocationManager ) StrategyBaseTVLLimits(_strategyManager, _pauserRegistry, _version) { - if (address(_delegationManager) == address(0) || address(_allocationManager) == address(0)) { - revert OperatorIntegrationInvalid(); - } + require( + address(_delegationManager) != address(0) && address(_allocationManager) != address(0), + OperatorIntegrationInvalid() + ); delegationManager = _delegationManager; allocationManager = _allocationManager; } @@ -54,8 +54,8 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL function initialize( VaultConfig memory config ) public initializer { - if (config.vaultAdmin == address(0)) revert InvalidVaultAdmin(); - if (config.duration == 0 || config.duration > MAX_DURATION) revert InvalidDuration(); + require(config.vaultAdmin != address(0), InvalidVaultAdmin()); + require(config.duration != 0 && config.duration <= MAX_DURATION, InvalidDuration()); _setTVLLimits(config.maxPerDeposit, config.stakeCap); _initializeStrategyBase(config.underlyingToken); @@ -75,12 +75,12 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL * @notice Locks the vault, preventing new deposits and withdrawals until maturity. */ function lock() external override onlyVaultAdmin { - if (_state != VaultState.Deposits) revert VaultAlreadyLocked(); + require(_state == VaultState.Deposits, VaultAlreadyLocked()); uint32 currentTimestamp = _currentTimestamp(); lockedAt = currentTimestamp; uint32 newUnlockAt = currentTimestamp + duration; - if (newUnlockAt < currentTimestamp) revert InvalidDuration(); + require(newUnlockAt >= currentTimestamp, InvalidDuration()); unlockAt = newUnlockAt; _state = VaultState.Allocations; @@ -98,7 +98,8 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL // already recorded; noop return; } - if (_state != VaultState.Allocations || block.timestamp < unlockAt) revert DurationNotElapsed(); + require(_state == VaultState.Allocations, DurationNotElapsed()); + require(block.timestamp >= unlockAt, DurationNotElapsed()); _state = VaultState.Withdrawals; maturedAt = _currentTimestamp(); @@ -124,7 +125,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL function transferVaultAdmin( address newVaultAdmin ) external override onlyVaultAdmin { - if (newVaultAdmin == address(0)) revert InvalidVaultAdmin(); + require(newVaultAdmin != address(0), InvalidVaultAdmin()); emit VaultAdminUpdated(vaultAdmin, newVaultAdmin); vaultAdmin = newVaultAdmin; } @@ -175,25 +176,19 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL } function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override { - if (!depositsOpen()) { - revert DepositsLocked(); - } + require(depositsOpen(), DepositsLocked()); super._beforeDeposit(token, amount); } function _beforeWithdrawal(address recipient, IERC20 token, uint256 amountShares) internal virtual override { - if (!withdrawalsOpen()) { - revert WithdrawalsLocked(); - } + require(withdrawalsOpen(), WithdrawalsLocked()); super._beforeWithdrawal(recipient, token, amountShares); } function _configureOperatorIntegration( VaultConfig memory config ) internal { - if (config.operatorSet.avs == address(0) || config.operatorSet.id == 0) { - revert OperatorIntegrationInvalid(); - } + require(config.operatorSet.avs != address(0) && config.operatorSet.id != 0, OperatorIntegrationInvalid()); _operatorSet = config.operatorSet; delegationManager.registerAsOperator( @@ -247,7 +242,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL */ function _currentTimestamp() internal view returns (uint32) { uint256 ts = block.timestamp; - if (ts > type(uint32).max) revert TimestampOverflow(); + require(ts <= type(uint32).max, TimestampOverflow()); return uint32(ts); } } diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index 09502eff98..d458ac02d4 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -31,7 +31,6 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM uint32 internal constant OPERATOR_ALLOCATION_DELAY = 3; string internal constant OPERATOR_METADATA_URI = "ipfs://strategy-manager-vault"; bytes internal constant REGISTRATION_DATA = hex"DEADBEEF"; - function setUp() public override { EigenLayerUnitTestSetup.setUp(); From 6ae6c8dac47fb10ac59efdf2866a305ca5fe9aa2 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Dec 2025 13:43:38 -0500 Subject: [PATCH 10/18] refactor: remove unneeded helpers --- .../interfaces/IDurationVaultStrategy.sol | 38 ++---- src/contracts/interfaces/IStrategyFactory.sol | 18 +-- .../strategies/DurationVaultStrategy.sol | 109 +++++++----------- .../DurationVaultStrategyStorage.sol | 30 ++--- src/contracts/strategies/StrategyFactory.sol | 17 ++- .../strategies/StrategyFactoryStorage.sol | 8 +- src/test/unit/DurationVaultStrategyUnit.t.sol | 1 - src/test/unit/StrategyFactoryUnit.t.sol | 3 +- .../unit/StrategyManagerDurationUnit.t.sol | 2 +- 9 files changed, 83 insertions(+), 143 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index 8673e90fd8..32fff475fa 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -8,16 +8,15 @@ import "./IDelegationManager.sol"; import "./IAllocationManager.sol"; import "../libraries/OperatorSetLib.sol"; -/** - * @title Interface for time-bound EigenLayer vault strategies. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - */ +/// @title Interface for time-bound EigenLayer vault strategies. +/// @author Layr Labs, Inc. +/// @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service interface IDurationVaultStrategy is IStrategy { enum VaultState { - Deposits, - Allocations, - Withdrawals + UNINITIALIZED, + DEPOSITS, + ALLOCATIONS, + WITHDRAWALS } struct VaultConfig { @@ -68,35 +67,20 @@ interface IDurationVaultStrategy is IStrategy { event VaultMatured(uint32 maturedAt); - event VaultAdminUpdated(address indexed previousAdmin, address indexed newAdmin); - event MetadataURIUpdated(string newMetadataURI); - /** - * @notice Locks the vault, preventing further deposits / withdrawals until maturity. - */ + /// @notice Locks the vault, preventing further deposits / withdrawals until maturity. function lock() external; - /** - * @notice Marks the vault as matured once the configured duration has elapsed. - * @dev After maturation, withdrawals are permitted while deposits remain disabled. - */ + /// @notice Marks the vault as matured once the configured duration has elapsed. + /// @dev After maturation, withdrawals are permitted while deposits remain disabled. function markMatured() external; - /** - * @notice Updates the vault metadata URI. - */ + /// @notice Updates the vault metadata URI. function updateMetadataURI( string calldata newMetadataURI ) external; - /** - * @notice Transfers vault admin privileges to a new address. - */ - function transferVaultAdmin( - address newVaultAdmin - ) external; - function vaultAdmin() external view returns (address); function duration() external view returns (uint32); function lockedAt() external view returns (uint32); diff --git a/src/contracts/interfaces/IStrategyFactory.sol b/src/contracts/interfaces/IStrategyFactory.sol index 7ce2ccfb8c..04987e1166 100644 --- a/src/contracts/interfaces/IStrategyFactory.sol +++ b/src/contracts/interfaces/IStrategyFactory.sol @@ -49,24 +49,18 @@ interface IStrategyFactory { IERC20 token ) external returns (IStrategy newStrategy); - /** - * @notice Deploys a new duration-bound vault strategy contract. - * @dev Enforces the same blacklist semantics as vanilla strategies. - */ + /// @notice Deploys a new duration-bound vault strategy contract. + /// @dev Enforces the same blacklist semantics as vanilla strategies. function deployDurationVaultStrategy( IDurationVaultStrategy.VaultConfig calldata config ) external returns (IDurationVaultStrategy newVault); - /** - * @notice Returns all duration vaults that have ever been deployed for a given token. - */ + /// @notice Returns all duration vaults that have ever been deployed for a given token. function getDurationVaults( IERC20 token ) external view returns (IDurationVaultStrategy[] memory); - /** - * @notice Owner-only function to pass through a call to `StrategyManager.addStrategiesToDepositWhitelist` - */ + /// @notice Owner-only function to pass through a call to `StrategyManager.addStrategiesToDepositWhitelist` function whitelistStrategies( IStrategy[] calldata strategiesToWhitelist ) external; @@ -76,9 +70,7 @@ interface IStrategyFactory { IStrategy[] calldata strategiesToRemoveFromWhitelist ) external; - /** - * @notice Owner-only function to update the beacon used for deploying duration vault strategies. - */ + /// @notice Owner-only function to update the beacon used for deploying duration vault strategies. function setDurationVaultBeacon( IBeacon newDurationVaultBeacon ) external; diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index d34ccccc45..c77dc4e0e7 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -8,20 +8,12 @@ import "../interfaces/IDelegationManager.sol"; import "../interfaces/IAllocationManager.sol"; import "../libraries/OperatorSetLib.sol"; -/** - * @title Duration-bound EigenLayer vault strategy with configurable deposit caps and windows. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - */ +/// @title Duration-bound EigenLayer vault strategy with configurable deposit caps and windows. +/// @author Layr Labs, Inc. +/// @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLLimits { using OperatorSetLib for OperatorSet; - /// @notice Constant representing the full allocation magnitude (1 WAD) for allocation manager calls. - uint64 internal constant FULL_ALLOCATION = 1e18; - - /// @notice Maximum allowable duration (approximately 2 years). - uint32 internal constant MAX_DURATION = uint32(2 * 365 days); - /// @notice Delegation manager reference used to register the vault as an operator. IDelegationManager public immutable override delegationManager; @@ -36,10 +28,9 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL constructor( IStrategyManager _strategyManager, IPauserRegistry _pauserRegistry, - string memory _version, IDelegationManager _delegationManager, IAllocationManager _allocationManager - ) StrategyBaseTVLLimits(_strategyManager, _pauserRegistry, _version) { + ) StrategyBaseTVLLimits(_strategyManager, _pauserRegistry) { require( address(_delegationManager) != address(0) && address(_allocationManager) != address(0), OperatorIntegrationInvalid() @@ -48,9 +39,7 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL allocationManager = _allocationManager; } - /** - * @notice Initializes the vault configuration. - */ + /// @notice Initializes the vault configuration. function initialize( VaultConfig memory config ) public initializer { @@ -64,54 +53,52 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL metadataURI = config.metadataURI; _configureOperatorIntegration(config); - _state = VaultState.Deposits; + _state = VaultState.DEPOSITS; emit VaultInitialized( - vaultAdmin, config.underlyingToken, duration, config.maxPerDeposit, config.stakeCap, metadataURI + vaultAdmin, + config.underlyingToken, + duration, + config.maxPerDeposit, + config.stakeCap, + metadataURI ); } - /** - * @notice Locks the vault, preventing new deposits and withdrawals until maturity. - */ + /// @notice Locks the vault, preventing new deposits and withdrawals until maturity. function lock() external override onlyVaultAdmin { - require(_state == VaultState.Deposits, VaultAlreadyLocked()); + require(_state == VaultState.DEPOSITS, VaultAlreadyLocked()); - uint32 currentTimestamp = _currentTimestamp(); + uint32 currentTimestamp = uint32(block.timestamp); lockedAt = currentTimestamp; uint32 newUnlockAt = currentTimestamp + duration; require(newUnlockAt >= currentTimestamp, InvalidDuration()); unlockAt = newUnlockAt; - _state = VaultState.Allocations; + _state = VaultState.ALLOCATIONS; emit VaultLocked(lockedAt, unlockAt); _allocateFullMagnitude(); } - /** - * @notice Marks the vault as matured once the configured duration elapses. Callable by anyone. - */ + /// @notice Marks the vault as matured once the configured duration elapses. Callable by anyone. function markMatured() external override { - if (_state == VaultState.Withdrawals) { + if (_state == VaultState.WITHDRAWALS) { // already recorded; noop return; } - require(_state == VaultState.Allocations, DurationNotElapsed()); + require(_state == VaultState.ALLOCATIONS, DurationNotElapsed()); require(block.timestamp >= unlockAt, DurationNotElapsed()); - - _state = VaultState.Withdrawals; - maturedAt = _currentTimestamp(); + _state = VaultState.WITHDRAWALS; + maturedAt = uint32(block.timestamp); emit VaultMatured(maturedAt); _deallocateAll(); _deregisterFromOperatorSet(); } - /** - * @notice Updates the metadata URI describing the vault. - */ + /// @notice Updates the metadata URI describing the vault. function updateMetadataURI( string calldata newMetadataURI ) external override onlyVaultAdmin { @@ -119,17 +106,6 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL emit MetadataURIUpdated(newMetadataURI); } - /** - * @notice Transfers admin privileges to a new address. - */ - function transferVaultAdmin( - address newVaultAdmin - ) external override onlyVaultAdmin { - require(newVaultAdmin != address(0), InvalidVaultAdmin()); - emit VaultAdminUpdated(vaultAdmin, newVaultAdmin); - vaultAdmin = newVaultAdmin; - } - /// @inheritdoc IDurationVaultStrategy function unlockTimestamp() public view override returns (uint32) { return unlockAt; @@ -137,12 +113,12 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL /// @inheritdoc IDurationVaultStrategy function isLocked() public view override returns (bool) { - return _state != VaultState.Deposits; + return _state != VaultState.DEPOSITS; } /// @inheritdoc IDurationVaultStrategy function isMatured() public view override returns (bool) { - return _state == VaultState.Withdrawals; + return _state == VaultState.WITHDRAWALS; } /// @inheritdoc IDurationVaultStrategy @@ -157,12 +133,12 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL /// @inheritdoc IDurationVaultStrategy function depositsOpen() public view override returns (bool) { - return _state == VaultState.Deposits; + return _state == VaultState.DEPOSITS; } /// @inheritdoc IDurationVaultStrategy function withdrawalsOpen() public view override returns (bool) { - return _state != VaultState.Allocations; + return _state != VaultState.ALLOCATIONS; } /// @inheritdoc IDurationVaultStrategy @@ -175,12 +151,27 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL return (_operatorSet.avs, _operatorSet.id); } - function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override { + function operatorSetRegistered() public view override returns (bool) { + return _state == VaultState.DEPOSITS || _state == VaultState.ALLOCATIONS; + } + + function allocationsActive() public view override returns (bool) { + return _state == VaultState.ALLOCATIONS; + } + + function _beforeDeposit( + IERC20 token, + uint256 amount + ) internal virtual override { require(depositsOpen(), DepositsLocked()); super._beforeDeposit(token, amount); } - function _beforeWithdrawal(address recipient, IERC20 token, uint256 amountShares) internal virtual override { + function _beforeWithdrawal( + address recipient, + IERC20 token, + uint256 amountShares + ) internal virtual override { require(withdrawalsOpen(), WithdrawalsLocked()); super._beforeWithdrawal(recipient, token, amountShares); } @@ -201,7 +192,6 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL params.operatorSetIds[0] = config.operatorSet.id; params.data = config.operatorSetRegistrationData; allocationManager.registerForOperatorSets(address(this), params); - operatorSetRegistered = true; } function _allocateFullMagnitude() internal { @@ -212,7 +202,6 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL params[0].newMagnitudes = new uint64[](1); params[0].newMagnitudes[0] = FULL_ALLOCATION; allocationManager.modifyAllocations(address(this), params); - allocationsActive = true; } function _deallocateAll() internal { @@ -223,7 +212,6 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL params[0].newMagnitudes = new uint64[](1); params[0].newMagnitudes[0] = 0; allocationManager.modifyAllocations(address(this), params); - allocationsActive = false; } function _deregisterFromOperatorSet() internal { @@ -233,16 +221,5 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL params.operatorSetIds = new uint32[](1); params.operatorSetIds[0] = _operatorSet.id; allocationManager.deregisterFromOperatorSets(params); - operatorSetRegistered = false; - } - - /** - * @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. - */ - function _currentTimestamp() internal view returns (uint32) { - uint256 ts = block.timestamp; - require(ts <= type(uint32).max, TimestampOverflow()); - return uint32(ts); } } diff --git a/src/contracts/strategies/DurationVaultStrategyStorage.sol b/src/contracts/strategies/DurationVaultStrategyStorage.sol index d946e48c1e..ae49419a05 100644 --- a/src/contracts/strategies/DurationVaultStrategyStorage.sol +++ b/src/contracts/strategies/DurationVaultStrategyStorage.sol @@ -4,12 +4,16 @@ pragma solidity ^0.8.27; import "../interfaces/IDurationVaultStrategy.sol"; import "../libraries/OperatorSetLib.sol"; -/** - * @title Storage layout for DurationVaultStrategy. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - */ +/// @title Storage layout for DurationVaultStrategy. +/// @author Layr Labs, Inc. +/// @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service abstract contract DurationVaultStrategyStorage is IDurationVaultStrategy { + /// @notice Constant representing the full allocation magnitude (1 WAD) for allocation manager calls. + uint64 internal constant FULL_ALLOCATION = 1e18; + + /// @notice Maximum allowable duration (approximately 2 years). + uint32 internal constant MAX_DURATION = uint32(2 * 365 days); + /// @notice Address empowered to configure and lock the vault. address public vaultAdmin; @@ -34,17 +38,7 @@ abstract contract DurationVaultStrategyStorage is IDurationVaultStrategy { /// @notice Tracks the lifecycle of the vault (deposits -> allocations -> withdrawals). VaultState internal _state; - /// @notice True when allocations are currently active (i.e. slashable) for the configured operator set. - bool public allocationsActive; - - /// @notice True when the vault remains registered for the operator set. - bool public operatorSetRegistered; - - /** - * @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[41] private __gap; + /// @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[43] private __gap; } - - diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index 800bdb7414..3ad3e4daab 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -102,9 +102,7 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable strategyManager.addStrategiesToDepositWhitelist(strategiesToWhitelist); } - /** - * @notice Deploys a new duration vault strategy backed by the configured beacon. - */ + /// @notice Deploys a new duration vault strategy backed by the configured beacon. function deployDurationVaultStrategy( IDurationVaultStrategy.VaultConfig calldata config ) external onlyWhenNotPaused(PAUSED_NEW_STRATEGIES) returns (IDurationVaultStrategy newVault) { @@ -129,9 +127,7 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable _emitDurationVaultDeployed(newVault, underlyingToken, config); } - /** - * @notice Owner-only function to pass through a call to `StrategyManager.removeStrategiesFromDepositWhitelist` - */ + /// @notice Owner-only function to pass through a call to `StrategyManager.removeStrategiesFromDepositWhitelist` function removeStrategiesFromWhitelist( IStrategy[] calldata strategiesToRemoveFromWhitelist ) external onlyOwner { @@ -153,9 +149,7 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable strategyBeacon = _strategyBeacon; } - /** - * @notice Owner-only function to update the duration vault beacon. - */ + /// @notice Owner-only function to update the duration vault beacon. function setDurationVaultBeacon( IBeacon newDurationVaultBeacon ) external onlyOwner { @@ -176,7 +170,10 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable durationVaultBeacon = newDurationVaultBeacon; } - function _registerDurationVault(IERC20 token, IDurationVaultStrategy vault) internal { + function _registerDurationVault( + IERC20 token, + IDurationVaultStrategy vault + ) internal { durationVaultsByToken[token].push(vault); } diff --git a/src/contracts/strategies/StrategyFactoryStorage.sol b/src/contracts/strategies/StrategyFactoryStorage.sol index b3bfe81776..e580352529 100644 --- a/src/contracts/strategies/StrategyFactoryStorage.sol +++ b/src/contracts/strategies/StrategyFactoryStorage.sol @@ -28,10 +28,8 @@ abstract contract StrategyFactoryStorage is IStrategyFactory { /// @notice Mapping token => all duration vault strategies deployed for the token. mapping(IERC20 => IDurationVaultStrategy[]) internal durationVaultsByToken; - /** - * @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. - * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - */ + /// @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. + /// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps uint256[46] private __gap; } diff --git a/src/test/unit/DurationVaultStrategyUnit.t.sol b/src/test/unit/DurationVaultStrategyUnit.t.sol index 7d0f74536c..0cc0d0a26d 100644 --- a/src/test/unit/DurationVaultStrategyUnit.t.sol +++ b/src/test/unit/DurationVaultStrategyUnit.t.sol @@ -34,7 +34,6 @@ contract DurationVaultStrategyUnitTests is StrategyBaseTVLLimitsUnitTests { durationVaultImplementation = new DurationVaultStrategy( strategyManager, pauserRegistry, - "9.9.9", IDelegationManager(address(delegationManagerMock)), IAllocationManager(address(allocationManagerMock)) ); diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index a88e87a006..b72277413a 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -65,14 +65,13 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { durationVaultImplementation = new DurationVaultStrategy( IStrategyManager(address(strategyManagerMock)), pauserRegistry, - "9.9.9", IDelegationManager(address(delegationManagerMock)), IAllocationManager(address(allocationManagerMock)) ); durationVaultBeacon = new UpgradeableBeacon(address(durationVaultImplementation)); durationVaultBeacon.transferOwnership(beaconProxyOwner); - strategyFactoryImplementation = new StrategyFactory(IStrategyManager(address(strategyManagerMock)), pauserRegistry, "9.9.9"); + strategyFactoryImplementation = new StrategyFactory(IStrategyManager(address(strategyManagerMock)), pauserRegistry); strategyFactory = StrategyFactory( address( diff --git a/src/test/unit/StrategyManagerDurationUnit.t.sol b/src/test/unit/StrategyManagerDurationUnit.t.sol index d458ac02d4..568327ff88 100644 --- a/src/test/unit/StrategyManagerDurationUnit.t.sol +++ b/src/test/unit/StrategyManagerDurationUnit.t.sol @@ -31,6 +31,7 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM uint32 internal constant OPERATOR_ALLOCATION_DELAY = 3; string internal constant OPERATOR_METADATA_URI = "ipfs://strategy-manager-vault"; bytes internal constant REGISTRATION_DATA = hex"DEADBEEF"; + function setUp() public override { EigenLayerUnitTestSetup.setUp(); @@ -53,7 +54,6 @@ contract StrategyManagerDurationUnitTests is EigenLayerUnitTestSetup, IStrategyM durationVaultImplementation = new DurationVaultStrategy( IStrategyManager(address(strategyManager)), pauserRegistry, - "9.9.9", IDelegationManager(address(delegationManagerMock)), IAllocationManager(address(allocationManagerMock)) ); From 0334bb274b3cbee054171eadaf9dd453432e75e7 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Dec 2025 19:53:40 -0500 Subject: [PATCH 11/18] chore: remove unneeded error --- src/contracts/interfaces/IDurationVaultStrategy.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index 32fff475fa..9f46d14f3c 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -41,16 +41,12 @@ interface IDurationVaultStrategy is IStrategy { error OnlyVaultAdmin(); /// @dev Thrown when attempting to lock an already locked vault. error VaultAlreadyLocked(); - /// @dev Thrown when attempting to unlock or update a vault that has not been locked yet. - error VaultNotLocked(); /// @dev Thrown when attempting to deposit after the vault has been locked. error DepositsLocked(); /// @dev Thrown when attempting to withdraw while funds remain locked. error WithdrawalsLocked(); /// @dev Thrown when attempting to mark the vault as matured before duration elapses. error DurationNotElapsed(); - /// @dev Thrown when attempting to convert a timestamp that exceeds uint32 bounds. - error TimestampOverflow(); /// @dev Thrown when operator integration inputs are missing or invalid. error OperatorIntegrationInvalid(); From baeae247823001be1609e79fce4c566a4f00aa5d Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 17:48:38 -0500 Subject: [PATCH 12/18] refactor: changeable TVL limits --- src/contracts/interfaces/IDurationVaultStrategy.sol | 7 +++++++ src/contracts/strategies/DurationVaultStrategy.sol | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/contracts/interfaces/IDurationVaultStrategy.sol b/src/contracts/interfaces/IDurationVaultStrategy.sol index 9f46d14f3c..817d9dc25f 100644 --- a/src/contracts/interfaces/IDurationVaultStrategy.sol +++ b/src/contracts/interfaces/IDurationVaultStrategy.sol @@ -77,6 +77,13 @@ interface IDurationVaultStrategy is IStrategy { string calldata newMetadataURI ) external; + /// @notice Updates the TVL limits for max deposit per transaction and total stake cap. + /// @dev Only callable by the vault admin while deposits are open (before lock). + function updateTVLLimits( + uint256 newMaxPerDeposit, + uint256 newStakeCap + ) external; + function vaultAdmin() external view returns (address); function duration() external view returns (uint32); function lockedAt() external view returns (uint32); diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index c77dc4e0e7..a42dc7d74f 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -106,6 +106,16 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL emit MetadataURIUpdated(newMetadataURI); } + /// @notice Updates the TVL limits for max deposit per transaction and total stake cap. + /// @dev Only callable by the vault admin while deposits are open (before lock). + function updateTVLLimits( + uint256 newMaxPerDeposit, + uint256 newStakeCap + ) external override onlyVaultAdmin { + require(depositsOpen(), DepositsLocked()); + _setTVLLimits(newMaxPerDeposit, newStakeCap); + } + /// @inheritdoc IDurationVaultStrategy function unlockTimestamp() public view override returns (uint32) { return unlockAt; From f345dfe69e2e528a87a9122ded247b939c37a141 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 19:17:22 -0500 Subject: [PATCH 13/18] test: integration tests --- script/tasks/build_nonsigner_proof.s.sol | 46 +++ script/tasks/generate_bn254_cert.s.sol | 170 +++++++++ script/utils/ExistingDeploymentParser.sol | 3 + .../strategies/DurationVaultStrategy.sol | 6 +- .../integration/IntegrationDeployer.t.sol | 16 + .../tests/DurationVaultIntegration.t.sol | 335 ++++++++++++++++++ src/test/integration/users/AVS.t.sol | 15 +- 7 files changed, 587 insertions(+), 4 deletions(-) create mode 100644 script/tasks/build_nonsigner_proof.s.sol create mode 100644 script/tasks/generate_bn254_cert.s.sol create mode 100644 src/test/integration/tests/DurationVaultIntegration.t.sol diff --git a/script/tasks/build_nonsigner_proof.s.sol b/script/tasks/build_nonsigner_proof.s.sol new file mode 100644 index 0000000000..c0f7778500 --- /dev/null +++ b/script/tasks/build_nonsigner_proof.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; + +import {BN254} from "src/contracts/libraries/BN254.sol"; +import {Merkle} from "src/contracts/libraries/Merkle.sol"; +import {LeafCalculatorMixin} from "src/contracts/mixins/LeafCalculatorMixin.sol"; +import {IOperatorTableCalculatorTypes as Types} from "src/contracts/interfaces/IOperatorTableCalculator.sol"; + +contract BuildNonsignerProof is Script, LeafCalculatorMixin { + using BN254 for *; + + // Build a keccak Merkle proof for a 2-operator tree (index 0 or 1) + // Inputs are operator G1 pubkeys and weight arrays + function runTwoPacked( + uint256[2] memory op1XY, + uint256[] memory w1, + uint256[2] memory op2XY, + uint256[] memory w2, + uint32 indexToProve + ) external pure returns (bytes memory proof, bytes32 root, bytes32 leaf) { + Types.BN254OperatorInfo[] memory ops = new Types.BN254OperatorInfo[](2); + ops[0].pubkey = BN254.G1Point(op1XY[0], op1XY[1]); + ops[0].weights = w1; + ops[1].pubkey = BN254.G1Point(op2XY[0], op2XY[1]); + ops[1].weights = w2; + + bytes32[] memory leaves = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + leaves[i] = calculateOperatorInfoLeaf(ops[i]); + } + + root = Merkle.merkleizeKeccak(leaves); + proof = Merkle.getProofKeccak(leaves, indexToProve); + leaf = leaves[indexToProve]; + + console2.log("root"); + console2.logBytes32(root); + console2.log("leaf"); + console2.logBytes32(leaf); + console2.log("proof bytes len"); + console2.logUint(proof.length); + } +} diff --git a/script/tasks/generate_bn254_cert.s.sol b/script/tasks/generate_bn254_cert.s.sol new file mode 100644 index 0000000000..3813ed39db --- /dev/null +++ b/script/tasks/generate_bn254_cert.s.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; + +import {BN254} from "src/contracts/libraries/BN254.sol"; +import {OperatorSet} from "src/contracts/libraries/OperatorSetLib.sol"; +import {IBN254CertificateVerifier} from "src/contracts/interfaces/IBN254CertificateVerifier.sol"; +import {IKeyRegistrar} from "src/contracts/interfaces/IKeyRegistrar.sol"; + +// Test utilities (FFI-based G2 mul and add) +import {BN256G2} from "src/test/utils/BN256G2.sol"; + +contract GenerateBN254Cert is Script { + using BN254 for BN254.G1Point; + + function _g2Mul( + uint256 sk + ) internal returns (BN254.G2Point memory g2) { + // Multiply G2 generator by private key using FFI helper + // BN256G2 has no mul API; reuse OperatorWalletLib.mul logic via BN256G2 + ffi helper + // We reconstruct the same flow inline here to avoid importing the VM cheat directly. + // BN256G2.ECTwistMul is not available, so we rely on the go helper invoked by BN256G2 via ffi + // However BN256G2 library exposes only add; we keep the FFI path present in src/test/utils/BN256G2.go via forge --ffi. + + // We call the same go helper that tests use, via vm.ffi under the hood. + // Here we replicate it by invoking the cheatcode address directly. + address HEVM = address(uint160(uint256(keccak256("hevm cheat code")))); + bytes memory out; + + // x1 (index 1) + { + bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "1")); + (bool ok, bytes memory res) = HEVM.call(input); + require(ok, "ffi x1 failed"); + out = res; + g2.X[1] = abi.decode(out, (uint256)); + } + // x0 (index 2) + { + bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "2")); + (bool ok, bytes memory res) = HEVM.call(input); + require(ok, "ffi x0 failed"); + out = res; + g2.X[0] = abi.decode(out, (uint256)); + } + // y1 (index 3) + { + bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "3")); + (bool ok, bytes memory res) = HEVM.call(input); + require(ok, "ffi y1 failed"); + out = res; + g2.Y[1] = abi.decode(out, (uint256)); + } + // y0 (index 4) + { + bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "4")); + (bool ok, bytes memory res) = HEVM.call(input); + require(ok, "ffi y0 failed"); + out = res; + g2.Y[0] = abi.decode(out, (uint256)); + } + } + + function _ffiArgs( + uint256 sk, + string memory which + ) internal pure returns (string[] memory args) { + args = new string[](5); + args[0] = "go"; + args[1] = "run"; + args[2] = "src/test/utils/g2mul.go"; + args[3] = vm.toString(sk); + args[4] = which; + } + + // Single-signer helper: gets apk from KeyRegistrar, no FFI, no broadcasts + function runSingle( + address bn254Verifier, + address keyRegistrar, + address avs, + uint32 setId, + address operator, + uint32 referenceTimestamp, + bytes32 messageHash, + uint256 signerSk + ) external view returns (bytes32 digest, BN254.G1Point memory sigAgg, BN254.G2Point memory apkAgg) { + digest = IBN254CertificateVerifier(bn254Verifier).calculateCertificateDigest(referenceTimestamp, messageHash); + + BN254.G1Point memory h = BN254.hashToG1(digest); + sigAgg = h.scalar_mul(signerSk); + + (, BN254.G2Point memory pkG2) = IKeyRegistrar(keyRegistrar).getBN254Key(OperatorSet(avs, setId), operator); + apkAgg = pkG2; + + console2.log("digest"); + console2.logBytes32(digest); + console2.log("sigAgg.X"); + console2.logUint(sigAgg.X); + console2.log("sigAgg.Y"); + console2.logUint(sigAgg.Y); + console2.log("apk.X0"); + console2.logUint(apkAgg.X[0]); + console2.log("apk.X1"); + console2.logUint(apkAgg.X[1]); + console2.log("apk.Y0"); + console2.logUint(apkAgg.Y[0]); + console2.log("apk.Y1"); + console2.logUint(apkAgg.Y[1]); + } + + function run( + address bn254Verifier, + uint32 referenceTimestamp, + bytes32 messageHash, + uint256 signerSk1, + uint256 signerSk2 + ) external returns (bytes32 digest, BN254.G1Point memory sigAgg, BN254.G2Point memory apkAgg) { + digest = IBN254CertificateVerifier(bn254Verifier).calculateCertificateDigest(referenceTimestamp, messageHash); + + BN254.G1Point memory h = BN254.hashToG1(digest); + BN254.G1Point memory sig1 = h.scalar_mul(signerSk1); + BN254.G1Point memory sig2 = h.scalar_mul(signerSk2); + sigAgg = sig1.plus(sig2); + + BN254.G2Point memory pk1 = _g2Mul(signerSk1); + BN254.G2Point memory pk2 = _g2Mul(signerSk2); + // Aggregate in G2: apk = pk1 + pk2 + (apkAgg.X[0], apkAgg.X[1], apkAgg.Y[0], apkAgg.Y[1]) = + BN256G2.ECTwistAdd(pk1.X[0], pk1.X[1], pk1.Y[0], pk1.Y[1], pk2.X[0], pk2.X[1], pk2.Y[0], pk2.Y[1]); + + console2.log("digest"); + console2.logBytes32(digest); + console2.log("sigAgg.X"); + console2.logUint(sigAgg.X); + console2.log("sigAgg.Y"); + console2.logUint(sigAgg.Y); + console2.log("apk.X0"); + console2.logUint(apkAgg.X[0]); + console2.log("apk.X1"); + console2.logUint(apkAgg.X[1]); + console2.log("apk.Y0"); + console2.logUint(apkAgg.Y[0]); + console2.log("apk.Y1"); + console2.logUint(apkAgg.Y[1]); + } + + // Fully offline: compute digest locally and accept G2 pubkey directly. + function runRaw( + uint32 referenceTimestamp, + bytes32 messageHash, + uint256 signerSk, + uint256 apkX0, + uint256 apkX1, + uint256 apkY0, + uint256 apkY1 + ) external view returns (bytes32 digest, BN254.G1Point memory sig, BN254.G2Point memory apk) { + bytes32 TYPEHASH = keccak256("BN254Certificate(uint32 referenceTimestamp,bytes32 messageHash)"); + digest = keccak256(abi.encode(TYPEHASH, referenceTimestamp, messageHash)); + + BN254.G1Point memory h = BN254.hashToG1(digest); + sig = h.scalar_mul(signerSk); + + apk.X[0] = apkX0; + apk.X[1] = apkX1; + apk.Y[0] = apkY0; + apk.Y[1] = apkY1; + } +} diff --git a/script/utils/ExistingDeploymentParser.sol b/script/utils/ExistingDeploymentParser.sol index 8485bcedd0..6615f5eb24 100644 --- a/script/utils/ExistingDeploymentParser.sol +++ b/script/utils/ExistingDeploymentParser.sol @@ -16,6 +16,7 @@ import "../../src/contracts/permissions/PermissionController.sol"; import "../../src/contracts/strategies/StrategyFactory.sol"; import "../../src/contracts/strategies/StrategyBase.sol"; import "../../src/contracts/strategies/StrategyBaseTVLLimits.sol"; +import "../../src/contracts/strategies/DurationVaultStrategy.sol"; import "../../src/contracts/strategies/EigenStrategy.sol"; import "../../src/contracts/pods/EigenPod.sol"; @@ -103,6 +104,7 @@ contract ExistingDeploymentParser is Script, Logger { PauserRegistry public eigenLayerPauserReg; UpgradeableBeacon public eigenPodBeacon; UpgradeableBeacon public strategyBeacon; + UpgradeableBeacon public durationVaultBeacon; /// @dev AllocationManager IAllocationManager public allocationManager; @@ -138,6 +140,7 @@ contract ExistingDeploymentParser is Script, Logger { StrategyFactory public strategyFactory; StrategyFactory public strategyFactoryImplementation; StrategyBase public baseStrategyImplementation; + DurationVaultStrategy public durationVaultImplementation; StrategyBase public strategyFactoryBeaconImplementation; // Token diff --git a/src/contracts/strategies/DurationVaultStrategy.sol b/src/contracts/strategies/DurationVaultStrategy.sol index a42dc7d74f..bb6d3b805c 100644 --- a/src/contracts/strategies/DurationVaultStrategy.sol +++ b/src/contracts/strategies/DurationVaultStrategy.sol @@ -182,7 +182,11 @@ contract DurationVaultStrategy is DurationVaultStrategyStorage, StrategyBaseTVLL IERC20 token, uint256 amountShares ) internal virtual override { - require(withdrawalsOpen(), WithdrawalsLocked()); + if (!withdrawalsOpen()) { + address redistributionRecipient = allocationManager.getRedistributionRecipient(_operatorSet); + bool isRedistribution = recipient == redistributionRecipient; + require(isRedistribution, WithdrawalsLocked()); + } super._beforeWithdrawal(recipient, token, amountShares); } diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index e50b6325df..d94868f54f 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -16,6 +16,7 @@ import "src/contracts/core/StrategyManager.sol"; import "src/contracts/strategies/StrategyFactory.sol"; import "src/contracts/strategies/StrategyBase.sol"; import "src/contracts/strategies/StrategyBaseTVLLimits.sol"; +import "src/contracts/strategies/DurationVaultStrategy.sol"; import "src/contracts/pods/EigenPodManager.sol"; import "src/contracts/pods/EigenPod.sol"; import "src/contracts/permissions/PauserRegistry.sol"; @@ -313,6 +314,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { } if (address(eigenPodBeacon) == address(0)) eigenPodBeacon = new UpgradeableBeacon(address(emptyContract)); if (address(strategyBeacon) == address(0)) strategyBeacon = new UpgradeableBeacon(address(emptyContract)); + if (address(durationVaultBeacon) == address(0)) durationVaultBeacon = new UpgradeableBeacon(address(emptyContract)); // multichain if (address(keyRegistrar) == address(0)) { keyRegistrar = KeyRegistrar(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); @@ -370,6 +372,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { // Beacon implementations eigenPodImplementation = new EigenPod(DEPOSIT_CONTRACT, eigenPodManager); baseStrategyImplementation = new StrategyBase(strategyManager, eigenLayerPauserReg); + durationVaultImplementation = new DurationVaultStrategy(strategyManager, eigenLayerPauserReg, delegationManager, allocationManager); // Pre-longtail StrategyBaseTVLLimits implementation // TODO - need to update ExistingDeploymentParser @@ -422,6 +425,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { // StrategyBase Beacon strategyBeacon.upgradeTo(address(baseStrategyImplementation)); + durationVaultBeacon.upgradeTo(address(durationVaultImplementation)); // Upgrade All deployed strategy contracts to new base strategy for (uint i = 0; i < numStrategiesDeployed; i++) { @@ -456,6 +460,18 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { allocationManager.initialize({initialPausedStatus: 0}); strategyFactory.initialize({_initialOwner: executorMultisig, _initialPausedStatus: 0, _strategyBeacon: strategyBeacon}); + + rewardsCoordinator.initialize({ + initialOwner: executorMultisig, + initialPausedStatus: 0, + _rewardsUpdater: executorMultisig, + _activationDelay: 0, + _defaultSplitBips: 5000 + }); + + cheats.startPrank(executorMultisig); + strategyFactory.setDurationVaultBeacon(durationVaultBeacon); + cheats.stopPrank(); } /// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist diff --git a/src/test/integration/tests/DurationVaultIntegration.t.sol b/src/test/integration/tests/DurationVaultIntegration.t.sol new file mode 100644 index 0000000000..56a2a2d442 --- /dev/null +++ b/src/test/integration/tests/DurationVaultIntegration.t.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + +import "src/contracts/interfaces/IDurationVaultStrategy.sol"; +import "src/contracts/interfaces/IStrategy.sol"; +import "src/contracts/interfaces/IDelegationManager.sol"; +import "src/contracts/interfaces/IAllocationManager.sol"; +import "src/contracts/interfaces/IRewardsCoordinator.sol"; +import "src/contracts/strategies/StrategyBaseTVLLimits.sol"; + +import "src/test/integration/IntegrationChecks.t.sol"; +import "src/test/integration/users/User.t.sol"; +import "src/test/integration/users/AVS.t.sol"; + +contract Integration_DurationVault is IntegrationCheckUtils { + struct DurationVaultContext { + IDurationVaultStrategy vault; + ERC20PresetFixedSupply asset; + AVS avs; + OperatorSet operatorSet; + } + + uint32 internal constant DEFAULT_DURATION = 10 days; + uint internal constant VAULT_MAX_PER_DEPOSIT = 200 ether; + uint internal constant VAULT_STAKE_CAP = 1000 ether; + + function test_durationVaultLifecycle_flow_deposit_lock_mature() public { + DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); + User staker = new User("duration-staker"); + + uint depositAmount = 120 ether; + ctx.asset.transfer(address(staker), depositAmount); + + IStrategy[] memory strategies = _durationStrategyArray(ctx.vault); + uint[] memory tokenBalances = _singleAmountArray(depositAmount); + uint[] memory depositShares = _calculateExpectedShares(strategies, tokenBalances); + + staker.depositIntoEigenlayer(strategies, tokenBalances); + check_Deposit_State(staker, strategies, depositShares); + + _delegateToVault(staker, ctx.vault); + ctx.vault.lock(); + assertTrue(ctx.vault.allocationsActive(), "allocations should be active after lock"); + assertTrue(allocationManager.isOperatorSlashable(address(ctx.vault), ctx.operatorSet), "should be slashable"); + + // Cannot deposit once locked. + uint extraDeposit = 10 ether; + User lateStaker = new User("duration-late-staker"); + ctx.asset.transfer(address(lateStaker), extraDeposit); + uint[] memory lateTokenBalances = _singleAmountArray(extraDeposit); + cheats.expectRevert(IDurationVaultStrategy.DepositsLocked.selector); + lateStaker.depositIntoEigenlayer(strategies, lateTokenBalances); + + // Queue withdrawal prior to maturity. + uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies); + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, withdrawableShares); + + // Mature the vault and allow withdrawals. + cheats.warp(block.timestamp + ctx.vault.duration() + 1); + ctx.vault.markMatured(); + assertTrue(ctx.vault.withdrawalsOpen(), "withdrawals must open after maturity"); + assertFalse(ctx.vault.allocationsActive(), "allocations disabled after maturity"); + + // Advance past deallocation delay to ensure slashability cleared. + cheats.roll(block.number + allocationManager.DEALLOCATION_DELAY() + 1); + assertFalse(allocationManager.isOperatorSlashable(address(ctx.vault), ctx.operatorSet), "should not be slashable"); + + _rollBlocksForCompleteWithdrawals(withdrawals); + IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[0]); + assertEq(address(tokens[0]), address(ctx.asset), "unexpected token returned"); + assertEq(ctx.asset.balanceOf(address(staker)), depositAmount, "staker should recover deposit"); + } + + function test_durationVault_operatorIntegrationAndMetadataUpdate() public { + DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); + + assertTrue(delegationManager.isOperator(address(ctx.vault)), "vault must self-register as operator"); + assertEq(delegationManager.delegatedTo(address(ctx.vault)), address(ctx.vault), "vault should self delegate"); + assertTrue(ctx.vault.operatorSetRegistered(), "operator set should be marked registered"); + + (address avsAddress, uint32 operatorSetId) = ctx.vault.operatorSetInfo(); + assertEq(avsAddress, ctx.operatorSet.avs, "avs mismatch"); + assertEq(operatorSetId, ctx.operatorSet.id, "operator set id mismatch"); + assertTrue(allocationManager.isOperatorSlashable(address(ctx.vault), ctx.operatorSet), "vault must be slashable"); + + string memory newURI = "ipfs://duration-vault/metadata"; + cheats.expectEmit(false, false, false, true, address(ctx.vault)); + emit IDurationVaultStrategy.MetadataURIUpdated(newURI); + ctx.vault.updateMetadataURI(newURI); + assertEq(ctx.vault.metadataURI(), newURI, "metadata not updated"); + } + + function test_durationVault_TVLLimits_enforced_and_frozen_after_lock() public { + DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); + User staker = new User("duration-tvl-staker"); + + (uint maxPerDepositBefore, uint maxStakeBefore) = StrategyBaseTVLLimits(address(ctx.vault)).getTVLLimits(); + assertEq(maxPerDepositBefore, VAULT_MAX_PER_DEPOSIT, "initial max per deposit mismatch"); + assertEq(maxStakeBefore, VAULT_STAKE_CAP, "initial stake cap mismatch"); + + uint newMaxPerDeposit = 50 ether; + uint newStakeCap = 100 ether; + ctx.vault.updateTVLLimits(newMaxPerDeposit, newStakeCap); + + IStrategy[] memory strategies = _durationStrategyArray(ctx.vault); + uint[] memory amounts = _singleAmountArray(60 ether); + ctx.asset.transfer(address(staker), amounts[0]); + cheats.expectRevert(IStrategyErrors.MaxPerDepositExceedsMax.selector); + staker.depositIntoEigenlayer(strategies, amounts); + + // Deposit within limits. + uint firstDeposit = 50 ether; + ctx.asset.transfer(address(staker), firstDeposit); + amounts[0] = firstDeposit; + staker.depositIntoEigenlayer(strategies, amounts); + + uint secondDeposit = 40 ether; + ctx.asset.transfer(address(staker), secondDeposit); + amounts[0] = secondDeposit; + staker.depositIntoEigenlayer(strategies, amounts); + + // Hitting total cap reverts. + uint thirdDeposit = 20 ether; + ctx.asset.transfer(address(staker), thirdDeposit); + amounts[0] = thirdDeposit; + cheats.expectRevert(IStrategyErrors.BalanceExceedsMaxTotalDeposits.selector); + staker.depositIntoEigenlayer(strategies, amounts); + + ctx.vault.lock(); + cheats.expectRevert(IDurationVaultStrategy.DepositsLocked.selector); + ctx.vault.updateTVLLimits(10 ether, 20 ether); + } + + function test_durationVault_rewards_claim_while_locked() public { + DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); + User staker = new User("duration-reward-staker"); + uint depositAmount = 80 ether; + ctx.asset.transfer(address(staker), depositAmount); + + IStrategy[] memory strategies = _durationStrategyArray(ctx.vault); + uint[] memory tokenBalances = _singleAmountArray(depositAmount); + staker.depositIntoEigenlayer(strategies, tokenBalances); + _delegateToVault(staker, ctx.vault); + ctx.vault.lock(); + + // Prepare reward token and fund RewardsCoordinator. + ERC20PresetFixedSupply rewardToken = new ERC20PresetFixedSupply("RewardToken", "RWRD", 1e24, address(this)); + uint rewardAmount = 25 ether; + rewardToken.transfer(address(rewardsCoordinator), rewardAmount); + + // Allow this test to submit roots. + cheats.startPrank(executorMultisig); + rewardsCoordinator.setRewardsUpdater(address(this)); + cheats.stopPrank(); + + // Build single-earner, single-token merkle claim. + (IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim, bytes32 rootHash) = + _buildSingleLeafClaim(address(staker), rewardToken, rewardAmount); + + rewardsCoordinator.submitRoot(rootHash, uint32(block.timestamp - 1)); + claim.rootIndex = uint32(rewardsCoordinator.getDistributionRootsLength() - 1); + + cheats.prank(address(staker)); + rewardsCoordinator.processClaim(claim, address(staker)); + assertEq(rewardToken.balanceOf(address(staker)), rewardAmount, "staker failed to claim rewards"); + } + + function test_durationVault_slashing_routes_to_insurance_and_blocks_after_maturity() public { + address insuranceRecipient = _randomInsuranceRecipient(); + DurationVaultContext memory ctx = _deployDurationVault(insuranceRecipient); + User staker = new User("duration-slash-staker"); + uint depositAmount = 200 ether; + ctx.asset.transfer(address(staker), depositAmount); + + IStrategy[] memory strategies = _durationStrategyArray(ctx.vault); + uint[] memory tokenBalances = _singleAmountArray(depositAmount); + staker.depositIntoEigenlayer(strategies, tokenBalances); + _delegateToVault(staker, ctx.vault); + ctx.vault.lock(); + + uint slashWad = 0.25e18; + IAllocationManager.SlashingParams memory slashParams; + slashParams.operator = address(ctx.vault); + slashParams.operatorSetId = ctx.operatorSet.id; + slashParams.strategies = strategies; + slashParams.wadsToSlash = _singleAmountArray(slashWad); + slashParams.description = "insurance event"; + + (uint slashId,) = ctx.avs.slashOperator(slashParams); + uint redistributed = + strategyManager.clearBurnOrRedistributableSharesByStrategy(ctx.operatorSet, slashId, IStrategy(address(ctx.vault))); + uint expectedRedistribution = (depositAmount * slashWad) / 1e18; + assertEq(redistributed, expectedRedistribution, "unexpected redistribution amount"); + assertEq(ctx.asset.balanceOf(insuranceRecipient), expectedRedistribution, "insurance recipient not paid"); + + // Mature the vault and advance beyond slashable window. + cheats.warp(block.timestamp + ctx.vault.duration() + 1); + ctx.vault.markMatured(); + cheats.roll(block.number + allocationManager.DEALLOCATION_DELAY() + 2); + + cheats.expectRevert(IAllocationManagerErrors.OperatorNotSlashable.selector); + ctx.avs.slashOperator(slashParams); + assertEq(ctx.asset.balanceOf(insuranceRecipient), expectedRedistribution, "post-maturity slash should not pay"); + } + + function test_durationVault_slashing_affectsQueuedWithdrawalsAndPaysInsurance() public { + address insuranceRecipient = _randomInsuranceRecipient(); + DurationVaultContext memory ctx = _deployDurationVault(insuranceRecipient); + User staker = new User("duration-slash-queued"); + uint depositAmount = 180 ether; + ctx.asset.transfer(address(staker), depositAmount); + + IStrategy[] memory strategies = _durationStrategyArray(ctx.vault); + uint[] memory tokenBalances = _singleAmountArray(depositAmount); + staker.depositIntoEigenlayer(strategies, tokenBalances); + _delegateToVault(staker, ctx.vault); + ctx.vault.lock(); + + uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies); + Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, withdrawableShares); + + uint slashWad = 0.3e18; + IAllocationManager.SlashingParams memory slashParams; + slashParams.operator = address(ctx.vault); + slashParams.operatorSetId = ctx.operatorSet.id; + slashParams.strategies = strategies; + slashParams.wadsToSlash = _singleAmountArray(slashWad); + slashParams.description = "queued withdrawal slash"; + + (uint slashId,) = ctx.avs.slashOperator(slashParams); + uint redistributed = + strategyManager.clearBurnOrRedistributableSharesByStrategy(ctx.operatorSet, slashId, IStrategy(address(ctx.vault))); + uint expectedRedistribution = (depositAmount * slashWad) / 1e18; + assertEq(redistributed, expectedRedistribution, "queued slash redistribution mismatch"); + assertEq(ctx.asset.balanceOf(insuranceRecipient), expectedRedistribution, "insurance recipient incorrect"); + + _rollBlocksForCompleteWithdrawals(withdrawals); + cheats.expectRevert(IDurationVaultStrategy.WithdrawalsLocked.selector); + staker.completeWithdrawalAsTokens(withdrawals[0]); + + cheats.warp(block.timestamp + ctx.vault.duration() + 1); + ctx.vault.markMatured(); + cheats.roll(block.number + allocationManager.DEALLOCATION_DELAY() + 2); + _rollBlocksForCompleteWithdrawals(withdrawals); + + IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawals[0]); + assertEq(address(tokens[0]), address(ctx.asset), "unexpected withdrawal token"); + assertEq( + ctx.asset.balanceOf(address(staker)), depositAmount - expectedRedistribution, "staker balance after queued slash incorrect" + ); + } + + function _deployDurationVault(address insuranceRecipient) internal returns (DurationVaultContext memory ctx) { + ERC20PresetFixedSupply asset = new ERC20PresetFixedSupply("Duration Asset", "DURA", 1e24, address(this)); + AVS avsInstance = new AVS("duration-avs"); + avsInstance.updateAVSMetadataURI("https://avs-metadata.local"); + + avsInstance.createOperatorSet(new IStrategy[](0)); + OperatorSet memory opSet = avsInstance.createRedistributingOperatorSet(new IStrategy[](0), insuranceRecipient); + + IDurationVaultStrategy.VaultConfig memory config; + config.underlyingToken = asset; + config.vaultAdmin = address(this); + config.duration = DEFAULT_DURATION; + config.maxPerDeposit = VAULT_MAX_PER_DEPOSIT; + config.stakeCap = VAULT_STAKE_CAP; + config.metadataURI = "ipfs://duration-vault"; + config.operatorSet = opSet; + config.operatorSetRegistrationData = ""; + config.delegationApprover = address(0); + config.operatorAllocationDelay = 0; + config.operatorMetadataURI = "ipfs://duration-vault/operator"; + + IDurationVaultStrategy vault = IDurationVaultStrategy(address(strategyFactory.deployDurationVaultStrategy(config))); + + IStrategy[] memory strategies = _durationStrategyArray(vault); + avsInstance.addStrategiesToOperatorSet(opSet.id, strategies); + + ctx = DurationVaultContext({vault: vault, asset: asset, avs: avsInstance, operatorSet: opSet}); + } + + function _durationStrategyArray(IDurationVaultStrategy vault) internal pure returns (IStrategy[] memory arr) { + arr = new IStrategy[](1); + arr[0] = IStrategy(address(vault)); + } + + function _singleAmountArray(uint amount) internal pure returns (uint[] memory arr) { + arr = new uint[](1); + arr[0] = amount; + } + + function _delegateToVault(User staker, IDurationVaultStrategy vault) internal { + IDelegationManager.SignatureWithExpiry memory emptySig; + cheats.startPrank(address(staker)); + delegationManager.delegateTo(address(vault), emptySig, bytes32(0)); + cheats.stopPrank(); + assertEq(delegationManager.delegatedTo(address(staker)), address(vault), "delegation failed"); + } + + function _randomInsuranceRecipient() internal view returns (address) { + return address(uint160(uint(keccak256(abi.encodePacked(block.timestamp, address(this)))))); + } + + function _buildSingleLeafClaim(address earner, IERC20 token, uint amount) + internal + pure + returns (IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim, bytes32 rootHash) + { + IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] memory tokenLeaves = new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](1); + tokenLeaves[0] = IRewardsCoordinatorTypes.TokenTreeMerkleLeaf({token: token, cumulativeEarnings: amount}); + bytes32 tokenLeafHash = keccak256(abi.encodePacked(uint8(1), address(token), amount)); + IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf memory earnerLeaf = + IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf({earner: earner, earnerTokenRoot: tokenLeafHash}); + rootHash = keccak256(abi.encodePacked(uint8(0), earner, tokenLeafHash)); + + uint32[] memory tokenIndices = new uint32[](1); + tokenIndices[0] = 0; + bytes[] memory tokenProofs = new bytes[](1); + tokenProofs[0] = bytes(""); + + claim = IRewardsCoordinatorTypes.RewardsMerkleClaim({ + rootIndex: 0, + earnerIndex: 0, + earnerTreeProof: bytes(""), + earnerLeaf: earnerLeaf, + tokenIndices: tokenIndices, + tokenTreeProofs: tokenProofs, + tokenLeaves: tokenLeaves + }); + } +} + diff --git a/src/test/integration/users/AVS.t.sol b/src/test/integration/users/AVS.t.sol index 115bfb9a96..122bafb798 100644 --- a/src/test/integration/users/AVS.t.sol +++ b/src/test/integration/users/AVS.t.sol @@ -210,7 +210,7 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { "slashOperator", string.concat( "{operator: ", - User(payable(params.operator)).NAME_COLORED(), + _formatActor(params.operator), ", operatorSetId: ", cheats.toString(params.operatorSetId), ", strategy: ", @@ -246,7 +246,7 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { "slashOperator", string.concat( "{operator: ", - User(payable(params.operator)).NAME_COLORED(), + _formatActor(params.operator), ", operatorSetId: ", cheats.toString(params.operatorSetId), ", strategy: ", @@ -285,7 +285,7 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { "slashOperator", string.concat( "{operator: ", - operator.NAME_COLORED(), + _formatActor(address(operator)), ", operatorSetId: ", cheats.toString(operatorSetId), ", strategy: ", @@ -412,4 +412,13 @@ contract AVS is Logger, IAllocationManagerTypes, IAVSRegistrar { function _tryPrankAppointee_AllocationManager(bytes4 selector) internal { return _tryPrankAppointee(address(allocationManager), selector); } + + function _formatActor(address actor) internal view returns (string memory) { + if (actor == address(0)) return "address(0)"; + try Logger(actor).NAME_COLORED() returns (string memory colored) { + return colored; + } catch { + return cheats.toString(actor); + } + } } From 4a422b1f53b5bbe08f616b48d71cbbe29a502b5c Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 19:18:14 -0500 Subject: [PATCH 14/18] chore: remove junk --- script/tasks/build_nonsigner_proof.s.sol | 46 ------ script/tasks/generate_bn254_cert.s.sol | 170 ----------------------- 2 files changed, 216 deletions(-) delete mode 100644 script/tasks/build_nonsigner_proof.s.sol delete mode 100644 script/tasks/generate_bn254_cert.s.sol diff --git a/script/tasks/build_nonsigner_proof.s.sol b/script/tasks/build_nonsigner_proof.s.sol deleted file mode 100644 index c0f7778500..0000000000 --- a/script/tasks/build_nonsigner_proof.s.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {Script} from "forge-std/Script.sol"; -import {console2} from "forge-std/console2.sol"; - -import {BN254} from "src/contracts/libraries/BN254.sol"; -import {Merkle} from "src/contracts/libraries/Merkle.sol"; -import {LeafCalculatorMixin} from "src/contracts/mixins/LeafCalculatorMixin.sol"; -import {IOperatorTableCalculatorTypes as Types} from "src/contracts/interfaces/IOperatorTableCalculator.sol"; - -contract BuildNonsignerProof is Script, LeafCalculatorMixin { - using BN254 for *; - - // Build a keccak Merkle proof for a 2-operator tree (index 0 or 1) - // Inputs are operator G1 pubkeys and weight arrays - function runTwoPacked( - uint256[2] memory op1XY, - uint256[] memory w1, - uint256[2] memory op2XY, - uint256[] memory w2, - uint32 indexToProve - ) external pure returns (bytes memory proof, bytes32 root, bytes32 leaf) { - Types.BN254OperatorInfo[] memory ops = new Types.BN254OperatorInfo[](2); - ops[0].pubkey = BN254.G1Point(op1XY[0], op1XY[1]); - ops[0].weights = w1; - ops[1].pubkey = BN254.G1Point(op2XY[0], op2XY[1]); - ops[1].weights = w2; - - bytes32[] memory leaves = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - leaves[i] = calculateOperatorInfoLeaf(ops[i]); - } - - root = Merkle.merkleizeKeccak(leaves); - proof = Merkle.getProofKeccak(leaves, indexToProve); - leaf = leaves[indexToProve]; - - console2.log("root"); - console2.logBytes32(root); - console2.log("leaf"); - console2.logBytes32(leaf); - console2.log("proof bytes len"); - console2.logUint(proof.length); - } -} diff --git a/script/tasks/generate_bn254_cert.s.sol b/script/tasks/generate_bn254_cert.s.sol deleted file mode 100644 index 3813ed39db..0000000000 --- a/script/tasks/generate_bn254_cert.s.sol +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {Script} from "forge-std/Script.sol"; -import {console2} from "forge-std/console2.sol"; - -import {BN254} from "src/contracts/libraries/BN254.sol"; -import {OperatorSet} from "src/contracts/libraries/OperatorSetLib.sol"; -import {IBN254CertificateVerifier} from "src/contracts/interfaces/IBN254CertificateVerifier.sol"; -import {IKeyRegistrar} from "src/contracts/interfaces/IKeyRegistrar.sol"; - -// Test utilities (FFI-based G2 mul and add) -import {BN256G2} from "src/test/utils/BN256G2.sol"; - -contract GenerateBN254Cert is Script { - using BN254 for BN254.G1Point; - - function _g2Mul( - uint256 sk - ) internal returns (BN254.G2Point memory g2) { - // Multiply G2 generator by private key using FFI helper - // BN256G2 has no mul API; reuse OperatorWalletLib.mul logic via BN256G2 + ffi helper - // We reconstruct the same flow inline here to avoid importing the VM cheat directly. - // BN256G2.ECTwistMul is not available, so we rely on the go helper invoked by BN256G2 via ffi - // However BN256G2 library exposes only add; we keep the FFI path present in src/test/utils/BN256G2.go via forge --ffi. - - // We call the same go helper that tests use, via vm.ffi under the hood. - // Here we replicate it by invoking the cheatcode address directly. - address HEVM = address(uint160(uint256(keccak256("hevm cheat code")))); - bytes memory out; - - // x1 (index 1) - { - bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "1")); - (bool ok, bytes memory res) = HEVM.call(input); - require(ok, "ffi x1 failed"); - out = res; - g2.X[1] = abi.decode(out, (uint256)); - } - // x0 (index 2) - { - bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "2")); - (bool ok, bytes memory res) = HEVM.call(input); - require(ok, "ffi x0 failed"); - out = res; - g2.X[0] = abi.decode(out, (uint256)); - } - // y1 (index 3) - { - bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "3")); - (bool ok, bytes memory res) = HEVM.call(input); - require(ok, "ffi y1 failed"); - out = res; - g2.Y[1] = abi.decode(out, (uint256)); - } - // y0 (index 4) - { - bytes memory input = abi.encodeWithSignature("ffi(string[])", _ffiArgs(sk, "4")); - (bool ok, bytes memory res) = HEVM.call(input); - require(ok, "ffi y0 failed"); - out = res; - g2.Y[0] = abi.decode(out, (uint256)); - } - } - - function _ffiArgs( - uint256 sk, - string memory which - ) internal pure returns (string[] memory args) { - args = new string[](5); - args[0] = "go"; - args[1] = "run"; - args[2] = "src/test/utils/g2mul.go"; - args[3] = vm.toString(sk); - args[4] = which; - } - - // Single-signer helper: gets apk from KeyRegistrar, no FFI, no broadcasts - function runSingle( - address bn254Verifier, - address keyRegistrar, - address avs, - uint32 setId, - address operator, - uint32 referenceTimestamp, - bytes32 messageHash, - uint256 signerSk - ) external view returns (bytes32 digest, BN254.G1Point memory sigAgg, BN254.G2Point memory apkAgg) { - digest = IBN254CertificateVerifier(bn254Verifier).calculateCertificateDigest(referenceTimestamp, messageHash); - - BN254.G1Point memory h = BN254.hashToG1(digest); - sigAgg = h.scalar_mul(signerSk); - - (, BN254.G2Point memory pkG2) = IKeyRegistrar(keyRegistrar).getBN254Key(OperatorSet(avs, setId), operator); - apkAgg = pkG2; - - console2.log("digest"); - console2.logBytes32(digest); - console2.log("sigAgg.X"); - console2.logUint(sigAgg.X); - console2.log("sigAgg.Y"); - console2.logUint(sigAgg.Y); - console2.log("apk.X0"); - console2.logUint(apkAgg.X[0]); - console2.log("apk.X1"); - console2.logUint(apkAgg.X[1]); - console2.log("apk.Y0"); - console2.logUint(apkAgg.Y[0]); - console2.log("apk.Y1"); - console2.logUint(apkAgg.Y[1]); - } - - function run( - address bn254Verifier, - uint32 referenceTimestamp, - bytes32 messageHash, - uint256 signerSk1, - uint256 signerSk2 - ) external returns (bytes32 digest, BN254.G1Point memory sigAgg, BN254.G2Point memory apkAgg) { - digest = IBN254CertificateVerifier(bn254Verifier).calculateCertificateDigest(referenceTimestamp, messageHash); - - BN254.G1Point memory h = BN254.hashToG1(digest); - BN254.G1Point memory sig1 = h.scalar_mul(signerSk1); - BN254.G1Point memory sig2 = h.scalar_mul(signerSk2); - sigAgg = sig1.plus(sig2); - - BN254.G2Point memory pk1 = _g2Mul(signerSk1); - BN254.G2Point memory pk2 = _g2Mul(signerSk2); - // Aggregate in G2: apk = pk1 + pk2 - (apkAgg.X[0], apkAgg.X[1], apkAgg.Y[0], apkAgg.Y[1]) = - BN256G2.ECTwistAdd(pk1.X[0], pk1.X[1], pk1.Y[0], pk1.Y[1], pk2.X[0], pk2.X[1], pk2.Y[0], pk2.Y[1]); - - console2.log("digest"); - console2.logBytes32(digest); - console2.log("sigAgg.X"); - console2.logUint(sigAgg.X); - console2.log("sigAgg.Y"); - console2.logUint(sigAgg.Y); - console2.log("apk.X0"); - console2.logUint(apkAgg.X[0]); - console2.log("apk.X1"); - console2.logUint(apkAgg.X[1]); - console2.log("apk.Y0"); - console2.logUint(apkAgg.Y[0]); - console2.log("apk.Y1"); - console2.logUint(apkAgg.Y[1]); - } - - // Fully offline: compute digest locally and accept G2 pubkey directly. - function runRaw( - uint32 referenceTimestamp, - bytes32 messageHash, - uint256 signerSk, - uint256 apkX0, - uint256 apkX1, - uint256 apkY0, - uint256 apkY1 - ) external view returns (bytes32 digest, BN254.G1Point memory sig, BN254.G2Point memory apk) { - bytes32 TYPEHASH = keccak256("BN254Certificate(uint32 referenceTimestamp,bytes32 messageHash)"); - digest = keccak256(abi.encode(TYPEHASH, referenceTimestamp, messageHash)); - - BN254.G1Point memory h = BN254.hashToG1(digest); - sig = h.scalar_mul(signerSk); - - apk.X[0] = apkX0; - apk.X[1] = apkX1; - apk.Y[0] = apkY0; - apk.Y[1] = apkY1; - } -} From a10cab3fa0803b73a074dd00ee658e4a30da0e01 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 19:40:39 -0500 Subject: [PATCH 15/18] test: fork test fix --- src/test/integration/IntegrationDeployer.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index d94868f54f..c91728eb32 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -276,6 +276,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { _deployProxies(); // deploy proxies (if undeployed) _deployImplementations(); _upgradeProxies(); + strategyFactory.setDurationVaultBeacon(durationVaultBeacon); cheats.stopPrank(); } From 478eb77735863f7378407b22fef91f4ad3ba975e Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 19:57:47 -0500 Subject: [PATCH 16/18] test: fork test fix 2 --- script/deploy/devnet/deploy_from_scratch.s.sol | 3 ++- script/releases/TestUtils.sol | 2 +- src/contracts/strategies/StrategyFactory.sol | 6 +++++- src/test/integration/IntegrationDeployer.t.sol | 12 ++++++------ src/test/unit/StrategyFactoryUnit.t.sol | 9 +++++---- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/script/deploy/devnet/deploy_from_scratch.s.sol b/script/deploy/devnet/deploy_from_scratch.s.sol index 6c646de7e2..c34dd7845e 100644 --- a/script/deploy/devnet/deploy_from_scratch.s.sol +++ b/script/deploy/devnet/deploy_from_scratch.s.sol @@ -344,7 +344,8 @@ contract DeployFromScratch is Script, Test { StrategyFactory.initialize.selector, executorMultisig, 0, // initial paused status - IBeacon(strategyBeacon) + IBeacon(strategyBeacon), + IBeacon(address(0)) ) ); diff --git a/script/releases/TestUtils.sol b/script/releases/TestUtils.sol index b8335406e9..3d287f2879 100644 --- a/script/releases/TestUtils.sol +++ b/script/releases/TestUtils.sol @@ -967,7 +967,7 @@ library TestUtils { StrategyFactory strategyFactory ) internal { vm.expectRevert(errInit); - strategyFactory.initialize(address(0), 0, UpgradeableBeacon(address(0))); + strategyFactory.initialize(address(0), 0, UpgradeableBeacon(address(0)), UpgradeableBeacon(address(0))); } /// multichain/ diff --git a/src/contracts/strategies/StrategyFactory.sol b/src/contracts/strategies/StrategyFactory.sol index 3ad3e4daab..8bae750e3a 100644 --- a/src/contracts/strategies/StrategyFactory.sol +++ b/src/contracts/strategies/StrategyFactory.sol @@ -32,11 +32,15 @@ contract StrategyFactory is StrategyFactoryStorage, OwnableUpgradeable, Pausable function initialize( address _initialOwner, uint256 _initialPausedStatus, - IBeacon _strategyBeacon + IBeacon _strategyBeacon, + IBeacon _durationVaultBeacon ) public virtual initializer { _transferOwnership(_initialOwner); _setPausedStatus(_initialPausedStatus); _setStrategyBeacon(_strategyBeacon); + if (address(_durationVaultBeacon) != address(0)) { + _setDurationVaultBeacon(_durationVaultBeacon); + } } /// @notice Deploy a new StrategyBase contract for the ERC20 token, using a beacon proxy diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index c91728eb32..343520b1a4 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -276,7 +276,6 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { _deployProxies(); // deploy proxies (if undeployed) _deployImplementations(); _upgradeProxies(); - strategyFactory.setDurationVaultBeacon(durationVaultBeacon); cheats.stopPrank(); } @@ -460,7 +459,12 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { allocationManager.initialize({initialPausedStatus: 0}); - strategyFactory.initialize({_initialOwner: executorMultisig, _initialPausedStatus: 0, _strategyBeacon: strategyBeacon}); + strategyFactory.initialize({ + _initialOwner: executorMultisig, + _initialPausedStatus: 0, + _strategyBeacon: strategyBeacon, + _durationVaultBeacon: durationVaultBeacon + }); rewardsCoordinator.initialize({ initialOwner: executorMultisig, @@ -469,10 +473,6 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser { _activationDelay: 0, _defaultSplitBips: 5000 }); - - cheats.startPrank(executorMultisig); - strategyFactory.setDurationVaultBeacon(durationVaultBeacon); - cheats.stopPrank(); } /// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist diff --git a/src/test/unit/StrategyFactoryUnit.t.sol b/src/test/unit/StrategyFactoryUnit.t.sol index b72277413a..922afdee01 100644 --- a/src/test/unit/StrategyFactoryUnit.t.sol +++ b/src/test/unit/StrategyFactoryUnit.t.sol @@ -78,12 +78,12 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { new TransparentUpgradeableProxy( address(strategyFactoryImplementation), address(eigenLayerProxyAdmin), - abi.encodeWithSelector(StrategyFactory.initialize.selector, initialOwner, initialPausedStatus, strategyBeacon) + abi.encodeWithSelector( + StrategyFactory.initialize.selector, initialOwner, initialPausedStatus, strategyBeacon, durationVaultBeacon + ) ) ) ); - - strategyFactory.setDurationVaultBeacon(durationVaultBeacon); } function test_initialization() public view { @@ -117,7 +117,8 @@ contract StrategyFactoryUnitTests is EigenLayerUnitTestSetup { strategyFactory.initialize({ _initialOwner: initialOwner, _initialPausedStatus: initialPausedStatus, - _strategyBeacon: strategyBeacon + _strategyBeacon: strategyBeacon, + _durationVaultBeacon: durationVaultBeacon }); } From 9ea4b2db653187a5e4924fba559632def89f8a85 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 20:09:29 -0500 Subject: [PATCH 17/18] chore: fix fork 3 --- .../tests/DurationVaultIntegration.t.sol | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/test/integration/tests/DurationVaultIntegration.t.sol b/src/test/integration/tests/DurationVaultIntegration.t.sol index 56a2a2d442..62ac7f8733 100644 --- a/src/test/integration/tests/DurationVaultIntegration.t.sol +++ b/src/test/integration/tests/DurationVaultIntegration.t.sol @@ -27,7 +27,22 @@ contract Integration_DurationVault is IntegrationCheckUtils { uint internal constant VAULT_MAX_PER_DEPOSIT = 200 ether; uint internal constant VAULT_STAKE_CAP = 1000 ether; - function test_durationVaultLifecycle_flow_deposit_lock_mature() public { + bool internal durationVaultSupported; + + modifier vaultSupported() { + if (!durationVaultSupported) { + console.log("DurationVaultIntegration: duration vault beacon not configured, skipping"); + return; + } + _; + } + + function setUp() public virtual override { + super.setUp(); + durationVaultSupported = address(strategyFactory.durationVaultBeacon()) != address(0); + } + + function test_durationVaultLifecycle_flow_deposit_lock_mature() public vaultSupported { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); User staker = new User("duration-staker"); @@ -74,7 +89,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.asset.balanceOf(address(staker)), depositAmount, "staker should recover deposit"); } - function test_durationVault_operatorIntegrationAndMetadataUpdate() public { + function test_durationVault_operatorIntegrationAndMetadataUpdate() public vaultSupported { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); assertTrue(delegationManager.isOperator(address(ctx.vault)), "vault must self-register as operator"); @@ -93,7 +108,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.vault.metadataURI(), newURI, "metadata not updated"); } - function test_durationVault_TVLLimits_enforced_and_frozen_after_lock() public { + function test_durationVault_TVLLimits_enforced_and_frozen_after_lock() public vaultSupported { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); User staker = new User("duration-tvl-staker"); @@ -134,7 +149,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { ctx.vault.updateTVLLimits(10 ether, 20 ether); } - function test_durationVault_rewards_claim_while_locked() public { + function test_durationVault_rewards_claim_while_locked() public vaultSupported { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); User staker = new User("duration-reward-staker"); uint depositAmount = 80 ether; @@ -168,7 +183,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(rewardToken.balanceOf(address(staker)), rewardAmount, "staker failed to claim rewards"); } - function test_durationVault_slashing_routes_to_insurance_and_blocks_after_maturity() public { + function test_durationVault_slashing_routes_to_insurance_and_blocks_after_maturity() public vaultSupported { address insuranceRecipient = _randomInsuranceRecipient(); DurationVaultContext memory ctx = _deployDurationVault(insuranceRecipient); User staker = new User("duration-slash-staker"); @@ -206,7 +221,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.asset.balanceOf(insuranceRecipient), expectedRedistribution, "post-maturity slash should not pay"); } - function test_durationVault_slashing_affectsQueuedWithdrawalsAndPaysInsurance() public { + function test_durationVault_slashing_affectsQueuedWithdrawalsAndPaysInsurance() public vaultSupported { address insuranceRecipient = _randomInsuranceRecipient(); DurationVaultContext memory ctx = _deployDurationVault(insuranceRecipient); User staker = new User("duration-slash-queued"); From c7497d3d799101661b384be0030a2c43491c5b7e Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 4 Dec 2025 20:14:26 -0500 Subject: [PATCH 18/18] chore: revert --- .../tests/DurationVaultIntegration.t.sol | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/test/integration/tests/DurationVaultIntegration.t.sol b/src/test/integration/tests/DurationVaultIntegration.t.sol index 62ac7f8733..56a2a2d442 100644 --- a/src/test/integration/tests/DurationVaultIntegration.t.sol +++ b/src/test/integration/tests/DurationVaultIntegration.t.sol @@ -27,22 +27,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { uint internal constant VAULT_MAX_PER_DEPOSIT = 200 ether; uint internal constant VAULT_STAKE_CAP = 1000 ether; - bool internal durationVaultSupported; - - modifier vaultSupported() { - if (!durationVaultSupported) { - console.log("DurationVaultIntegration: duration vault beacon not configured, skipping"); - return; - } - _; - } - - function setUp() public virtual override { - super.setUp(); - durationVaultSupported = address(strategyFactory.durationVaultBeacon()) != address(0); - } - - function test_durationVaultLifecycle_flow_deposit_lock_mature() public vaultSupported { + function test_durationVaultLifecycle_flow_deposit_lock_mature() public { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); User staker = new User("duration-staker"); @@ -89,7 +74,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.asset.balanceOf(address(staker)), depositAmount, "staker should recover deposit"); } - function test_durationVault_operatorIntegrationAndMetadataUpdate() public vaultSupported { + function test_durationVault_operatorIntegrationAndMetadataUpdate() public { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); assertTrue(delegationManager.isOperator(address(ctx.vault)), "vault must self-register as operator"); @@ -108,7 +93,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.vault.metadataURI(), newURI, "metadata not updated"); } - function test_durationVault_TVLLimits_enforced_and_frozen_after_lock() public vaultSupported { + function test_durationVault_TVLLimits_enforced_and_frozen_after_lock() public { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); User staker = new User("duration-tvl-staker"); @@ -149,7 +134,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { ctx.vault.updateTVLLimits(10 ether, 20 ether); } - function test_durationVault_rewards_claim_while_locked() public vaultSupported { + function test_durationVault_rewards_claim_while_locked() public { DurationVaultContext memory ctx = _deployDurationVault(_randomInsuranceRecipient()); User staker = new User("duration-reward-staker"); uint depositAmount = 80 ether; @@ -183,7 +168,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(rewardToken.balanceOf(address(staker)), rewardAmount, "staker failed to claim rewards"); } - function test_durationVault_slashing_routes_to_insurance_and_blocks_after_maturity() public vaultSupported { + function test_durationVault_slashing_routes_to_insurance_and_blocks_after_maturity() public { address insuranceRecipient = _randomInsuranceRecipient(); DurationVaultContext memory ctx = _deployDurationVault(insuranceRecipient); User staker = new User("duration-slash-staker"); @@ -221,7 +206,7 @@ contract Integration_DurationVault is IntegrationCheckUtils { assertEq(ctx.asset.balanceOf(insuranceRecipient), expectedRedistribution, "post-maturity slash should not pay"); } - function test_durationVault_slashing_affectsQueuedWithdrawalsAndPaysInsurance() public vaultSupported { + function test_durationVault_slashing_affectsQueuedWithdrawalsAndPaysInsurance() public { address insuranceRecipient = _randomInsuranceRecipient(); DurationVaultContext memory ctx = _deployDurationVault(insuranceRecipient); User staker = new User("duration-slash-queued");