Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 74 additions & 8 deletions src/core/LiquidToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -177,16 +177,20 @@ contract LiquidToken is
) external nonReentrant whenNotPaused returns (bytes32) {
if (assets.length != amounts.length) revert ArrayLengthMismatch();

// Check if we have enough funds from staked and unstaked balances
// Check if we have enough funds from staked (pre-slashing) and unstaked balances
// Here we make a UX decision to check pre-slashing `depositShares` on EL, which means caller can ask for the same amount they deposited, and the fn takes care of the actual accounting
// This removes the burden from the caller and from the manager (when calling `settleUserWithdrawals`) to track slashing on the LAT
if (!_previewWithdrawal(assets, amounts)) revert InvalidWithdrawalRequest();

// Calculate the amount of LAT shares to receive from the user in exchange for the
// withdrawal request with the right to fulfill after a period delay
// Calculate the amount of LAT shares to receive from the user in exchange for the withdrawal request with the right to fulfill after a period delay
// We "charge" the user the equivalent at pre-slashing LAT price, to maintain fair pricing regardless of slashing
uint256 totalShares = 0;
uint256[] memory elWithdrawableShares = new uint256[](assets.length);
for (uint256 i = 0; i < assets.length; i++) {
if (!liquidTokenManager.tokenIsSupported(assets[i])) revert UnsupportedAsset(assets[i]);
if (amounts[i] == 0) revert ZeroAmount();
totalShares += calculateShares(assets[i], amounts[i]);
elWithdrawableShares[i] = liquidTokenManager.getWithdrawableAssetAmount(assets[i], amounts[i], true);
if (elWithdrawableShares[i] == 0) revert ZeroAmount();

totalShares += calculateSharesNoSlashing(assets[i], amounts[i]); // Charge user at pre-slashing LAT price
}

if (totalShares == 0) revert ZeroAmount();
Expand All @@ -204,13 +208,21 @@ contract LiquidToken is
_transfer(msg.sender, address(this), totalShares);

// Create a withdrawal request for the user
withdrawalManager.createWithdrawalRequest(assets, amounts, totalShares, msg.sender, requestId);
withdrawalManager.createWithdrawalRequest(
assets,
amounts,
elWithdrawableShares,
totalShares,
msg.sender,
requestId
);

return requestId;
}

/// @inheritdoc ILiquidToken
function previewWithdrawal(IERC20[] memory assets, uint256[] memory amounts) external view override returns (bool) {
if (assets.length != amounts.length) revert ArrayLengthMismatch();
return _previewWithdrawal(assets, amounts);
}

Expand Down Expand Up @@ -311,6 +323,15 @@ contract LiquidToken is
return liquidTokenManager.convertFromUnitOfAccount(asset, amountInUnitOfAccount);
}

/// @notice Calculate shares at pre-slashing LAT price
/// @param asset The asset to calculate shares for
/// @param amount The amount of the asset
/// @return shares The number of LAT shares at pre-slashing price
function calculateSharesNoSlashing(IERC20 asset, uint256 amount) public view returns (uint256) {
uint256 assetAmountInUnitOfAccount = liquidTokenManager.convertToUnitOfAccount(asset, amount);
return _convertToSharesNoSlashing(assetAmountInUnitOfAccount);
}

// ------------------------------------------------------------------------------
// Getter functions
// ------------------------------------------------------------------------------
Expand All @@ -331,7 +352,10 @@ contract LiquidToken is
);

// Staked withdrawable asset balances
total += liquidTokenManager.getWithdrawableAssetBalance(supportedTokens[i], false);
total += liquidTokenManager.convertToUnitOfAccount(
supportedTokens[i],
liquidTokenManager.getWithdrawableAssetBalance(supportedTokens[i], false) // After any slashing
);
}

return total;
Expand Down Expand Up @@ -385,6 +409,46 @@ contract LiquidToken is
return (shares * totalAsset) / supply;
}

