diff --git a/src/core/LiquidTokenManager.sol b/src/core/LiquidTokenManager.sol index e712a6c8..0e1299db 100644 --- a/src/core/LiquidTokenManager.sol +++ b/src/core/LiquidTokenManager.sol @@ -20,8 +20,10 @@ import {IStakerNode} from "../interfaces/IStakerNode.sol"; import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; import {ITokenRegistryOracle} from "../interfaces/ITokenRegistryOracle.sol"; import {IWithdrawalManager} from "../interfaces/IWithdrawalManager.sol"; -import {ILSTSwapRouter} from "../interfaces/ILSTSwapRouter.sol"; - +import {LTMValidation} from "../libraries/LTMValidation.sol"; +import {LTMBalances} from "../libraries/LTMBalances.sol"; +import {LTMWithdrawalProcessor} from "../libraries/LTMWithdrawalProcessor.sol"; +import {LTMHelpers} from "../libraries/LTMHelpers.sol"; /// @title LiquidTokenManager /// @notice Manages liquid tokens and their staking to EigenLayer strategies contract LiquidTokenManager is @@ -32,19 +34,20 @@ contract LiquidTokenManager is { using SafeERC20 for IERC20; using Math for uint256; + using LTMValidation for uint256; + using LTMValidation for address; // ------------------------------------------------------------------------------ // State // ------------------------------------------------------------------------------ /// @notice Role identifier for staking operations - bytes32 public constant STRATEGY_CONTROLLER_ROLE = keccak256("STRATEGY_CONTROLLER_ROLE"); + bytes32 public constant STRATEGY_CONTROLLER_ROLE = + keccak256("STRATEGY_CONTROLLER_ROLE"); /// @notice Role identifier for asset price update operations - bytes32 public constant PRICE_UPDATER_ROLE = keccak256("PRICE_UPDATER_ROLE"); - - /// @notice Number of decimal places used for price representation - uint256 public constant PRICE_DECIMALS = 18; + bytes32 public constant PRICE_UPDATER_ROLE = + keccak256("PRICE_UPDATER_ROLE"); /// @notice EigenLayer contracts IStrategyManager public strategyManager; @@ -69,10 +72,6 @@ contract LiquidTokenManager is /// @notice v2 contracts IWithdrawalManager public withdrawalManager; - ILSTSwapRouter public lstSwapRouter; - - /// @notice Constant for ETH address representation - address private constant _ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @notice Total redemptions created uint256 private _redemptionNonce; @@ -91,18 +90,13 @@ contract LiquidTokenManager is __AccessControl_init(); __ReentrancyGuard_init(); - if ( - address(init.strategyManager) == address(0) || - address(init.delegationManager) == address(0) || - address(init.liquidToken) == address(0) || - address(init.initialOwner) == address(0) || - address(init.priceUpdater) == address(0) || - address(init.tokenRegistryOracle) == address(0) || - address(init.withdrawalManager) == address(0) || - address(init.lstSwapRouter) == address(0) - ) { - revert ZeroAddress(); - } + address(init.strategyManager).validateNotZero(); + address(init.delegationManager).validateNotZero(); + address(init.liquidToken).validateNotZero(); + address(init.initialOwner).validateNotZero(); + address(init.priceUpdater).validateNotZero(); + address(init.tokenRegistryOracle).validateNotZero(); + address(init.withdrawalManager).validateNotZero(); _grantRole(DEFAULT_ADMIN_ROLE, init.initialOwner); _grantRole(STRATEGY_CONTROLLER_ROLE, init.strategyController); @@ -114,21 +108,6 @@ contract LiquidTokenManager is delegationManager = init.delegationManager; tokenRegistryOracle = init.tokenRegistryOracle; withdrawalManager = init.withdrawalManager; - lstSwapRouter = init.lstSwapRouter; - } - - // ------------------------------------------------------------------------------ - // Admin functions - // ------------------------------------------------------------------------------ - - /// @inheritdoc ILiquidTokenManager - function updateLSTSwapRouter(address newLstSwapRouter) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (newLstSwapRouter == address(0)) revert ZeroAddress(); - - address oldLsr = address(lstSwapRouter); - lstSwapRouter = ILSTSwapRouter(newLstSwapRouter); - - emit LSTSwapRouterUpdated(oldLsr, newLstSwapRouter, msg.sender); } // ------------------------------------------------------------------------------ @@ -147,20 +126,25 @@ contract LiquidTokenManager is address fallbackSource, bytes4 fallbackFn ) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (address(tokenStrategies[token]) != address(0)) revert TokenExists(address(token)); - if (address(token) == address(0)) revert ZeroAddress(); - if (decimals == 0) revert InvalidDecimals(); - if (volatilityThreshold != 0 && (volatilityThreshold < 1e16 || volatilityThreshold > 1e18)) - revert InvalidThreshold(); - if (address(strategy) == address(0)) revert ZeroAddress(); + if (address(tokenStrategies[token]) != address(0)) + revert LTMValidation.E12(); // TokenExists + address(token).validateNotZero(); + if (decimals == 0) revert LTMValidation.E04(); // InvalidDecimals + if ( + volatilityThreshold != 0 && + (volatilityThreshold < 1e16 || volatilityThreshold > 1e18) + ) revert LTMValidation.E05(); // InvalidThreshold + address(strategy).validateNotZero(); if (address(strategyTokens[strategy]) != address(0)) { - revert StrategyAlreadyAssigned(address(strategy), address(strategyTokens[strategy])); + revert LTMValidation.E13(); // StrategyAlreadyAssigned } // Price source validation and configuration bool isNative = (primaryType == 0 && primarySource == address(0)); - if (!isNative && (primaryType < 1 || primaryType > 3)) revert InvalidPriceSource(); - if (!isNative && primarySource == address(0)) revert InvalidPriceSource(); + if (!isNative && (primaryType < 1 || primaryType > 5)) + revert LTMValidation.E14(); // InvalidPriceSource + if (!isNative && primarySource == address(0)) + revert LTMValidation.E14(); // InvalidPriceSource if (!isNative) { tokenRegistryOracle.configureToken( address(token), @@ -172,14 +156,17 @@ contract LiquidTokenManager is ); } - try IERC20Metadata(address(token)).decimals() returns (uint8 decimalsFromContract) { - if (decimalsFromContract == 0) revert InvalidDecimals(); - if (decimals != decimalsFromContract) revert InvalidDecimals(); + try IERC20Metadata(address(token)).decimals() returns ( + uint8 decimalsFromContract + ) { + if (decimalsFromContract == 0) revert LTMValidation.E04(); // InvalidDecimals + if (decimals != decimalsFromContract) revert LTMValidation.E04(); // InvalidDecimals } catch {} // Fallback to `decimals` if token contract doesn't implement `decimals()` uint256 fetchedPrice; if (!isNative) { - (uint256 price, bool ok) = tokenRegistryOracle._getTokenPrice_getter(address(token)); - if (!ok || price == 0) revert TokenPriceFetchFailed(); + (uint256 price, bool ok) = tokenRegistryOracle + ._getTokenPrice_getter(address(token)); + if (!ok || price == 0) revert LTMValidation.E15(); // TokenPriceFetchFailed fetchedPrice = price; } else { fetchedPrice = 1e18; @@ -194,22 +181,31 @@ contract LiquidTokenManager is strategyTokens[strategy] = token; supportedTokens.push(token); - emit TokenAdded(token, decimals, fetchedPrice, volatilityThreshold, address(strategy), msg.sender); + emit TokenAdded( + token, + decimals, + fetchedPrice, + volatilityThreshold, + address(strategy), + msg.sender + ); } /// @inheritdoc ILiquidTokenManager function removeToken(IERC20 token) external onlyRole(DEFAULT_ADMIN_ROLE) { TokenInfo memory info = tokens[token]; - if (info.decimals == 0) revert TokenNotSupported(token); + if (info.decimals == 0) revert LTMValidation.E07(); // TokenNotSupported IERC20[] memory assets = new IERC20[](1); assets[0] = token; // Check for unstaked balances - if (liquidToken.balanceAssets(assets)[0] > 0) revert TokenInUse(token); + if (liquidToken.balanceAssets(assets)[0] > 0) + revert LTMValidation.E16(); // TokenInUse // Check for pending withdrawal balances - if (liquidToken.balanceQueuedAssets(assets)[0] > 0) revert TokenInUse(token); + if (liquidToken.balanceQueuedAssets(assets)[0] > 0) + revert LTMValidation.E16(); // TokenInUse // Check for staked withdrawable balances IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); @@ -217,9 +213,13 @@ contract LiquidTokenManager is unchecked { for (uint256 i = 0; i < len; i++) { - uint256 stakedWithdrawableBalance = getWithdrawableAssetBalanceNode(token, nodes[i].getId(), true); + uint256 stakedWithdrawableBalance = getWithdrawableAssetBalanceNode( + token, + nodes[i].getId(), + true + ); if (stakedWithdrawableBalance > 0) { - revert TokenInUse(token); + revert LTMValidation.E16(); // TokenInUse } } } @@ -248,21 +248,31 @@ contract LiquidTokenManager is } /// @inheritdoc ILiquidTokenManager - function updatePrice(IERC20 token, uint256 newPrice) external onlyRole(PRICE_UPDATER_ROLE) { - if (tokens[token].decimals == 0) revert TokenNotSupported(token); - if (newPrice == 0) revert InvalidPrice(); + function updatePrice( + IERC20 token, + uint256 newPrice + ) external onlyRole(PRICE_UPDATER_ROLE) { + if (tokens[token].decimals == 0) revert LTMValidation.E07(); // TokenNotSupported + if (newPrice == 0) revert LTMValidation.E06(); // InvalidPrice uint256 oldPrice = tokens[token].pricePerUnit; - if (oldPrice == 0) revert InvalidPrice(); + if (oldPrice == 0) revert LTMValidation.E06(); // InvalidPrice // Find the ratio of price change and compare it against the asset's volatility threshold if (tokens[token].volatilityThreshold != 0) { - uint256 absPriceDiff = (newPrice > oldPrice) ? newPrice - oldPrice : oldPrice - newPrice; + uint256 absPriceDiff = (newPrice > oldPrice) + ? newPrice - oldPrice + : oldPrice - newPrice; uint256 changeRatio = (absPriceDiff * 1e18) / oldPrice; if (changeRatio > tokens[token].volatilityThreshold) { - emit VolatilityCheckFailed(token, oldPrice, newPrice, changeRatio); - revert VolatilityThresholdHit(token, changeRatio); + emit VolatilityCheckFailed( + token, + oldPrice, + newPrice, + changeRatio + ); + revert LTMValidation.E17(); // VolatilityThresholdHit } } @@ -271,12 +281,21 @@ contract LiquidTokenManager is } /// @inheritdoc ILiquidTokenManager - function setVolatilityThreshold(IERC20 asset, uint256 newThreshold) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (address(asset) == address(0)) revert ZeroAddress(); - if (tokens[asset].decimals == 0) revert TokenNotSupported(asset); - if (newThreshold != 0 && (newThreshold < 1e16 || newThreshold > 1e18)) revert InvalidThreshold(); - - emit VolatilityThresholdUpdated(asset, tokens[asset].volatilityThreshold, newThreshold, msg.sender); + function setVolatilityThreshold( + IERC20 asset, + uint256 newThreshold + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + address(asset).validateNotZero(); + if (tokens[asset].decimals == 0) revert LTMValidation.E07(); // TokenNotSupported + if (newThreshold != 0 && (newThreshold < 1e16 || newThreshold > 1e18)) + revert LTMValidation.E05(); // InvalidThreshold + + emit VolatilityThresholdUpdated( + asset, + tokens[asset].volatilityThreshold, + newThreshold, + msg.sender + ); tokens[asset].volatilityThreshold = newThreshold; } @@ -285,20 +304,24 @@ contract LiquidTokenManager is function delegateNodes( uint256[] calldata nodeIds, address[] calldata operators, - ISignatureUtilsMixinTypes.SignatureWithExpiry[] calldata approverSignatureAndExpiries, + ISignatureUtilsMixinTypes.SignatureWithExpiry[] + calldata approverSignatureAndExpiries, bytes32[] calldata approverSalts ) external onlyRole(STRATEGY_CONTROLLER_ROLE) { uint256 arrayLength = nodeIds.length; - if (operators.length != arrayLength) revert LengthMismatch(operators.length, arrayLength); - if (approverSignatureAndExpiries.length != arrayLength) - revert LengthMismatch(approverSignatureAndExpiries.length, arrayLength); - if (approverSalts.length != arrayLength) revert LengthMismatch(approverSalts.length, arrayLength); + operators.length.validateLengths(arrayLength); + approverSignatureAndExpiries.length.validateLengths(arrayLength); + approverSalts.length.validateLengths(arrayLength); // Call for nodes to delegate themselves (on EigenLayer) to corresponding operators for (uint256 i = 0; i < arrayLength; i++) { IStakerNode node = stakerNodeCoordinator.getNodeById((nodeIds[i])); - node.delegate(operators[i], approverSignatureAndExpiries[i], approverSalts[i]); + node.delegate( + operators[i], + approverSignatureAndExpiries[i], + approverSalts[i] + ); emit NodeDelegated(nodeIds[i], operators[i]); } } @@ -318,18 +341,24 @@ contract LiquidTokenManager is ) external onlyRole(STRATEGY_CONTROLLER_ROLE) nonReentrant { for (uint256 i = 0; i < allocations.length; i++) { NodeAllocation memory allocation = allocations[i]; - _stakeAssetsToNode(allocation.nodeId, allocation.assets, allocation.amounts); + _stakeAssetsToNode( + allocation.nodeId, + allocation.assets, + allocation.amounts + ); } } /// @dev Called by `stakeAssetsToNode` and `stakeAssetsToNodes` - function _stakeAssetsToNode(uint256 nodeId, IERC20[] memory assets, uint256[] memory amounts) internal { + function _stakeAssetsToNode( + uint256 nodeId, + IERC20[] memory assets, + uint256[] memory amounts + ) internal { uint256 assetsLength = assets.length; uint256 amountsLength = amounts.length; - if (assetsLength != amountsLength) { - revert LengthMismatch(assetsLength, amountsLength); - } + assetsLength.validateLengths(amountsLength); IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); @@ -338,12 +367,10 @@ contract LiquidTokenManager is for (uint256 i = 0; i < assetsLength; i++) { IERC20 asset = assets[i]; if (amounts[i] == 0) { - revert InvalidStakingAmount(amounts[i]); + revert LTMValidation.E08(); // InvalidStakingAmount } IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); strategiesForNode[i] = strategy; } @@ -362,201 +389,28 @@ contract LiquidTokenManager is assets[i].safeTransfer(address(node), depositAmounts[i]); } - emit AssetsStakedToNode(nodeId, depositAssets, depositAmounts, msg.sender); - - // Call for node to deposit assets into EigenLayer - node.depositAssets(depositAssets, depositAmounts, strategiesForNode); - - emit AssetsDepositedToEigenlayer(depositAssets, depositAmounts, strategiesForNode, address(node)); - } - - /// @inheritdoc ILiquidTokenManager - function swapAndStakeAssetsToNodes( - NodeAllocationWithSwap[] calldata allocationsWithSwaps - ) external onlyRole(STRATEGY_CONTROLLER_ROLE) nonReentrant { - for (uint256 i = 0; i < allocationsWithSwaps.length; i++) { - NodeAllocationWithSwap memory allocationWithSwap = allocationsWithSwaps[i]; - _swapAndStakeAssetsToNode( - allocationWithSwap.nodeId, - allocationWithSwap.assetsToSwap, - allocationWithSwap.amountsToSwap, - allocationWithSwap.assetsToStake - ); - } - } - - /// @inheritdoc ILiquidTokenManager - function swapAndStakeAssetsToNode( - uint256 nodeId, - IERC20[] memory assetsToSwap, - uint256[] memory amountsToSwap, - IERC20[] memory assetsToStake - ) external onlyRole(STRATEGY_CONTROLLER_ROLE) nonReentrant { - _swapAndStakeAssetsToNode(nodeId, assetsToSwap, amountsToSwap, assetsToStake); - } - - /// @dev Called by `swapAndStakeAssetsToNode` and `swapAndStakeAssetsToNodes` - /// @dev Flow: LTM >> DEX >> LTM (using LSR for routing data) - function _swapAndStakeAssetsToNode( - uint256 nodeId, - IERC20[] memory assetsToSwap, - uint256[] memory amountsToSwap, - IERC20[] memory assetsToStake - ) internal { - uint256 assetsLength = assetsToStake.length; - - if (assetsLength != assetsToSwap.length) { - revert LengthMismatch(assetsLength, assetsToSwap.length); - } - if (assetsLength != amountsToSwap.length) { - revert LengthMismatch(assetsLength, amountsToSwap.length); - } - - // Validate that ETH is not used as direct tokenIn or tokenOut (only as bridge asset) - for (uint256 i = 0; i < assetsLength; i++) { - if (address(assetsToSwap[i]) == _ETH_ADDRESS) { - revert ETHNotSupportedAsDirectToken(address(assetsToSwap[i])); - } - if (address(assetsToStake[i]) == _ETH_ADDRESS) { - revert ETHNotSupportedAsDirectToken(address(assetsToStake[i])); - } - } - - IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); - - // Find EigenLayer strategies for the given assets - IStrategy[] memory strategiesForNode = new IStrategy[](assetsLength); - for (uint256 i = 0; i < assetsLength; i++) { - IERC20 asset = assetsToStake[i]; // using `assetsToStake` not `assetsToSwap` - if (amountsToSwap[i] == 0) { - revert InvalidStakingAmount(amountsToSwap[i]); - } - IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } - strategiesForNode[i] = strategy; - } - - // Bring unstaked assets in from `LiquidToken` - liquidToken.transferAssets(assetsToSwap, amountsToSwap, address(this)); - - uint256[] memory amountsToStake = new uint256[](assetsLength); - - // Swap using LSR - for every `tokenIn`, swap to corresponding `tokenOut` - for (uint256 i = 0; i < assetsLength; i++) { - address tokenIn = address(assetsToSwap[i]); - address tokenOut = address(assetsToStake[i]); - uint256 amountIn = amountsToSwap[i]; - - if (tokenIn == tokenOut) { - // No swap needed, direct stake - amountsToStake[i] = amountIn; - } else { - // Get swap plan from LSR - (, ILSTSwapRouter.MultiStepExecutionPlan memory plan) = lstSwapRouter.getCompleteMultiStepPlan( - tokenIn, - tokenOut, - amountIn, - address(this) // LTM is the recipient - ); - - // Execute the swap plan step by step - uint256 actualAmountOut = _executeLsrSwapPlan(tokenOut, plan); - amountsToStake[i] = actualAmountOut; - - emit SwapExecuted(tokenIn, tokenOut, amountIn, actualAmountOut, nodeId); - } - } - - IERC20[] memory depositAssets = new IERC20[](assetsLength); - uint256[] memory depositAmounts = new uint256[](assetsLength); - - // Transfer assets to node - for (uint256 i = 0; i < assetsLength; i++) { - depositAssets[i] = assetsToStake[i]; - depositAmounts[i] = amountsToStake[i]; - assetsToStake[i].safeTransfer(address(node), amountsToStake[i]); - } - - emit AssetsSwappedAndStakedToNode( + emit AssetsStakedToNode( nodeId, - assetsToSwap, - amountsToSwap, - assetsToStake, - amountsToStake, + depositAssets, + depositAmounts, msg.sender ); // Call for node to deposit assets into EigenLayer node.depositAssets(depositAssets, depositAmounts, strategiesForNode); - emit AssetsDepositedToEigenlayer(depositAssets, depositAmounts, strategiesForNode, address(node)); - } - - /// @dev Executes a swap plan from LSR following LTM >> DEX >> LTM flow - /// @param tokenOut Output token address - /// @param plan Execution plan from LSR - /// @return actualAmountOut The actual amount received from the swap - function _executeLsrSwapPlan( - address tokenOut, - ILSTSwapRouter.MultiStepExecutionPlan memory plan - ) internal returns (uint256 actualAmountOut) { - require(plan.steps.length > 0, "Empty swap plan"); - require(address(lstSwapRouter) != address(0), "LSR not configured"); - - // Track balances before and after - uint256 initialBalance = tokenOut == _ETH_ADDRESS - ? address(this).balance - : IERC20(tokenOut).balanceOf(address(this)); - - // Execute each step in the plan - for (uint256 i = 0; i < plan.steps.length; i++) { - ILSTSwapRouter.SwapStep memory step = plan.steps[i]; - - // Approve the target DEX to spend our tokens - if (step.tokenIn != _ETH_ADDRESS) { - IERC20(step.tokenIn).safeApprove(step.target, 0); - IERC20(step.tokenIn).safeApprove(step.target, step.amountIn); - } - - // Execute the swap on the DEX - (bool success, bytes memory returnData) = step.target.call{value: step.value}(step.data); - - if (!success) { - // Decode revert reason if possible - if (returnData.length > 0) { - assembly { - let returnDataSize := mload(returnData) - revert(add(32, returnData), returnDataSize) - } - } else { - revert("Swap execution failed"); - } - } - - // Reset approval - if (step.tokenIn != _ETH_ADDRESS) { - IERC20(step.tokenIn).safeApprove(step.target, 0); - } - } - - // Calculate actual output amount - uint256 finalBalance = tokenOut == _ETH_ADDRESS - ? address(this).balance - : IERC20(tokenOut).balanceOf(address(this)); - - actualAmountOut = finalBalance - initialBalance; - - // Validate we received at least the minimum expected - ILSTSwapRouter.SwapStep memory lastStep = plan.steps[plan.steps.length - 1]; - require(actualAmountOut >= lastStep.minAmountOut, "Insufficient output amount"); - - return actualAmountOut; + emit AssetsDepositedToEigenlayer( + depositAssets, + depositAmounts, + strategiesForNode, + address(node) + ); } /// @inheritdoc ILiquidTokenManager - function undelegateNodes(uint256[] calldata nodeIds) external override onlyRole(STRATEGY_CONTROLLER_ROLE) { + function undelegateNodes( + uint256[] calldata nodeIds + ) external override onlyRole(STRATEGY_CONTROLLER_ROLE) { // Fetch and add all asset balances from the node to queued balances for (uint256 i = 0; i < nodeIds.length; i++) { _createRedemptionNodeUndelegation(nodeIds[i]); @@ -566,32 +420,50 @@ contract LiquidTokenManager is /// @dev Called by `undelegateNodes` function _createRedemptionNodeUndelegation(uint256 nodeId) private { IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); - uint256 nonce = delegationManager.cumulativeWithdrawalsQueued(address(node)); + uint256 nonce = delegationManager.cumulativeWithdrawalsQueued( + address(node) + ); address delegatedTo = node.getOperatorDelegation(); // Find strategies and deposit shares - (IStrategy[] memory redemptionStrategies, uint256[] memory redemptionShares) = strategyManager.getDeposits( - address(node) - ); + ( + IStrategy[] memory redemptionStrategies, + uint256[] memory redemptionShares + ) = strategyManager.getDeposits(address(node)); // Find withdrawable shares - (uint256[] memory redemptionElWithdrawableShares, ) = delegationManager.getWithdrawableShares( - address(node), - redemptionStrategies - ); + (uint256[] memory redemptionElWithdrawableShares, ) = delegationManager + .getWithdrawableShares(address(node), redemptionStrategies); // Undelegate node on EL and return withdrawal info ( bytes32[] memory withdrawalRoots, IDelegationManagerTypes.Withdrawal[] memory withdrawals, IERC20[] memory redemptionAssets - ) = _processNodeForUndelegation(nodeId, node, delegatedTo, redemptionStrategies, redemptionShares, nonce); + ) = _processNodeForUndelegation( + nodeId, + node, + delegatedTo, + redemptionStrategies, + redemptionShares, + nonce + ); // Credit queued asset shares with total withdrawable shares - liquidToken.creditQueuedAssetElShares(redemptionAssets, redemptionElWithdrawableShares); + liquidToken.creditQueuedAssetElShares( + redemptionAssets, + redemptionElWithdrawableShares + ); bytes32[] memory requestIds = new bytes32[](1); - requestIds[0] = keccak256(abi.encode(redemptionAssets, redemptionShares, block.timestamp, _redemptionNonce)); + requestIds[0] = keccak256( + abi.encode( + redemptionAssets, + redemptionShares, + block.timestamp, + _redemptionNonce + ) + ); emit RedemptionCreatedForNodeUndelegation( _createRedemption( @@ -608,6 +480,7 @@ contract LiquidTokenManager is nodeId ); } + /// @dev Called by `_createRedemptionNodeUndelegation` function _processNodeForUndelegation( uint256 nodeId, @@ -629,7 +502,9 @@ contract LiquidTokenManager is emit NodeUndelegated(nodeId, delegatedTo); // Construct withdrawal structs - withdrawals = new IDelegationManagerTypes.Withdrawal[](withdrawalRoots.length); + withdrawals = new IDelegationManagerTypes.Withdrawal[]( + withdrawalRoots.length + ); redemptionAssets = new IERC20[](withdrawalRoots.length); // We can use a 1D array since every withdrawal corresponds to only 1 asset // The order of strategies in `withdrawalRoots[]` is the same as that of `redemptionStrategies[]` @@ -640,20 +515,26 @@ contract LiquidTokenManager is redemptionAssets[i] = strategyTokens[redemptionStrategies[i]]; uint256[] memory requestScaledShares = new uint256[](1); - requestScaledShares[0] = _scaleSharesForNodeAsset(nodeId, redemptionAssets[i], redemptionShares[i]); + requestScaledShares[0] = _scaleSharesForNodeAsset( + nodeId, + redemptionAssets[i], + redemptionShares[i] + ); - IDelegationManagerTypes.Withdrawal memory withdrawal = IDelegationManagerTypes.Withdrawal({ - staker: address(node), - delegatedTo: node.getOperatorDelegation(), - withdrawer: address(node), - nonce: nonce++, - startBlock: uint32(block.number), - strategies: requestStrategies, - scaledShares: requestScaledShares - }); + IDelegationManagerTypes.Withdrawal + memory withdrawal = IDelegationManagerTypes.Withdrawal({ + staker: address(node), + delegatedTo: node.getOperatorDelegation(), + withdrawer: address(node), + nonce: nonce++, + startBlock: uint32(block.number), + strategies: requestStrategies, + scaledShares: requestScaledShares + }); // Make sure our withdrawal struct is the same as what EL computed - if (withdrawalRoots[i] != keccak256(abi.encode(withdrawal))) revert InvalidWithdrawalRoot(); + if (withdrawalRoots[i] != keccak256(abi.encode(withdrawal))) + revert LTMValidation.E18(); // InvalidWithdrawalRoot withdrawals[i] = withdrawal; } @@ -667,8 +548,8 @@ contract LiquidTokenManager is IERC20[][] calldata assets, uint256[][] calldata elDepositShares ) external override nonReentrant onlyRole(STRATEGY_CONTROLLER_ROLE) { - if (assets.length != nodeIds.length) revert LengthMismatch(assets.length, nodeIds.length); - if (elDepositShares.length != nodeIds.length) revert LengthMismatch(elDepositShares.length, nodeIds.length); + assets.length.validateLengths(nodeIds.length); + elDepositShares.length.validateLengths(nodeIds.length); _createRedemptionRebalancing(nodeIds, assets, elDepositShares); } @@ -680,13 +561,16 @@ contract LiquidTokenManager is uint256[][] calldata nodeElDepositShares ) internal { bytes32[] memory withdrawalRoots = new bytes32[](nodeIds.length); - IDelegationManagerTypes.Withdrawal[] memory withdrawals = new IDelegationManagerTypes.Withdrawal[]( - nodeIds.length - ); + IDelegationManagerTypes.Withdrawal[] + memory withdrawals = new IDelegationManagerTypes.Withdrawal[]( + nodeIds.length + ); bytes32[] memory requestIds = new bytes32[](nodeIds.length); IERC20[] memory redemptionAssets = new IERC20[](supportedTokens.length); - uint256[] memory redemptionElWithdrawableShares = new uint256[](supportedTokens.length); + uint256[] memory redemptionElWithdrawableShares = new uint256[]( + supportedTokens.length + ); uint256 uniqueTokenCount = 0; for (uint256 i = 0; i < nodeIds.length; i++) { @@ -707,14 +591,23 @@ contract LiquidTokenManager is ); requestIds[i] = keccak256( - abi.encode(nodeAssets[i], nodeElDepositShares[i], block.timestamp, i, _redemptionNonce) + abi.encode( + nodeAssets[i], + nodeElDepositShares[i], + block.timestamp, + i, + _redemptionNonce + ) ); } // Credit queued asset shares with total withdrawable shares // Here we specifically factor in any slashing of staked funds in order to maintain accurate values for AUM calc // If there is any additional slashing after this (during EL withdrawal queue period), we handle it in redemption completion - liquidToken.creditQueuedAssetElShares(redemptionAssets, redemptionElWithdrawableShares); + liquidToken.creditQueuedAssetElShares( + redemptionAssets, + redemptionElWithdrawableShares + ); emit RedemptionCreatedForRebalancing( _createRedemption( @@ -743,22 +636,30 @@ contract LiquidTokenManager is uint256 uniqueTokenCount = currentUniqueTokenCount; for (uint256 j = 0; j < assets.length; j++) { - if (elDepositShares[j] == 0) { - revert ZeroAmount(); - } + elDepositShares[j].validateAmount(); - uint256 depositShares = getDepositAssetBalanceNode(assets[j], nodeId, true); + uint256 depositShares = getDepositAssetBalanceNode( + assets[j], + nodeId, + true + ); // EL deposits for the asset must exist and cannot be less than proposed clawback amount if (depositShares == 0 || depositShares < elDepositShares[j]) { - revert InsufficientBalance(assets[j], elDepositShares[j], depositShares); + revert LTMValidation.E19(); // InsufficientBalance } - uint256 withdrawableShares = getWithdrawableAssetBalanceNode(assets[j], nodeId, true); + uint256 withdrawableShares = getWithdrawableAssetBalanceNode( + assets[j], + nodeId, + true + ); bool found = false; for (uint256 k = 0; k < uniqueTokenCount; k++) { if (redemptionAssets[k] == assets[j]) { - redemptionElWithdrawableShares[k] += (elDepositShares[j] * withdrawableShares) / depositShares; // Factor in any slashing + redemptionElWithdrawableShares[k] += + (elDepositShares[j] * withdrawableShares) / + depositShares; // Factor in any slashing found = true; break; } @@ -779,18 +680,23 @@ contract LiquidTokenManager is function settleUserWithdrawals( UserWithdrawalsSettlement calldata settlement ) external override nonReentrant onlyRole(STRATEGY_CONTROLLER_ROLE) { - if (settlement.elAssets.length != settlement.nodeIds.length) - revert LengthMismatch(settlement.elAssets.length, settlement.nodeIds.length); - if (settlement.elDepositShares.length != settlement.nodeIds.length) - revert LengthMismatch(settlement.elDepositShares.length, settlement.nodeIds.length); + settlement.elAssets.length.validateLengths(settlement.nodeIds.length); + settlement.elDepositShares.length.validateLengths( + settlement.nodeIds.length + ); // Check if all associated withdrawal requests actually get fulfilled from the input amounts - (IERC20[] memory redemptionAssets, uint256[] memory redemptionElWithdrawableShares) = _verifyAllRequestsSettle( - settlement - ); + ( + IERC20[] memory redemptionAssets, + uint256[] memory redemptionElWithdrawableShares + ) = _verifyAllRequestsSettle(settlement); // Create a redemption for the settlement by withdrawing from staker nodes - _createRedemptionUserWithdrawals(settlement, redemptionAssets, redemptionElWithdrawableShares); + _createRedemptionUserWithdrawals( + settlement, + redemptionAssets, + redemptionElWithdrawableShares + ); } /// @notice Checks if the cumulative amounts per asset once drawn would actually settle ALL user withdrawal requests @@ -799,47 +705,67 @@ contract LiquidTokenManager is UserWithdrawalsSettlement calldata settlement ) internal returns (IERC20[] memory, uint256[] memory) { // Get all associated withdrawal requests (reverts for any invalid request id) - IWithdrawalManager.WithdrawalRequest[] memory withdrawalRequests = withdrawalManager.getWithdrawalRequests( - settlement.requestIds - ); + IWithdrawalManager.WithdrawalRequest[] + memory withdrawalRequests = withdrawalManager.getWithdrawalRequests( + settlement.requestIds + ); uint256 uniqueTokenCount; IERC20[] memory redemptionAssets = new IERC20[](supportedTokens.length); - uint256[] memory redemptionElDepositShares = new uint256[](supportedTokens.length); + uint256[] memory redemptionElDepositShares = new uint256[]( + supportedTokens.length + ); // Aggregate cumulative amounts that need to be settled, across all withdrawal requests, - (uniqueTokenCount, redemptionAssets, redemptionElDepositShares) = _processWithdrawalRequests( - withdrawalRequests - ); + ( + uniqueTokenCount, + redemptionAssets, + redemptionElDepositShares + ) = _processWithdrawalRequests(withdrawalRequests); // Track the proposed amounts to be clawed back from nodes - uint256[] memory proposedRedemptionElDepositShares = new uint256[](uniqueTokenCount); + uint256[] memory proposedRedemptionElDepositShares = new uint256[]( + uniqueTokenCount + ); // Track the withdrawable amounts after slashing, for internal accounting // This allows correct AUM calc, where queued balances are checked, slashing is included - uint256[] memory redemptionElWithdrawableShares = new uint256[](uniqueTokenCount); + uint256[] memory redemptionElWithdrawableShares = new uint256[]( + uniqueTokenCount + ); for (uint256 i = 0; i < settlement.nodeIds.length; i++) { for (uint256 j = 0; j < settlement.elAssets[i].length; j++) { IERC20 token = settlement.elAssets[i][j]; - if (settlement.elDepositShares[i][j] == 0) { - revert ZeroAmount(); - } + settlement.elDepositShares[i][j].validateAmount(); - uint256 depositShares = getDepositAssetBalanceNode(token, settlement.nodeIds[i], true); + uint256 depositShares = getDepositAssetBalanceNode( + token, + settlement.nodeIds[i], + true + ); // EL deposits for the asset must exist and cannot be less than proposed clawback amount - if (depositShares == 0 || depositShares < settlement.elDepositShares[i][j]) { - revert InsufficientBalance(token, settlement.elDepositShares[i][j], depositShares); + if ( + depositShares == 0 || + depositShares < settlement.elDepositShares[i][j] + ) { + revert LTMValidation.E19(); // InsufficientBalance } - uint256 withdrawableShares = getWithdrawableAssetBalanceNode(token, settlement.nodeIds[i], true); + uint256 withdrawableShares = getWithdrawableAssetBalanceNode( + token, + settlement.nodeIds[i], + true + ); for (uint256 k = 0; k < uniqueTokenCount; k++) { if (redemptionAssets[k] == token) { - proposedRedemptionElDepositShares[k] += settlement.elDepositShares[i][j]; + proposedRedemptionElDepositShares[k] += settlement + .elDepositShares[i][j]; redemptionElWithdrawableShares[k] += - (settlement.elDepositShares[i][j] * withdrawableShares) / + (settlement.elDepositShares[i][j] * + withdrawableShares) / depositShares; // Factor in any slashing break; } @@ -851,32 +777,43 @@ contract LiquidTokenManager is // We are not concerned with slashing here, hence we use the EL `depositShares` -- slashing loss will be passed on after withdrawal completion // We allow 10 bps margin of error for rounding for (uint256 i = 0; i < uniqueTokenCount; i++) { - uint256 upperMargin = Math.mulDiv(redemptionElDepositShares[i], 10, 10000, Math.Rounding.Up); - uint256 lowerMargin = Math.mulDiv(redemptionElDepositShares[i], 10, 10000, Math.Rounding.Down); + uint256 upperMargin = Math.mulDiv( + redemptionElDepositShares[i], + 10, + 10000, + Math.Rounding.Up + ); + uint256 lowerMargin = Math.mulDiv( + redemptionElDepositShares[i], + 10, + 10000, + Math.Rounding.Down + ); uint256 maxAllowed = redemptionElDepositShares[i] + upperMargin; uint256 minAllowed = redemptionElDepositShares[i] - lowerMargin; if ( - proposedRedemptionElDepositShares[i] > maxAllowed || proposedRedemptionElDepositShares[i] < minAllowed + proposedRedemptionElDepositShares[i] > maxAllowed || + proposedRedemptionElDepositShares[i] < minAllowed ) { - revert RequestsDoNotSettle( - address(redemptionAssets[i]), - proposedRedemptionElDepositShares[i], - redemptionElDepositShares[i] - ); + revert LTMValidation.E20(); // RequestsDoNotSettle } } // Trim arrays to actual sizes assembly { mstore(redemptionAssets, uniqueTokenCount) + mstore(redemptionElWithdrawableShares, uniqueTokenCount) } // Credit queued asset shares with total withdrawable amounts, post slashing // As noted above, here we specifically factor in any slashing to maintain accurate AUM calc // If there is any additional slashing after this (during EL withdrawal queue period), we handle it in redemption completion - liquidToken.creditQueuedAssetElShares(redemptionAssets, redemptionElWithdrawableShares); + liquidToken.creditQueuedAssetElShares( + redemptionAssets, + redemptionElWithdrawableShares + ); return (redemptionAssets, redemptionElWithdrawableShares); } @@ -887,31 +824,17 @@ contract LiquidTokenManager is ) internal view - returns (uint256 uniqueTokenCount, IERC20[] memory redemptionAssets, uint256[] memory redemptionElDepositShares) + returns ( + uint256 uniqueTokenCount, + IERC20[] memory redemptionAssets, + uint256[] memory redemptionElDepositShares + ) { - redemptionAssets = new IERC20[](supportedTokens.length); - redemptionElDepositShares = new uint256[](supportedTokens.length); - uniqueTokenCount = 0; - - for (uint256 i = 0; i < withdrawalRequests.length; i++) { - IWithdrawalManager.WithdrawalRequest memory request = withdrawalRequests[i]; - for (uint256 j = 0; j < request.assets.length; j++) { - IERC20 token = request.assets[j]; - bool found = false; - for (uint256 k = 0; k < uniqueTokenCount; k++) { - if (redemptionAssets[k] == token) { - redemptionElDepositShares[k] += request.elWithdrawableShares[j]; // These are deposit shares for now, we will slash them on redemption completion - found = true; - break; - } - } - if (!found) { - redemptionAssets[uniqueTokenCount] = token; - redemptionElDepositShares[uniqueTokenCount] = request.elWithdrawableShares[j]; // These are deposit shares for now, we will slash them on redemption completion - uniqueTokenCount++; - } - } - } + return + LTMWithdrawalProcessor.processWithdrawalRequests( + withdrawalRequests, + supportedTokens + ); } /// @notice Creates a redemption for the unstaked funds portion of a user withdrawals settlement @@ -921,10 +844,13 @@ contract LiquidTokenManager is IERC20[] memory redemptionAssets, uint256[] memory redemptionElWithdrawableShares ) internal { - bytes32[] memory withdrawalRoots = new bytes32[](settlement.nodeIds.length); - IDelegationManagerTypes.Withdrawal[] memory withdrawals = new IDelegationManagerTypes.Withdrawal[]( + bytes32[] memory withdrawalRoots = new bytes32[]( settlement.nodeIds.length ); + IDelegationManagerTypes.Withdrawal[] + memory withdrawals = new IDelegationManagerTypes.Withdrawal[]( + settlement.nodeIds.length + ); // Call for EL withdrawals on staker nodes with the unscaled deposit shares for (uint256 i = 0; i < settlement.nodeIds.length; i++) { @@ -958,7 +884,7 @@ contract LiquidTokenManager is IERC20[] memory assets, uint256[] memory shares ) private returns (bytes32, IDelegationManagerTypes.Withdrawal memory) { - if (assets.length != shares.length) revert LengthMismatch(assets.length, shares.length); + assets.length.validateLengths(shares.length); // Build the Withdrawal struct IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); @@ -966,23 +892,29 @@ contract LiquidTokenManager is address staker = address(node); uint256 nonce = delegationManager.cumulativeWithdrawalsQueued(staker); address delegatedTo = node.getOperatorDelegation(); - uint256[] memory scaledShares = _scaleSharesForNode(nodeId, assets, shares); - - IDelegationManagerTypes.Withdrawal memory withdrawal = IDelegationManagerTypes.Withdrawal({ - staker: staker, - delegatedTo: delegatedTo, - withdrawer: staker, - nonce: nonce, - startBlock: uint32(block.number), - strategies: strategies, - scaledShares: scaledShares - }); + uint256[] memory scaledShares = _scaleSharesForNode( + nodeId, + assets, + shares + ); + + IDelegationManagerTypes.Withdrawal + memory withdrawal = IDelegationManagerTypes.Withdrawal({ + staker: staker, + delegatedTo: delegatedTo, + withdrawer: staker, + nonce: nonce, + startBlock: uint32(block.number), + strategies: strategies, + scaledShares: scaledShares + }); // Request withdrawal on EL bytes32 withdrawalRoot = node.withdrawAssets(strategies, shares); // Make sure our withdrawal struct is the same as what EL computed - if (withdrawalRoot != keccak256(abi.encode(withdrawal))) revert InvalidWithdrawalRoot(); + if (withdrawalRoot != keccak256(abi.encode(withdrawal))) + revert LTMValidation.E18(); // InvalidWithdrawalRoot return (withdrawalRoot, withdrawal); } @@ -995,7 +927,14 @@ contract LiquidTokenManager is uint256[] memory elWithdrawableShares, address receiver ) private returns (bytes32) { - bytes32 redemptionId = keccak256(abi.encode(requestIds, withdrawalRoots, block.timestamp, _redemptionNonce)); + bytes32 redemptionId = keccak256( + abi.encode( + requestIds, + withdrawalRoots, + block.timestamp, + _redemptionNonce + ) + ); _redemptionNonce += 1; Redemption memory redemption = Redemption({ @@ -1019,20 +958,27 @@ contract LiquidTokenManager is IDelegationManagerTypes.Withdrawal[][] calldata withdrawals, IERC20[][][] calldata assets ) external override nonReentrant onlyRole(STRATEGY_CONTROLLER_ROLE) { - if (withdrawals.length != nodeIds.length) revert LengthMismatch(withdrawals.length, nodeIds.length); - if (assets.length != nodeIds.length) revert LengthMismatch(withdrawals.length, nodeIds.length); + withdrawals.length.validateLengths(nodeIds.length); + assets.length.validateLengths(nodeIds.length); // Reverts for invalid `redemptionId` - Redemption memory redemption = withdrawalManager.getRedemption(redemptionId); + Redemption memory redemption = withdrawalManager.getRedemption( + redemptionId + ); address receiver = redemption.receiver; - if (receiver != address(withdrawalManager) && receiver != address(liquidToken)) - revert InvalidReceiver(receiver); + if ( + receiver != address(withdrawalManager) && + receiver != address(liquidToken) + ) revert LTMValidation.E11(); // InvalidReceiver // Check if the exact set of withdrawals concerned the redemption have been provided // Partial completion of a redemption is not accepted // Withdrawals that weren't part of the original redemption are not accepted - _validateRedemption(redemption.withdrawalRoots, withdrawals); + LTMValidation.validateRedemption( + redemption.withdrawalRoots, + withdrawals + ); // Track unique tokens received from completion of all withdrawals across all nodes IERC20[] memory receivedTokens = new IERC20[](supportedTokens.length); @@ -1050,8 +996,12 @@ contract LiquidTokenManager is // Keep track of the actual amounts received // This may differ from the original requested shares in the `Withdrawal` struct due to slashing - uint256[] memory receivedAmounts = new uint256[](supportedTokens.length); - uint256[] memory receivedElShares = new uint256[](supportedTokens.length); + uint256[] memory receivedAmounts = new uint256[]( + supportedTokens.length + ); + uint256[] memory receivedElShares = new uint256[]( + supportedTokens.length + ); // Transfer all withdrawn assets to `receiver`, either `LiquidToken` or `WithdrawalManager` for (uint256 i = 0; i < uniqueTokenCount; i++) { @@ -1064,9 +1014,11 @@ contract LiquidTokenManager is uint256 receiverBalanceAfter = token.balanceOf(receiver); // Calculate actual net amount transferred - uint256 netTransferredAmount = receiverBalanceAfter - receiverBalanceBefore; + uint256 netTransferredAmount = receiverBalanceAfter - + receiverBalanceBefore; receivedAmounts[i] = netTransferredAmount; - receivedElShares[i] = tokenStrategies[token].underlyingToSharesView(netTransferredAmount); + receivedElShares[i] = tokenStrategies[token] + .underlyingToSharesView(netTransferredAmount); } else { receivedAmounts[i] = 0; receivedElShares[i] = 0; @@ -1074,48 +1026,25 @@ contract LiquidTokenManager is } // Update Withdrawal Manager and retrieve the original requested shares - uint256[] memory requestedElShares = withdrawalManager.recordRedemptionCompleted( - // Accounting for any slashing during withdrawal queue period handled here - redemptionId, - receivedTokens, - receivedElShares - ); + uint256[] memory requestedElShares = withdrawalManager + .recordRedemptionCompleted( + // Accounting for any slashing during withdrawal queue period handled here + redemptionId, + receivedTokens, + receivedElShares + ); // If receiver is `LiquidToken`, we follow the debit done in `recordRedemptionCreated` with a corresponding credit to asset balances if (receiver == address(liquidToken)) { liquidToken.creditAssetBalances(receivedTokens, receivedAmounts); } - emit RedemptionCompleted(redemptionId, receivedTokens, requestedElShares, receivedAmounts); - } - - function _validateRedemption( - bytes32[] memory redemptionWithdrawalRoots, - IDelegationManagerTypes.Withdrawal[][] calldata withdrawals - ) internal pure { - uint256 totalWithdrawals = 0; - for (uint256 j = 0; j < withdrawals.length; j++) { - totalWithdrawals += withdrawals[j].length; - } - - bytes32[] memory allWithdrawalHashes = new bytes32[](totalWithdrawals); - uint256 index = 0; - for (uint256 j = 0; j < withdrawals.length; j++) { - for (uint256 k = 0; k < withdrawals[j].length; k++) { - allWithdrawalHashes[index++] = keccak256(abi.encode(withdrawals[j][k])); - } - } - - for (uint256 i = 0; i < redemptionWithdrawalRoots.length; i++) { - bool found = false; - for (uint256 h = 0; h < allWithdrawalHashes.length; h++) { - if (allWithdrawalHashes[h] == redemptionWithdrawalRoots[i]) { - found = true; - break; - } - } - if (!found) revert WithdrawalMissing(redemptionWithdrawalRoots[i]); - } + emit RedemptionCompleted( + redemptionId, + receivedTokens, + requestedElShares, + receivedAmounts + ); } /// @dev Called by `completeRedemption` @@ -1127,26 +1056,15 @@ contract LiquidTokenManager is uint256 uniqueTokenCount ) private returns (uint256) { IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); - IERC20[] memory receivedTokens = node.completeWithdrawals(withdrawals, assets); - - // Track received tokens - for (uint256 j = 0; j < receivedTokens.length; j++) { - IERC20 token = receivedTokens[j]; - - bool found = false; - for (uint256 k = 0; k < uniqueTokenCount; k++) { - if (uniqueTokens[k] == token) { - found = true; - break; - } - } - - if (!found) { - uniqueTokens[uniqueTokenCount++] = token; - } - } - - return uniqueTokenCount; + return + LTMWithdrawalProcessor.completeELWithdrawals( + nodeId, + withdrawals, + assets, + uniqueTokens, + uniqueTokenCount, + node + ); } /// @dev Called by `_createELWithdrawal` @@ -1155,36 +1073,32 @@ contract LiquidTokenManager is IERC20[] memory assets, uint256[] memory shares ) internal view returns (uint256[] memory) { - address nodeAddress = address(stakerNodeCoordinator.getNodeById(nodeId)); - uint256[] memory scaledShares = new uint256[](assets.length); - - for (uint256 i = 0; i < assets.length; i++) { - scaledShares[i] = shares[i].mulDiv( - delegationManager.depositScalingFactor(nodeAddress, tokenStrategies[assets[i]]), - 1e18 + return + LTMHelpers.scaleSharesForNode( + nodeId, + assets, + shares, + tokenStrategies, + delegationManager, + stakerNodeCoordinator ); - } - - return scaledShares; } /// @dev Called by `_createRedemptionNodeUndelegation`, - function _scaleSharesForNodeAsset(uint256 nodeId, IERC20 asset, uint256 shares) internal view returns (uint256) { - address nodeAddress = address(stakerNodeCoordinator.getNodeById(nodeId)); - - uint256 scaledSharesAsset = 0; - - scaledSharesAsset = shares.mulDiv( - delegationManager.depositScalingFactor(nodeAddress, tokenStrategies[asset]), - 1e18 - ); - - return scaledSharesAsset; - } - - /// @notice Fallback to receive ETH from swaps - receive() external payable { - // Accept ETH from DEX swaps + function _scaleSharesForNodeAsset( + uint256 nodeId, + IERC20 asset, + uint256 shares + ) internal view returns (uint256) { + return + LTMHelpers.scaleSharesForNodeAsset( + nodeId, + asset, + shares, + tokenStrategies, + delegationManager, + stakerNodeCoordinator + ); } // ------------------------------------------------------------------------------ @@ -1197,13 +1111,15 @@ contract LiquidTokenManager is } /// @inheritdoc ILiquidTokenManager - function getTokenInfo(IERC20 token) external view returns (TokenInfo memory) { - if (address(token) == address(0)) revert ZeroAddress(); + function getTokenInfo( + IERC20 token + ) external view returns (TokenInfo memory) { + address(token).validateNotZero(); TokenInfo memory tokenInfo = tokens[token]; if (tokenInfo.decimals == 0) { - revert TokenNotSupported(token); + revert LTMValidation.E07(); // TokenNotSupported } return tokenInfo; @@ -1211,28 +1127,26 @@ contract LiquidTokenManager is /// @inheritdoc ILiquidTokenManager function getTokenStrategy(IERC20 asset) external view returns (IStrategy) { - if (address(asset) == address(0)) revert ZeroAddress(); + address(asset).validateNotZero(); IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); return strategy; } /// @inheritdoc ILiquidTokenManager - function getTokensStrategies(IERC20[] memory assets) public view returns (IStrategy[] memory) { + function getTokensStrategies( + IERC20[] memory assets + ) public view returns (IStrategy[] memory) { IStrategy[] memory strategies = new IStrategy[](assets.length); for (uint256 i = 0; i < assets.length; i++) { IERC20 asset = assets[i]; - if (address(asset) == address(0)) revert ZeroAddress(); + address(asset).validateNotZero(); IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); strategies[i] = strategy; } @@ -1241,70 +1155,73 @@ contract LiquidTokenManager is } /// @inheritdoc ILiquidTokenManager - function getStrategyToken(IStrategy strategy) external view returns (IERC20) { - if (address(strategy) == address(0)) revert ZeroAddress(); + function getStrategyToken( + IStrategy strategy + ) external view returns (IERC20) { + address(strategy).validateNotZero(); IERC20 token = strategyTokens[strategy]; if (address(token) == address(0)) { - revert TokenForStrategyNotFound(address(strategy)); + revert LTMValidation.E21(); // TokenForStrategyNotFound } return token; } /// @inheritdoc ILiquidTokenManager - function getDepositAssetBalance(IERC20 asset, bool inElShares) external view returns (uint256) { + function getDepositAssetBalance( + IERC20 asset, + bool inElShares + ) external view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); uint256 totalBalance = 0; for (uint256 i = 0; i < nodes.length; i++) { - totalBalance += _getDepositAssetBalanceNode(asset, nodes[i], inElShares); + totalBalance += LTMBalances.getDepositBalance( + strategy, + address(nodes[i]), + inElShares + ); } return totalBalance; } /// @inheritdoc ILiquidTokenManager - function getDepositAssetBalanceNode(IERC20 asset, uint256 nodeId, bool inElShares) public view returns (uint256) { + function getDepositAssetBalanceNode( + IERC20 asset, + uint256 nodeId, + bool inElShares + ) public view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); - return _getDepositAssetBalanceNode(asset, node, inElShares); + return + LTMBalances.getDepositBalance(strategy, address(node), inElShares); } - /// @dev Called by `getDepositAssetBalance` and `getDepositAssetBalanceNode` - function _getDepositAssetBalanceNode( + /// @inheritdoc ILiquidTokenManager + function getWithdrawableAssetBalance( IERC20 asset, - IStakerNode node, bool inElShares - ) internal view returns (uint256) { + ) external view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } - return inElShares ? strategy.shares(address(node)) : strategy.userUnderlyingView(address(node)); - } - - /// @inheritdoc ILiquidTokenManager - function getWithdrawableAssetBalance(IERC20 asset, bool inElShares) external view returns (uint256) { - IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); uint256 totalBalance = 0; for (uint256 i = 0; i < nodes.length; i++) { - totalBalance += _getWithdrawableAssetBalanceNode(asset, nodes[i], inElShares); + totalBalance += LTMBalances.getWithdrawableBalance( + strategy, + address(nodes[i]), + inElShares, + delegationManager + ); } return totalBalance; @@ -1317,52 +1234,44 @@ contract LiquidTokenManager is bool inElShares ) public view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); - return _getWithdrawableAssetBalanceNode(asset, node, inElShares); + return + LTMBalances.getWithdrawableBalance( + strategy, + address(node), + inElShares, + delegationManager + ); } - /// @dev Called by `getWithdrawableAssetBalance` and `getWithdrawableAssetBalanceNode` - function _getWithdrawableAssetBalanceNode( + /// @inheritdoc ILiquidTokenManager + function getWithdrawableAssetAmount( IERC20 asset, - IStakerNode node, + uint256 amount, bool inElShares - ) internal view returns (uint256) { - IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } - - IStrategy[] memory strategies = new IStrategy[](1); - strategies[0] = strategy; - - (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares(address(node), strategies); - - if (withdrawableShares[0] == 0) { - return 0; - } - - return inElShares ? withdrawableShares[0] : strategy.sharesToUnderlyingView(withdrawableShares[0]); - } - - /// @inheritdoc ILiquidTokenManager - function getWithdrawableAssetAmount(IERC20 asset, uint256 amount, bool inElShares) external view returns (uint256) { + ) external view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); uint256 totalDepositBalance = 0; uint256 totalWithdrawableBalance = 0; for (uint256 i = 0; i < nodes.length; i++) { - totalDepositBalance += _getDepositAssetBalanceNode(asset, nodes[i], inElShares); - totalWithdrawableBalance += _getWithdrawableAssetBalanceNode(asset, nodes[i], inElShares); + totalDepositBalance += LTMBalances.getDepositBalance( + strategy, + address(nodes[i]), + inElShares + ); + totalWithdrawableBalance += LTMBalances.getWithdrawableBalance( + strategy, + address(nodes[i]), + inElShares, + delegationManager + ); } if (totalDepositBalance == 0 || totalWithdrawableBalance == 0) return 0; @@ -1376,44 +1285,54 @@ contract LiquidTokenManager is } /// @inheritdoc ILiquidTokenManager - function convertToUnitOfAccount(IERC20 token, uint256 amount) external view returns (uint256) { + function convertToUnitOfAccount( + IERC20 token, + uint256 amount + ) external view returns (uint256) { TokenInfo memory info = tokens[token]; - if (info.decimals == 0) revert TokenNotSupported(token); + if (info.decimals == 0) revert LTMValidation.E07(); // TokenNotSupported return amount.mulDiv(info.pricePerUnit, 10 ** info.decimals); } /// @inheritdoc ILiquidTokenManager - function convertFromUnitOfAccount(IERC20 token, uint256 amount) external view returns (uint256) { + function convertFromUnitOfAccount( + IERC20 token, + uint256 amount + ) external view returns (uint256) { TokenInfo memory info = tokens[token]; - if (info.decimals == 0) revert TokenNotSupported(token); + if (info.decimals == 0) revert LTMValidation.E07(); // TokenNotSupported return amount.mulDiv(10 ** info.decimals, info.pricePerUnit); } /// @inheritdoc ILiquidTokenManager - function isStrategySupported(IStrategy strategy) external view returns (bool) { + function isStrategySupported( + IStrategy strategy + ) external view returns (bool) { if (address(strategy) == address(0)) return false; return address(strategyTokens[strategy]) != address(0); } /// @inheritdoc ILiquidTokenManager - function assetSharesToUnderlying(IERC20 asset, uint256 amount) external view returns (uint256) { + function assetSharesToUnderlying( + IERC20 asset, + uint256 amount + ) external view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); return strategy.sharesToUnderlyingView(amount); } /// @inheritdoc ILiquidTokenManager - function assetUnderlyingToShares(IERC20 asset, uint256 amount) external view returns (uint256) { + function assetUnderlyingToShares( + IERC20 asset, + uint256 amount + ) external view returns (uint256) { IStrategy strategy = tokenStrategies[asset]; - if (address(strategy) == address(0)) { - revert StrategyNotFound(address(asset)); - } + address(strategy).validateStrategy(); return strategy.underlyingToSharesView(amount); } -} +} \ No newline at end of file diff --git a/src/interfaces/ILiquidTokenManager.sol b/src/interfaces/ILiquidTokenManager.sol index a525b8cb..f420dd57 100644 --- a/src/interfaces/ILiquidTokenManager.sol +++ b/src/interfaces/ILiquidTokenManager.sol @@ -29,7 +29,6 @@ interface ILiquidTokenManager { IStakerNodeCoordinator stakerNodeCoordinator; ITokenRegistryOracle tokenRegistryOracle; IWithdrawalManager withdrawalManager; - ILSTSwapRouter lstSwapRouter; address initialOwner; address strategyController; address priceUpdater; @@ -145,28 +144,6 @@ interface ILiquidTokenManager { /// @notice Emitted when a token is removed from the registry event TokenRemoved(IERC20 indexed token, address indexed remover); - /// @notice Emitted when LSTSwapRouter contract is updated - event LSTSwapRouterUpdated(address indexed oldLsr, address indexed newLsr, address updatedBy); - - /// @notice Emitted when assets are swapped and staked to a node - event AssetsSwappedAndStakedToNode( - uint256 indexed nodeId, - IERC20[] assetsSwapped, - uint256[] amountsSwapped, - IERC20[] assetsStaked, - uint256[] amountsStaked, - address indexed initiator - ); - - /// @notice Emitted when a swap is executed - event SwapExecuted( - address indexed tokenIn, - address indexed tokenOut, - uint256 amountIn, - uint256 amountOut, - uint256 indexed nodeId - ); - /// @notice Emitted when a redemption is created due to node undelegation /// @dev `assets` is a 1D array since each withdrawal will have only 1 corresponding asset event RedemptionCreatedForNodeUndelegation( @@ -290,10 +267,6 @@ interface ILiquidTokenManager { /// @param init Initialization parameters function initialize(Init memory init) external; - /// @notice Updates the LSTSwapRouter contract address - /// @param newLSTSwapRouter The new LSR contract address - function updateLSTSwapRouter(address newLSTSwapRouter) external; - /// @notice Adds a new token to the registry and configures its price sources /// @param token Address of the token to add /// @param decimals Number of decimals for the token @@ -352,22 +325,6 @@ interface ILiquidTokenManager { /// @param allocations Array of NodeAllocation structs containing staking information function stakeAssetsToNodes(NodeAllocation[] calldata allocations) external; - /// @notice Swaps multiple assets and stakes them to multiple nodes - /// @param allocationsWithSwaps Array of node allocations with swap instructions - function swapAndStakeAssetsToNodes(NodeAllocationWithSwap[] calldata allocationsWithSwaps) external; - - /// @notice Swaps assets and stakes them to a single node - /// @param nodeId The node ID to stake to - /// @param assetsToSwap Array of input tokens to swap from - /// @param amountsToSwap Array of amounts to swap - /// @param assetsToStake Array of output tokens to receive and stake - function swapAndStakeAssetsToNode( - uint256 nodeId, - IERC20[] memory assetsToSwap, - uint256[] memory amountsToSwap, - IERC20[] memory assetsToStake - ) external; - /// @notice Undelegates a set of staker nodes from their operators and creates a set of redemptions /// @dev A separate redemption is created for each node, since undelegating a node on EL queues one withdrawal per strategy /// @dev On completing a redemption created from undelegation, the funds are transferred to `LiquidToken` @@ -530,8 +487,4 @@ interface ILiquidTokenManager { /// @notice Returns the LiquidToken contract /// @return The ILiquidToken interface function liquidToken() external view returns (ILiquidToken); - - /// @notice Returns the LSTSwapRouter contract - /// @return The ILSTSwapRouter interface - function lstSwapRouter() external view returns (ILSTSwapRouter); -} +} \ No newline at end of file diff --git a/src/libraries/LTMBalances.sol b/src/libraries/LTMBalances.sol new file mode 100644 index 00000000..40c454dd --- /dev/null +++ b/src/libraries/LTMBalances.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStakerNode} from "../interfaces/IStakerNode.sol"; +import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; + +library LTMBalances { + using Math for uint256; + + function getDepositBalance( + IERC20 asset, + IStakerNode node, + bool inElShares, + mapping(IERC20 => IStrategy) storage tokenStrategies + ) external view returns (uint256) { + IStrategy strategy = tokenStrategies[asset]; + require(address(strategy) != address(0), "StrategyNotFound"); + + return inElShares ? strategy.shares(address(node)) : strategy.userUnderlyingView(address(node)); + } + + function getWithdrawableBalance( + IERC20 asset, + IStakerNode node, + bool inElShares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IDelegationManager delegationManager + ) external view returns (uint256) { + IStrategy strategy = tokenStrategies[asset]; + require(address(strategy) != address(0), "StrategyNotFound"); + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy; + + (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares(address(node), strategies); + + if (withdrawableShares[0] == 0) return 0; + + return inElShares ? withdrawableShares[0] : strategy.sharesToUnderlyingView(withdrawableShares[0]); + } + + function getAllDepositBalances( + IERC20 asset, + bool inElShares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IStakerNodeCoordinator stakerNodeCoordinator + ) external view returns (uint256 totalBalance) { + IStrategy strategy = tokenStrategies[asset]; + require(address(strategy) != address(0), "StrategyNotFound"); + + IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); + totalBalance = 0; + + for (uint256 i = 0; i < nodes.length; i++) { + totalBalance += inElShares + ? strategy.shares(address(nodes[i])) + : strategy.userUnderlyingView(address(nodes[i])); + } + } + + function getAllWithdrawableBalances( + IERC20 asset, + bool inElShares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IStakerNodeCoordinator stakerNodeCoordinator, + IDelegationManager delegationManager + ) external view returns (uint256 totalBalance) { + IStrategy strategy = tokenStrategies[asset]; + require(address(strategy) != address(0), "StrategyNotFound"); + + IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); + totalBalance = 0; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy; + + for (uint256 i = 0; i < nodes.length; i++) { + (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares( + address(nodes[i]), + strategies + ); + + if (withdrawableShares[0] > 0) { + totalBalance += inElShares + ? withdrawableShares[0] + : strategy.sharesToUnderlyingView(withdrawableShares[0]); + } + } + } + + function getWithdrawableAmount( + IERC20 asset, + uint256 amount, + bool inElShares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IStakerNodeCoordinator stakerNodeCoordinator, + IDelegationManager delegationManager + ) external view returns (uint256) { + // Direct implementation instead of calling other functions + IStrategy strategy = tokenStrategies[asset]; + require(address(strategy) != address(0), "StrategyNotFound"); + + IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); + uint256 totalDepositBalance = 0; + uint256 totalWithdrawableBalance = 0; + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy; + + // Calculate total deposit balance and withdrawable balance in one loop + for (uint256 i = 0; i < nodes.length; i++) { + address nodeAddress = address(nodes[i]); + + // Add deposit balance + totalDepositBalance += inElShares ? strategy.shares(nodeAddress) : strategy.userUnderlyingView(nodeAddress); + + // Add withdrawable balance + (uint256[] memory withdrawableShares, ) = delegationManager.getWithdrawableShares(nodeAddress, strategies); + + if (withdrawableShares[0] > 0) { + totalWithdrawableBalance += inElShares + ? withdrawableShares[0] + : strategy.sharesToUnderlyingView(withdrawableShares[0]); + } + } + + if (totalDepositBalance == 0 || totalWithdrawableBalance == 0) return 0; + + return amount.mulDiv(totalWithdrawableBalance, totalDepositBalance); + } +} \ No newline at end of file diff --git a/src/libraries/LTMCore.sol b/src/libraries/LTMCore.sol new file mode 100644 index 00000000..efeb6a60 --- /dev/null +++ b/src/libraries/LTMCore.sol @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {ISignatureUtilsMixinTypes} from "eigenlayer-contracts/src/contracts/interfaces/ISignatureUtilsMixin.sol"; +import {ILiquidToken} from "../interfaces/ILiquidToken.sol"; +import {IStakerNode} from "../interfaces/IStakerNode.sol"; +import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; +import {ITokenRegistryOracle} from "../interfaces/ITokenRegistryOracle.sol"; +import {ILiquidTokenManager} from "../interfaces/ILiquidTokenManager.sol"; +import {LTMValidation} from "./LTMValidation.sol"; +import {LTMBalances} from "./LTMBalances.sol"; + +library LTMCore { + using SafeERC20 for IERC20; + using LTMValidation for uint256; + using LTMValidation for address; + + event TokenAdded( + IERC20 indexed token, + uint8 decimals, + uint256 pricePerUnit, + uint256 volatilityThreshold, + address strategy, + address indexed caller + ); + + event TokenRemoved(IERC20 indexed token, address indexed caller); + + event TokenPriceUpdated( + IERC20 indexed token, + uint256 oldPrice, + uint256 newPrice, + address indexed caller + ); + + event VolatilityCheckFailed( + IERC20 indexed token, + uint256 oldPrice, + uint256 newPrice, + uint256 changeRatio + ); + + event VolatilityThresholdUpdated( + IERC20 indexed asset, + uint256 oldThreshold, + uint256 newThreshold, + address indexed caller + ); + + event AssetsStakedToNode( + uint256 indexed nodeId, + IERC20[] assets, + uint256[] amounts, + address indexed caller + ); + + event AssetsDepositedToEigenlayer( + IERC20[] assets, + uint256[] amounts, + IStrategy[] strategies, + address indexed node + ); + + event NodeDelegated(uint256 indexed nodeId, address indexed operator); + + struct TokenAdditionParams { + IERC20 token; + uint8 decimals; + uint256 volatilityThreshold; + IStrategy strategy; + uint8 primaryType; + address primarySource; + uint8 needsArg; + address fallbackSource; + bytes4 fallbackFn; + } + + function processTokenAddition( + TokenAdditionParams calldata params, + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens, + mapping(IERC20 => IStrategy) storage tokenStrategies, + mapping(IStrategy => IERC20) storage strategyTokens, + IERC20[] storage supportedTokens, + ITokenRegistryOracle tokenRegistryOracle + ) external { + if (address(tokenStrategies[params.token]) != address(0)) + revert LTMValidation.E12(); // TokenExists + address(params.token).validateNotZero(); + if (params.decimals == 0) revert LTMValidation.E04(); // InvalidDecimals + if ( + params.volatilityThreshold != 0 && + (params.volatilityThreshold < 1e16 || + params.volatilityThreshold > 1e18) + ) revert LTMValidation.E05(); // InvalidThreshold + address(params.strategy).validateNotZero(); + if (address(strategyTokens[params.strategy]) != address(0)) { + revert LTMValidation.E13(); // StrategyAlreadyAssigned + } + + // Price source validation and configuration + bool isNative = (params.primaryType == 0 && + params.primarySource == address(0)); + if (!isNative && (params.primaryType < 1 || params.primaryType > 5)) + revert LTMValidation.E14(); // InvalidPriceSource + if (!isNative && params.primarySource == address(0)) + revert LTMValidation.E14(); // InvalidPriceSource + if (!isNative) { + tokenRegistryOracle.configureToken( + address(params.token), + params.primaryType, + params.primarySource, + params.needsArg, + params.fallbackSource, + params.fallbackFn + ); + } + + try IERC20Metadata(address(params.token)).decimals() returns ( + uint8 decimalsFromContract + ) { + if (decimalsFromContract == 0) revert LTMValidation.E04(); // InvalidDecimals + if (params.decimals != decimalsFromContract) + revert LTMValidation.E04(); // InvalidDecimals + } catch {} // Fallback to `decimals` if token contract doesn't implement `decimals()` + + uint256 fetchedPrice; + if (!isNative) { + (uint256 price, bool ok) = tokenRegistryOracle + ._getTokenPrice_getter(address(params.token)); + if (!ok || price == 0) revert LTMValidation.E15(); // TokenPriceFetchFailed + fetchedPrice = price; + } else { + fetchedPrice = 1e18; + } + + tokens[params.token] = ILiquidTokenManager.TokenInfo({ + decimals: params.decimals, + pricePerUnit: fetchedPrice, + volatilityThreshold: params.volatilityThreshold + }); + tokenStrategies[params.token] = params.strategy; + strategyTokens[params.strategy] = params.token; + supportedTokens.push(params.token); + + emit TokenAdded( + params.token, + params.decimals, + fetchedPrice, + params.volatilityThreshold, + address(params.strategy), + msg.sender + ); + } + + function processTokenRemoval( + IERC20 token, + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens, + mapping(IERC20 => IStrategy) storage tokenStrategies, + mapping(IStrategy => IERC20) storage strategyTokens, + IERC20[] storage supportedTokens, + ILiquidToken liquidToken, + IStakerNodeCoordinator stakerNodeCoordinator, + ITokenRegistryOracle tokenRegistryOracle + ) external { + ILiquidTokenManager.TokenInfo memory info = tokens[token]; + if (info.decimals == 0) revert LTMValidation.E07(); // TokenNotSupported + + IERC20[] memory assets = new IERC20[](1); + assets[0] = token; + + if (liquidToken.balanceAssets(assets)[0] > 0) + revert LTMValidation.E16(); // TokenInUse + + if (liquidToken.balanceQueuedAssets(assets)[0] > 0) + revert LTMValidation.E16(); // TokenInUse + + IStakerNode[] memory nodes = stakerNodeCoordinator.getAllNodes(); + uint256 len = nodes.length; + + unchecked { + for (uint256 i = 0; i < len; i++) { + IStrategy strategy = tokenStrategies[token]; + // Fixed: Use delegationManager instead of getDelegationManager() + uint256 stakedWithdrawableBalance = LTMBalances + .getWithdrawableBalance( + strategy, + address(nodes[i]), + true, + stakerNodeCoordinator.delegationManager() + ); + if (stakedWithdrawableBalance > 0) { + revert LTMValidation.E16(); // TokenInUse + } + } + } + + uint256 tokenCount = supportedTokens.length; + for (uint256 i = 0; i < tokenCount; i++) { + if (supportedTokens[i] == token) { + supportedTokens[i] = supportedTokens[tokenCount - 1]; + supportedTokens.pop(); + break; + } + } + + tokenRegistryOracle.removeToken(address(token)); + + IStrategy strategy = tokenStrategies[token]; + if (address(strategy) != address(0)) { + delete strategyTokens[strategy]; + } + delete tokenStrategies[token]; + delete tokens[token]; + + emit TokenRemoved(token, msg.sender); + } + + function processPriceUpdate( + IERC20 token, + uint256 newPrice, + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens + ) external { + if (tokens[token].decimals == 0) revert LTMValidation.E07(); // TokenNotSupported + if (newPrice == 0) revert LTMValidation.E06(); // InvalidPrice + + uint256 oldPrice = tokens[token].pricePerUnit; + if (oldPrice == 0) revert LTMValidation.E06(); // InvalidPrice + + if (tokens[token].volatilityThreshold != 0) { + uint256 absPriceDiff = (newPrice > oldPrice) + ? newPrice - oldPrice + : oldPrice - newPrice; + uint256 changeRatio = (absPriceDiff * 1e18) / oldPrice; + + if (changeRatio > tokens[token].volatilityThreshold) { + emit VolatilityCheckFailed( + token, + oldPrice, + newPrice, + changeRatio + ); + revert LTMValidation.E17(); // VolatilityThresholdHit + } + } + + tokens[token].pricePerUnit = newPrice; + emit TokenPriceUpdated(token, oldPrice, newPrice, msg.sender); + } + + function processVolatilityThresholdUpdate( + IERC20 asset, + uint256 newThreshold, + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens + ) external { + address(asset).validateNotZero(); + if (tokens[asset].decimals == 0) revert LTMValidation.E07(); // TokenNotSupported + if (newThreshold != 0 && (newThreshold < 1e16 || newThreshold > 1e18)) + revert LTMValidation.E05(); // InvalidThreshold + + emit VolatilityThresholdUpdated( + asset, + tokens[asset].volatilityThreshold, + newThreshold, + msg.sender + ); + + tokens[asset].volatilityThreshold = newThreshold; + } + + function processDelegateNodes( + uint256[] calldata nodeIds, + address[] calldata operators, + ISignatureUtilsMixinTypes.SignatureWithExpiry[] + calldata approverSignatureAndExpiries, + bytes32[] calldata approverSalts, + IStakerNodeCoordinator stakerNodeCoordinator + ) external { + uint256 arrayLength = nodeIds.length; + + operators.length.validateLengths(arrayLength); + approverSignatureAndExpiries.length.validateLengths(arrayLength); + approverSalts.length.validateLengths(arrayLength); + + for (uint256 i = 0; i < arrayLength; i++) { + IStakerNode node = stakerNodeCoordinator.getNodeById((nodeIds[i])); + node.delegate( + operators[i], + approverSignatureAndExpiries[i], + approverSalts[i] + ); + emit NodeDelegated(nodeIds[i], operators[i]); + } + } + + function processStakingToNode( + uint256 nodeId, + IERC20[] memory assets, + uint256[] memory amounts, + mapping(IERC20 => IStrategy) storage tokenStrategies, + ILiquidToken liquidToken, + IStakerNodeCoordinator stakerNodeCoordinator + ) external { + uint256 assetsLength = assets.length; + uint256 amountsLength = amounts.length; + + assetsLength.validateLengths(amountsLength); + + IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); + + IStrategy[] memory strategiesForNode = new IStrategy[](assetsLength); + for (uint256 i = 0; i < assetsLength; i++) { + IERC20 asset = assets[i]; + if (amounts[i] == 0) { + revert LTMValidation.E08(); // InvalidStakingAmount + } + IStrategy strategy = tokenStrategies[asset]; + address(strategy).validateStrategy(); + strategiesForNode[i] = strategy; + } + + liquidToken.transferAssets(assets, amounts, address(this)); + + IERC20[] memory depositAssets = new IERC20[](assetsLength); + uint256[] memory depositAmounts = new uint256[](amountsLength); + + for (uint256 i = 0; i < assetsLength; i++) { + depositAssets[i] = assets[i]; + uint256 balance = assets[i].balanceOf(address(this)); + depositAmounts[i] = balance < amounts[i] ? balance : amounts[i]; + + assets[i].safeTransfer(address(node), depositAmounts[i]); + } + + emit AssetsStakedToNode( + nodeId, + depositAssets, + depositAmounts, + msg.sender + ); + + node.depositAssets(depositAssets, depositAmounts, strategiesForNode); + + emit AssetsDepositedToEigenlayer( + depositAssets, + depositAmounts, + strategiesForNode, + address(node) + ); + } +} \ No newline at end of file diff --git a/src/libraries/LTMGetters.sol b/src/libraries/LTMGetters.sol new file mode 100644 index 00000000..0bcdda63 --- /dev/null +++ b/src/libraries/LTMGetters.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {ILiquidTokenManager} from "../interfaces/ILiquidTokenManager.sol"; + +library LTMGetters { + using Math for uint256; + + function getSupportedTokens( + IERC20[] storage supportedTokens + ) internal pure returns (IERC20[] memory) { + return supportedTokens; + } + + function getTokenInfo( + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens, + IERC20 token + ) internal view returns (ILiquidTokenManager.TokenInfo memory) { + if (address(token) == address(0)) revert("Zero address"); + + ILiquidTokenManager.TokenInfo memory tokenInfo = tokens[token]; + + if (tokenInfo.decimals == 0) { + revert("Token not supported"); + } + + return tokenInfo; + } + + function getTokenStrategy( + mapping(IERC20 => IStrategy) storage tokenStrategies, + IERC20 asset + ) internal view returns (IStrategy) { + if (address(asset) == address(0)) revert("Zero address"); + + IStrategy strategy = tokenStrategies[asset]; + + if (address(strategy) == address(0)) revert("Strategy not found"); + + return strategy; + } + + function getStrategyToken( + mapping(IStrategy => IERC20) storage strategyTokens, + IStrategy strategy + ) internal view returns (IERC20) { + if (address(strategy) == address(0)) revert("Zero address"); + + IERC20 token = strategyTokens[strategy]; + + if (address(token) == address(0)) { + revert("Token for strategy not found"); + } + + return token; + } + + function tokenIsSupported( + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens, + IERC20 token + ) internal view returns (bool) { + return tokens[token].decimals != 0; + } + + function convertToUnitOfAccount( + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens, + IERC20 token, + uint256 amount + ) internal view returns (uint256) { + ILiquidTokenManager.TokenInfo memory info = tokens[token]; + if (info.decimals == 0) revert("Token not supported"); + + return amount.mulDiv(info.pricePerUnit, 10 ** info.decimals); + } + + function convertFromUnitOfAccount( + mapping(IERC20 => ILiquidTokenManager.TokenInfo) storage tokens, + IERC20 token, + uint256 amount + ) internal view returns (uint256) { + ILiquidTokenManager.TokenInfo memory info = tokens[token]; + if (info.decimals == 0) revert("Token not supported"); + + return amount.mulDiv(10 ** info.decimals, info.pricePerUnit); + } + + function isStrategySupported( + mapping(IStrategy => IERC20) storage strategyTokens, + IStrategy strategy + ) internal view returns (bool) { + if (address(strategy) == address(0)) return false; + return address(strategyTokens[strategy]) != address(0); + } + + function assetSharesToUnderlying( + mapping(IERC20 => IStrategy) storage tokenStrategies, + IERC20 asset, + uint256 amount + ) internal view returns (uint256) { + IStrategy strategy = tokenStrategies[asset]; + if (address(strategy) == address(0)) revert("Strategy not found"); + + return strategy.sharesToUnderlyingView(amount); + } + + function assetUnderlyingToShares( + mapping(IERC20 => IStrategy) storage tokenStrategies, + IERC20 asset, + uint256 amount + ) internal view returns (uint256) { + IStrategy strategy = tokenStrategies[asset]; + if (address(strategy) == address(0)) revert("Strategy not found"); + + return strategy.underlyingToSharesView(amount); + } +} \ No newline at end of file diff --git a/src/libraries/LTMHelpers.sol b/src/libraries/LTMHelpers.sol new file mode 100644 index 00000000..a99f2430 --- /dev/null +++ b/src/libraries/LTMHelpers.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStakerNode} from "../interfaces/IStakerNode.sol"; +import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +library LTMHelpers { + using Math for uint256; + + function scaleSharesForNode( + uint256 nodeId, + IERC20[] memory assets, + uint256[] memory shares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IDelegationManager delegationManager, + IStakerNodeCoordinator stakerNodeCoordinator + ) internal view returns (uint256[] memory) { + address nodeAddress = address( + stakerNodeCoordinator.getNodeById(nodeId) + ); + uint256[] memory scaledShares = new uint256[](assets.length); + + for (uint256 i = 0; i < assets.length; i++) { + scaledShares[i] = shares[i].mulDiv( + delegationManager.depositScalingFactor( + nodeAddress, + tokenStrategies[assets[i]] + ), + 1e18 + ); + } + + return scaledShares; + } + + function scaleSharesForNodeAsset( + uint256 nodeId, + IERC20 asset, + uint256 shares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IDelegationManager delegationManager, + IStakerNodeCoordinator stakerNodeCoordinator + ) internal view returns (uint256) { + address nodeAddress = address( + stakerNodeCoordinator.getNodeById(nodeId) + ); + + return + shares.mulDiv( + delegationManager.depositScalingFactor( + nodeAddress, + tokenStrategies[asset] + ), + 1e18 + ); + } +} \ No newline at end of file diff --git a/src/libraries/LTMRedemption.sol b/src/libraries/LTMRedemption.sol new file mode 100644 index 00000000..49a79fa4 --- /dev/null +++ b/src/libraries/LTMRedemption.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IWithdrawalManager} from "../interfaces/IWithdrawalManager.sol"; +import {ILiquidTokenManager} from "../interfaces/ILiquidTokenManager.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; + +library LTMRedemption { + using SafeERC20 for IERC20; + + function createRedemption( + bytes32[] memory requestIds, + bytes32[] memory withdrawalRoots, + IERC20[] memory assets, + uint256[] memory elWithdrawableShares, + address receiver, + uint256 redemptionNonce, + IWithdrawalManager withdrawalManager + ) external returns (bytes32 redemptionId) { + redemptionId = keccak256( + abi.encode( + requestIds, + withdrawalRoots, + block.timestamp, + redemptionNonce + ) + ); + + ILiquidTokenManager.Redemption memory redemption = ILiquidTokenManager + .Redemption({ + requestIds: requestIds, + withdrawalRoots: withdrawalRoots, + assets: assets, + elWithdrawableShares: elWithdrawableShares, + receiver: receiver + }); + + withdrawalManager.recordRedemptionCreated(redemptionId, redemption); + } + + function validateRedemption( + bytes32[] memory redemptionWithdrawalRoots, + bytes32[][] calldata withdrawalHashes + ) external pure { + uint256 totalWithdrawals = 0; + for (uint256 j = 0; j < withdrawalHashes.length; j++) { + totalWithdrawals += withdrawalHashes[j].length; + } + + bytes32[] memory allWithdrawalHashes = new bytes32[](totalWithdrawals); + uint256 index = 0; + for (uint256 j = 0; j < withdrawalHashes.length; j++) { + for (uint256 k = 0; k < withdrawalHashes[j].length; k++) { + allWithdrawalHashes[index++] = withdrawalHashes[j][k]; + } + } + + for (uint256 i = 0; i < redemptionWithdrawalRoots.length; i++) { + bool found = false; + for (uint256 h = 0; h < allWithdrawalHashes.length; h++) { + if (allWithdrawalHashes[h] == redemptionWithdrawalRoots[i]) { + found = true; + break; + } + } + require(found, "WithdrawalMissing"); + } + } + + function processAssetTransfers( + IERC20[] memory tokens, + address receiver, + mapping(IERC20 => IStrategy) storage tokenStrategies + ) + external + returns ( + uint256[] memory receivedAmounts, + uint256[] memory receivedElShares + ) + { + uint256 length = tokens.length; + receivedAmounts = new uint256[](length); + receivedElShares = new uint256[](length); + + for (uint256 i = 0; i < length; i++) { + IERC20 token = tokens[i]; + uint256 balance = token.balanceOf(address(this)); + + if (balance > 0) { + uint256 receiverBalanceBefore = token.balanceOf(receiver); + token.safeTransfer(receiver, balance); + uint256 receiverBalanceAfter = token.balanceOf(receiver); + + receivedAmounts[i] = + receiverBalanceAfter - + receiverBalanceBefore; + receivedElShares[i] = tokenStrategies[token] + .underlyingToSharesView(receivedAmounts[i]); + } + } + } +} \ No newline at end of file diff --git a/src/libraries/LTMSettlement.sol b/src/libraries/LTMSettlement.sol new file mode 100644 index 00000000..9c518de7 --- /dev/null +++ b/src/libraries/LTMSettlement.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IWithdrawalManager} from "../interfaces/IWithdrawalManager.sol"; + +library LTMSettlement { + using Math for uint256; + + function validateSettlement( + bytes32[] calldata requestIds, + uint256[] calldata nodeIds, + IERC20[][] calldata elAssets, + uint256[][] calldata elDepositShares + ) external pure { + require(elAssets.length == nodeIds.length, "LengthMismatch"); + require(elDepositShares.length == nodeIds.length, "LengthMismatch"); + } + + function processWithdrawalRequests( + IWithdrawalManager.WithdrawalRequest[] memory withdrawalRequests, + uint256 maxTokens + ) external pure returns ( + uint256 uniqueTokenCount, + IERC20[] memory redemptionAssets, + uint256[] memory redemptionElDepositShares + ) { + redemptionAssets = new IERC20[](maxTokens); + redemptionElDepositShares = new uint256[](maxTokens); + uniqueTokenCount = 0; + + for (uint256 i = 0; i < withdrawalRequests.length; i++) { + IWithdrawalManager.WithdrawalRequest memory request = withdrawalRequests[i]; + for (uint256 j = 0; j < request.assets.length; j++) { + IERC20 token = request.assets[j]; + bool found = false; + for (uint256 k = 0; k < uniqueTokenCount; k++) { + if (redemptionAssets[k] == token) { + redemptionElDepositShares[k] += request.elWithdrawableShares[j]; + found = true; + break; + } + } + if (!found) { + redemptionAssets[uniqueTokenCount] = token; + redemptionElDepositShares[uniqueTokenCount] = request.elWithdrawableShares[j]; + uniqueTokenCount++; + } + } + } + } + + function verifySettlementAmounts( + uint256[] memory redemptionElDepositShares, + uint256[] memory proposedRedemptionElDepositShares, + uint256 uniqueTokenCount + ) external pure { + for (uint256 i = 0; i < uniqueTokenCount; i++) { + uint256 upperMargin = Math.mulDiv(redemptionElDepositShares[i], 10, 10000, Math.Rounding.Up); + uint256 lowerMargin = Math.mulDiv(redemptionElDepositShares[i], 10, 10000, Math.Rounding.Down); + + uint256 maxAllowed = redemptionElDepositShares[i] + upperMargin; + uint256 minAllowed = redemptionElDepositShares[i] - lowerMargin; + + require( + proposedRedemptionElDepositShares[i] <= maxAllowed && + proposedRedemptionElDepositShares[i] >= minAllowed, + "RequestsDoNotSettle" + ); + } + } +} \ No newline at end of file diff --git a/src/libraries/LTMStaking.sol b/src/libraries/LTMStaking.sol new file mode 100644 index 00000000..7436c247 --- /dev/null +++ b/src/libraries/LTMStaking.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IStakerNode} from "../interfaces/IStakerNode.sol"; +import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; +import {ILiquidToken} from "../interfaces/ILiquidToken.sol"; + +library LTMStaking { + using SafeERC20 for IERC20; + + function validateStakingInputs( + IERC20[] memory assets, + uint256[] memory amounts, + mapping(IERC20 => IStrategy) storage tokenStrategies + ) external view returns (IStrategy[] memory strategies) { + require(assets.length == amounts.length, "LengthMismatch"); + + strategies = new IStrategy[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + require(amounts[i] != 0, "InvalidStakingAmount"); + IStrategy strategy = tokenStrategies[assets[i]]; + require(address(strategy) != address(0), "StrategyNotFound"); + strategies[i] = strategy; + } + } + + function transferAssetsFromLiquidToken( + IERC20[] memory assets, + uint256[] memory amounts, + ILiquidToken liquidToken + ) external { + liquidToken.transferAssets(assets, amounts, address(this)); + } + + function transferAssetsToNode( + IERC20[] memory assets, + uint256[] memory amounts, + address nodeAddress + ) external returns (uint256[] memory actualAmounts) { + actualAmounts = new uint256[](assets.length); + + for (uint256 i = 0; i < assets.length; i++) { + uint256 balance = assets[i].balanceOf(address(this)); + actualAmounts[i] = balance < amounts[i] ? balance : amounts[i]; + assets[i].safeTransfer(nodeAddress, actualAmounts[i]); + } + } + + function executeNodeDeposit( + IStakerNode node, + IERC20[] memory assets, + uint256[] memory amounts, + IStrategy[] memory strategies + ) external { + node.depositAssets(assets, amounts, strategies); + } +} \ No newline at end of file diff --git a/src/libraries/LTMTokenOps.sol b/src/libraries/LTMTokenOps.sol new file mode 100644 index 00000000..9cacc27a --- /dev/null +++ b/src/libraries/LTMTokenOps.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {ITokenRegistryOracle} from "../interfaces/ITokenRegistryOracle.sol"; +import {ILiquidToken} from "../interfaces/ILiquidToken.sol"; +import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; + +library LTMTokenOps { + struct TokenInfo { + uint8 decimals; + uint256 pricePerUnit; + uint256 volatilityThreshold; + } + + function addTokenValidation( + IERC20 token, + uint8 decimals, + uint256 volatilityThreshold, + IStrategy strategy, + mapping(IERC20 => IStrategy) storage tokenStrategies, + mapping(IStrategy => IERC20) storage strategyTokens + ) external view { + require(address(tokenStrategies[token]) == address(0), "TokenExists"); + require(address(token) != address(0), "ZeroAddress"); + require(decimals != 0, "InvalidDecimals"); + require( + volatilityThreshold == 0 || + (volatilityThreshold >= 1e16 && volatilityThreshold <= 1e18), + "InvalidThreshold" + ); + require(address(strategy) != address(0), "ZeroAddress"); + require( + address(strategyTokens[strategy]) == address(0), + "StrategyAlreadyAssigned" + ); + } + + function configurePriceSource( + IERC20 token, + uint8 primaryType, + address primarySource, + uint8 needsArg, + address fallbackSource, + bytes4 fallbackFn, + ITokenRegistryOracle tokenRegistryOracle + ) external returns (uint256 fetchedPrice) { + bool isNative = (primaryType == 0 && primarySource == address(0)); + + if (!isNative) { + require(primaryType >= 1 && primaryType <= 3, "InvalidPriceSource"); + require(primarySource != address(0), "InvalidPriceSource"); + + tokenRegistryOracle.configureToken( + address(token), + primaryType, + primarySource, + needsArg, + fallbackSource, + fallbackFn + ); + + (uint256 price, bool ok) = tokenRegistryOracle + ._getTokenPrice_getter(address(token)); + require(ok && price != 0, "TokenPriceFetchFailed"); + fetchedPrice = price; + } else { + fetchedPrice = 1e18; + } + } + + function validateDecimals( + IERC20 token, + uint8 expectedDecimals + ) external view { + try IERC20Metadata(address(token)).decimals() returns ( + uint8 decimalsFromContract + ) { + require( + decimalsFromContract != 0 && + decimalsFromContract == expectedDecimals, + "InvalidDecimals" + ); + } catch {} + } + + function executeTokenAddition( + IERC20 token, + TokenInfo memory info, + IStrategy strategy, + mapping(IERC20 => TokenInfo) storage tokens, + mapping(IERC20 => IStrategy) storage tokenStrategies, + mapping(IStrategy => IERC20) storage strategyTokens, + IERC20[] storage supportedTokens + ) external { + tokens[token] = info; + tokenStrategies[token] = strategy; + strategyTokens[strategy] = token; + supportedTokens.push(token); + } + + function validatePriceUpdate( + IERC20 token, + uint256 newPrice, + mapping(IERC20 => TokenInfo) storage tokens + ) external view returns (uint256 oldPrice, uint256 changeRatio) { + TokenInfo memory info = tokens[token]; + require(info.decimals != 0, "TokenNotSupported"); + require(newPrice != 0, "InvalidPrice"); + + oldPrice = info.pricePerUnit; + require(oldPrice != 0, "InvalidPrice"); + + if (info.volatilityThreshold != 0) { + uint256 absPriceDiff = (newPrice > oldPrice) + ? newPrice - oldPrice + : oldPrice - newPrice; + changeRatio = (absPriceDiff * 1e18) / oldPrice; + require( + changeRatio <= info.volatilityThreshold, + "VolatilityThresholdHit" + ); + } + } +} \ No newline at end of file diff --git a/src/libraries/LTMValidation.sol b/src/libraries/LTMValidation.sol new file mode 100644 index 00000000..df757255 --- /dev/null +++ b/src/libraries/LTMValidation.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IDelegationManagerTypes} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; + +library LTMValidation { + // Shortened error messages + error E01(); // LengthMismatch + error E02(); // ZeroAddress + error E03(); // ZeroAmount + error E04(); // InvalidDecimals + error E05(); // InvalidThreshold + error E06(); // InvalidPrice + error E07(); // TokenNotSupported + error E08(); // InvalidStakingAmount + error E09(); // StrategyNotFound + error E10(); // WithdrawalMissing + error E11(); // InvalidReceiver + error E12(); // TokenExists + error E13(); // StrategyAlreadyAssigned + error E14(); // InvalidPriceSource + error E15(); // TokenPriceFetchFailed + error E16(); // TokenInUse + error E17(); // VolatilityThresholdHit + error E18(); // InvalidWithdrawalRoot + error E19(); // InsufficientBalance + error E20(); // RequestsDoNotSettle + error E21(); // TokenForStrategyNotFound + + function validateLengths(uint256 len1, uint256 len2) internal pure { + if (len1 != len2) revert E01(); + } + + function validateNotZero(address addr) internal pure { + if (addr == address(0)) revert E02(); + } + + function validateAmount(uint256 amount) internal pure { + if (amount == 0) revert E03(); + } + + function validateStrategy(address strategy) internal pure { + if (strategy == address(0)) revert E09(); + } + + function validateRedemption( + bytes32[] memory redemptionWithdrawalRoots, + IDelegationManagerTypes.Withdrawal[][] calldata withdrawals + ) internal pure { + uint256 totalWithdrawals; + for (uint256 j = 0; j < withdrawals.length; j++) { + totalWithdrawals += withdrawals[j].length; + } + + bytes32[] memory allWithdrawalHashes = new bytes32[](totalWithdrawals); + uint256 index; + for (uint256 j = 0; j < withdrawals.length; j++) { + for (uint256 k = 0; k < withdrawals[j].length; k++) { + allWithdrawalHashes[index++] = keccak256( + abi.encode(withdrawals[j][k]) + ); + } + } + + for (uint256 i = 0; i < redemptionWithdrawalRoots.length; i++) { + bool found; + for (uint256 h = 0; h < allWithdrawalHashes.length; h++) { + if (allWithdrawalHashes[h] == redemptionWithdrawalRoots[i]) { + found = true; + break; + } + } + if (!found) revert E10(); + } + } +} \ No newline at end of file diff --git a/src/libraries/LTMWithdrawal.sol b/src/libraries/LTMWithdrawal.sol new file mode 100644 index 00000000..2df6e464 --- /dev/null +++ b/src/libraries/LTMWithdrawal.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IDelegationManagerTypes} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; +import {IStakerNode} from "../interfaces/IStakerNode.sol"; +import {IStakerNodeCoordinator} from "../interfaces/IStakerNodeCoordinator.sol"; + +library LTMWithdrawal { + using Math for uint256; + + function validateWithdrawalInputs( + uint256[] calldata nodeIds, + IERC20[][] calldata assets, + uint256[][] calldata elDepositShares + ) external pure { + require(assets.length == nodeIds.length, "LengthMismatch"); + require(elDepositShares.length == nodeIds.length, "LengthMismatch"); + } + + function processNodeForWithdrawal( + uint256 nodeId, + IERC20[] calldata assets, + uint256[] calldata elDepositShares, + IERC20[] memory redemptionAssets, + uint256[] memory redemptionElWithdrawableShares, + uint256 currentUniqueTokenCount, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IStakerNodeCoordinator stakerNodeCoordinator, + IDelegationManager delegationManager + ) external view returns (uint256 newUniqueTokenCount) { + uint256 uniqueTokenCount = currentUniqueTokenCount; + IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); + + for (uint256 j = 0; j < assets.length; j++) { + require(elDepositShares[j] != 0, "ZeroAmount"); + + IStrategy strategy = tokenStrategies[assets[j]]; + uint256 depositShares = strategy.shares(address(node)); + require( + depositShares != 0 && depositShares >= elDepositShares[j], + "InsufficientBalance" + ); + + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy; + (uint256[] memory withdrawableShares, ) = delegationManager + .getWithdrawableShares(address(node), strategies); + + bool found = false; + for (uint256 k = 0; k < uniqueTokenCount; k++) { + if (redemptionAssets[k] == assets[j]) { + redemptionElWithdrawableShares[k] += + (elDepositShares[j] * withdrawableShares[0]) / + depositShares; + found = true; + break; + } + } + if (!found) { + redemptionAssets[uniqueTokenCount] = assets[j]; + redemptionElWithdrawableShares[uniqueTokenCount] = + (elDepositShares[j] * withdrawableShares[0]) / + depositShares; + uniqueTokenCount++; + } + } + + return uniqueTokenCount; + } + + function createELWithdrawal( + uint256 nodeId, + IERC20[] memory assets, + uint256[] memory shares, + mapping(IERC20 => IStrategy) storage tokenStrategies, + IStakerNodeCoordinator stakerNodeCoordinator, + IDelegationManager delegationManager + ) + external + returns ( + bytes32 withdrawalRoot, + IDelegationManagerTypes.Withdrawal memory withdrawal + ) + { + require(assets.length == shares.length, "LengthMismatch"); + + IStakerNode node = stakerNodeCoordinator.getNodeById(nodeId); + IStrategy[] memory strategies = new IStrategy[](assets.length); + + for (uint256 i = 0; i < assets.length; i++) { + strategies[i] = tokenStrategies[assets[i]]; + } + + address staker = address(node); + uint256 nonce = delegationManager.cumulativeWithdrawalsQueued(staker); + address delegatedTo = node.getOperatorDelegation(); + + uint256[] memory scaledShares = new uint256[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + scaledShares[i] = shares[i].mulDiv( + delegationManager.depositScalingFactor(staker, strategies[i]), + 1e18 + ); + } + + withdrawal = IDelegationManagerTypes.Withdrawal({ + staker: staker, + delegatedTo: delegatedTo, + withdrawer: staker, + nonce: nonce, + startBlock: uint32(block.number), + strategies: strategies, + scaledShares: scaledShares + }); + + withdrawalRoot = node.withdrawAssets(strategies, shares); + require( + withdrawalRoot == keccak256(abi.encode(withdrawal)), + "InvalidWithdrawalRoot" + ); + } + + function trimArrays( + IERC20[] memory assets, + uint256[] memory shares, + uint256 actualLength + ) external pure { + assembly { + mstore(assets, actualLength) + mstore(shares, actualLength) + } + } +} \ No newline at end of file diff --git a/src/libraries/LTMWithdrawalProcessor.sol b/src/libraries/LTMWithdrawalProcessor.sol new file mode 100644 index 00000000..16966e0b --- /dev/null +++ b/src/libraries/LTMWithdrawalProcessor.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWithdrawalManager} from "../interfaces/IWithdrawalManager.sol"; +import {IStakerNode} from "../interfaces/IStakerNode.sol"; +import {IDelegationManagerTypes} from "eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; + +library LTMWithdrawalProcessor { + function processWithdrawalRequests( + IWithdrawalManager.WithdrawalRequest[] memory withdrawalRequests, + IERC20[] memory supportedTokens + ) + internal + pure + returns ( + uint256 uniqueTokenCount, + IERC20[] memory redemptionAssets, + uint256[] memory redemptionElDepositShares + ) + { + redemptionAssets = new IERC20[](supportedTokens.length); + redemptionElDepositShares = new uint256[](supportedTokens.length); + + for (uint256 i = 0; i < withdrawalRequests.length; i++) { + IWithdrawalManager.WithdrawalRequest + memory request = withdrawalRequests[i]; + for (uint256 j = 0; j < request.assets.length; j++) { + IERC20 token = request.assets[j]; + bool found = false; + for (uint256 k = 0; k < uniqueTokenCount; k++) { + if (redemptionAssets[k] == token) { + redemptionElDepositShares[k] += request + .elWithdrawableShares[j]; + found = true; + break; + } + } + if (!found) { + redemptionAssets[uniqueTokenCount] = token; + redemptionElDepositShares[uniqueTokenCount] = request + .elWithdrawableShares[j]; + uniqueTokenCount++; + } + } + } + } + + function completeELWithdrawals( + uint256 nodeId, + IDelegationManagerTypes.Withdrawal[] memory withdrawals, + IERC20[][] memory assets, + IERC20[] memory uniqueTokens, + uint256 uniqueTokenCount, + IStakerNode node + ) internal returns (uint256) { + IERC20[] memory receivedTokens = node.completeWithdrawals( + withdrawals, + assets + ); + + for (uint256 j = 0; j < receivedTokens.length; j++) { + IERC20 token = receivedTokens[j]; + bool found = false; + for (uint256 k = 0; k < uniqueTokenCount; k++) { + if (uniqueTokens[k] == token) { + found = true; + break; + } + } + if (!found) { + uniqueTokens[uniqueTokenCount++] = token; + } + } + + return uniqueTokenCount; + } +} \ No newline at end of file