/// @dev Called by `calculateSharesNoSlashing`
/// @dev Calculate shares using pre-slashing total assets
function _convertToSharesNoSlashing(uint256 amount) internal view returns (uint256) {
uint256 supply = totalSupply();
uint256 totalAssetPreSlashing = _totalAssetsNoSlashing();

// Check for totalAssets being 0 to avoid division by zero
if (supply == 0 || totalAssetPreSlashing == 0) {
return amount;
}

return (amount * supply) / totalAssetPreSlashing;
}

/// @dev Called by `_convertToSharesNoSlashing`
/// @dev Calculate total assets as if no slashing occurred
function _totalAssetsNoSlashing() internal view returns (uint256) {
IERC20[] memory supportedTokens = liquidTokenManager.getSupportedTokens();

uint256 total = 0;
for (uint256 i = 0; i < supportedTokens.length; i++) {
// Unstaked asset balances
total += liquidTokenManager.convertToUnitOfAccount(supportedTokens[i], _balanceAsset(supportedTokens[i]));

// Queued asset balances
total += liquidTokenManager.convertToUnitOfAccount(
supportedTokens[i],
_balanceQueuedAsset(supportedTokens[i])
);

// Pre-slashing staked balances
total += liquidTokenManager.convertToUnitOfAccount(
supportedTokens[i],
liquidTokenManager.getDepositAssetBalance(supportedTokens[i], false) // Pre-slashing
);
}

return total;
}

/// @dev Called by `balanceAssets` and `totalAssets`
function _balanceAsset(IERC20 asset) internal view returns (uint256) {
return assetBalances[address(asset)];
Expand All @@ -404,6 +468,8 @@ contract LiquidToken is
for (uint256 i = 0; i < assets.length; i++) {
IERC20 asset = assets[i];
if (
(!liquidTokenManager.tokenIsSupported(assets[i])) ||
(amounts[i] == 0) ||
(assetBalances[address(asset)] + liquidTokenManager.getDepositAssetBalance(asset, false)) < amounts[i] // Preview with pre-slashing balances
) {
isPossible = false;
Expand Down
34 changes: 31 additions & 3 deletions src/core/LiquidTokenManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -356,11 +356,13 @@ contract LiquidTokenManager is
// Transfer assets to node
for (uint256 i = 0; i < assetsLength; i++) {
depositAssets[i] = assets[i];
depositAmounts[i] = amounts[i];
assets[i].safeTransfer(address(node), amounts[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, assets, amounts, msg.sender);
emit AssetsStakedToNode(nodeId, depositAssets, depositAmounts, msg.sender);

// Call for node to deposit assets into EigenLayer
node.depositAssets(depositAssets, depositAmounts, strategiesForNode);
Expand Down Expand Up @@ -866,6 +868,11 @@ contract LiquidTokenManager is
}
}

// Trim arrays to actual sizes
assembly {
mstore(redemptionAssets, 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
Expand Down Expand Up @@ -1342,6 +1349,27 @@ contract LiquidTokenManager is
return inElShares ? withdrawableShares[0] : strategy.sharesToUnderlyingView(withdrawableShares[0]);
}

/// @inheritdoc ILiquidTokenManager
function getWithdrawableAssetAmount(IERC20 asset, uint256 amount, bool inElShares) external view returns (uint256) {
IStrategy strategy = tokenStrategies[asset];
if (address(strategy) == address(0)) {
revert StrategyNotFound(address(asset));
}

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);
}

if (totalDepositBalance == 0 || totalWithdrawableBalance == 0) return 0;

return amount.mulDiv(totalWithdrawableBalance, totalDepositBalance); // Withdrawable portion after any slashing
}

/// @inheritdoc ILiquidTokenManager
function tokenIsSupported(IERC20 token) external view returns (bool) {
return tokens[token].decimals != 0;
Expand Down
4 changes: 3 additions & 1 deletion src/core/StakerNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ contract StakerNode is IStakerNode, Initializable, ReentrancyGuardUpgradeable {
unchecked {
for (uint256 i = 0; i < assetsLength; i++) {
IERC20 asset = assets[i];
uint256 amount = amounts[i];
IStrategy strategy = strategies[i];

uint256 balance = assets[i].balanceOf(address(this));
uint256 amount = balance < amounts[i] ? balance : amounts[i];

asset.forceApprove(address(strategyManager), amount);

// Call EigenLayer contract to deposit asset
Expand Down
26 changes: 1 addition & 25 deletions src/core/WithdrawalManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,39 +107,15 @@ contract WithdrawalManager is IWithdrawalManager, Initializable, AccessControlUp
function createWithdrawalRequest(
IERC20[] memory assets,
uint256[] memory amounts,
uint256[] memory elWithdrawableShares,
uint256 sharesDeposited,
address user,
bytes32 requestId
) external override nonReentrant {
if (msg.sender != address(liquidToken)) revert NotLiquidToken(msg.sender);
if (sharesDeposited == 0) revert ZeroAmount();
if (assets.length != amounts.length) revert LengthMismatch();
if (assets.length == 0) revert ZeroAmount();
if (assets.length > MAX_WITHDRAWAL_ASSETS) revert ExceedsMaxAssets();
if (user == address(0)) revert ZeroAddress();
if (withdrawalRequests[requestId].user != address(0)) revert RequestAlreadyExists();

uint256[] memory elWithdrawableShares = new uint256[](assets.length);

// Check for duplicate assets and validate each asset
for (uint256 i = 0; i < assets.length; i++) {
if (address(assets[i]) == address(0)) revert ZeroAddress();
if (amounts[i] == 0) revert ZeroAmount();

// Check for duplicates
for (uint256 j = 0; j < i; j++) {
if (assets[i] == assets[j]) revert DuplicateAsset(address(assets[i]));
}

// Validate asset is supported
if (!liquidTokenManager.tokenIsSupported(assets[i])) {
revert UnsupportedAsset(assets[i]);
}

elWithdrawableShares[i] = liquidTokenManager.assetUnderlyingToShares(assets[i], amounts[i]);
if (elWithdrawableShares[i] == 0) revert ZeroAmount();
}

WithdrawalRequest memory request = WithdrawalRequest({
user: user,
assets: assets,
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/ILiquidTokenManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,13 @@ interface ILiquidTokenManager {
bool inElShares
) external view returns (uint256);

/// @notice Gets the withdrawable balance (after slashing) of an asset for a given amount
/// @dev This checks the balances across all nodes and factors in slashing across the system
/// @param asset The asset token address
/// @param amount The amount of asset to calculate corresponding withdrawable amount
/// @param inElShares Whether to return EL shares (true) or underlying amount (false)
function getWithdrawableAssetAmount(IERC20 asset, uint256 amount, bool inElShares) external view returns (uint256);

/// @notice Checks if a token is supported
/// @param token Address of the token to check
/// @return bool indicating whether the token is supported
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/IWithdrawalManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,14 @@ interface IWithdrawalManager {
/// @notice Creates a withdrawal request for a user when they initate one via `LiquidToken`
/// @param assets The final assets the the user wants to end up with
/// @param amounts The withdrawal amounts per asset
/// @param elWithdrawableShares Array of EL shares withdrawable per asset (after any slashing)
/// @param sharesDeposited The LAT shares deposited by the user, to be burned on withdrawal fulfilment
/// @param user The requesting user's address
/// @param requestId The unique identifier of the withdrawal request
function createWithdrawalRequest(
IERC20[] memory assets,
uint256[] memory amounts,
uint256[] memory elWithdrawableShares,
uint256 sharesDeposited,
address user,
bytes32 requestId
Expand Down
Loading