-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: ynEIGEN withdrawals #164
Conversation
src/WithdrawalQueueManager.sol
Outdated
@@ -291,6 +291,9 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr | |||
|
|||
uint256 unitOfAccountAmount = calculateRedemptionAmount(request.amount, redemptionRate); | |||
|
|||
// zero out the last decimal place |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the reason to zero out the last decimal place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it fails because of precision errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed at 7b35647.
src/WithdrawalQueueManager.sol
Outdated
@@ -291,6 +291,9 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr | |||
|
|||
uint256 unitOfAccountAmount = calculateRedemptionAmount(request.amount, redemptionRate); | |||
|
|||
// zero out the last decimal place | |||
unitOfAccountAmount = unitOfAccountAmount / 10 * 10 - 10; // @todo - is this safe? (maybe easier to just do `- 20`?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
> 18456789n / 10n * 10n - 10n
18456770n
From the first comment i'd asume you want the result here to be
18456789n -> 18456780n
if we can clarify first what's the objective here we can fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes objective is to not fail on this https://github.com/yieldnest/yieldnest-protocol/blob/yneigen-withdrawals/src/WithdrawalQueueManager.sol#L306
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we just push a slight excess of tokens to the vault all the time, instead of tolerating wei differences?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep probably better - 7b35647 (left my rpcurl there oops)
src/WithdrawalQueueManager.sol
Outdated
@@ -382,7 +385,7 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr | |||
uint256 amount, | |||
uint256 redemptionRate | |||
) public view returns (uint256) { | |||
return amount * redemptionRate / (10 ** redeemableAsset.decimals()); | |||
return amount * redemptionRate / (10 ** redeemableAsset.decimals()); // @todo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the @todo here?
Useful to add a clarifying comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right. im in not process of cleaning up. give me another day here pls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no worries, i'll leave the comments anyway, with the understanding this is an early draft
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed at 7b35647.
* @param amount The amount of assets to transfer. | ||
* @dev Requires the caller to be the redeemer and the contract to not be paused. | ||
*/ | ||
function transferRedemptionAssets(address to, uint256 amount, bytes calldata /* data */) public onlyRedeemer whenNotPaused nonReentrant { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another approach here is to send proportionate to the content of the vault. That way it's on the off-chain to decide the ratio and the on-chain simply uses what it has available without more complex logic.
uint256 totalRedemptionAssets = availableRedemptionAssets();
uint256 assetRatio = amount * YN_UNIT / totalRedemptionAssets;
for (asset of assets) {
uint256 amountToTransfer = balances[address(asset)] * assetRatio / YN_UNIT;
// Do transfer
}
We should be mindful about has here and repeated computation, we can duplicate code for less gas usage (Eg. availableRedemptionAssets logic)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggested another approach on Discord. will update this thread once we have a decision.
src/ynEIGEN/EigenStrategyManager.sol
Outdated
@@ -306,7 +305,15 @@ contract EigenStrategyManager is | |||
asset, | |||
strategies[asset].userUnderlyingView((address(node))) | |||
); | |||
stakedBalances[j] += strategyBalance; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is the most gas consumptive function in the current implementation that poses problems because of the existing unbounded loops.
Let's discuss if we can include a solution for this in this current upgrade since we're updating it regardless.
This is a general problem that applies to all the multi-asset vaults that stake the assets within protocols but it's particularly pathological in Eigenlayer because of the StakingNodes pattern that is necessary to communicate to validators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea here:
We could make the result of that inner loop cached in a storage variable per asset.
This variable will be updated all the time it when you allocate to StakinNodes or withdraw from StakingNodes.
The update function can also be permisionless - which would cover the slashing case.
This reduces it to a O(N) loop over the N assets which is reasonable for any multi-asset vault.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johnnyonline this is not addressed - we'll need to cover this one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not sure i understand what you mean here. but trying to optimize it a bit -
using function testTotalAssetsGas() public view {yneigen.totalAssets();})
as a test and forge snapshot
.
testTotalAssetsGas()
gas:
src/ynEIGEN/TokenStakingNode.sol
Outdated
emit DeallocatedTokens(_amount, _token); | ||
} | ||
|
||
function _wrapIfNeeded(uint256 _amount, IERC20 _token) internal returns (uint256, IERC20) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should have this asset-specific logic that handles rebasing tokens not contained here
currently all those asset specific things are in EigenStrategyManager
We should consider refactoring things so it's all contained in 1 contract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danoctavian we have this wrapping logic in the ynEigenDepositAdapter
currently.
//---------------------------------- WITHDRAWALS ------------------------------------- | ||
//-------------------------------------------------------------------------------------- | ||
|
||
function processPrincipalWithdrawals( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we don't do this at all in ynEigen?
The reason this was a thing in ynETH is because
-
we had to take fees on withdrawals - therefore we had to insert fee collection logic on the withdrawal path + the trusted input on how much was rewards
-
there was accounting to change for each StakingNode (allocated staked eth needed to change)
-
The principal withdrawals came out in 32 ETH chunks (no further granularity) - therefore we had to have the "withdraw" and "reinvest" breakdown
As far as i can tell 1,2,3 don't apply here, but there is some accounting inside each of the TokenStakingNode instances.
What if simply have a role there that is able to do this and is constrained to send things to the vault, and leave this contract fully unchanged?
it will act on each TokenStakingNode as it does to trigger withdrawals and queuedWithdrawals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we may need to store just a reference to the redemptionAssetVault since the staking nodes query the Manager for data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Withdraw/Reinvest breakdown does have a use-case actually, that's reallocation.
That's fair.
Thinking how we can simplify the code as much as we can
function initializeV2( | ||
address _redemptionAssetsVault, | ||
address _withdrawer | ||
) external onlyRole(DEFAULT_ADMIN_ROLE) reinitializer(2) notZeroAddress(_redemptionAssetsVault) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the experience with ynETH and the issues with this, we should not use
onlyRole(DEFAULT_ADMIN_ROLE)
and call the initialize as part of upgradeAndCall().
Need to think carefully through this but i believe it would be correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/ynEIGEN/EigenStrategyManager.sol
Outdated
|
||
uint256 strategyWithdrawalQueueBalance; | ||
uint256 queuedShares = node.queuedShares(strategies[asset]); | ||
if (queuedShares > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's please use { } all the time, eliminates the possibility of making a block scope mistake.
Main thoughts here: Instead of making changes to TokenStakingNodesManager, If any reinvest/withdraw logic is still maintained we hold it in in EigenStrategyManager. _wrapIfNeeded would also only be in EigenStrategyManager, to not add logic specific to that in TokenStakingNode.
becomes
And TokenStakingNodesManager does not interfere with withdrawal. Thinking along the lines of to be able to have TokenStakingNodesManager/TokenStakingNode something we can take and attach to other protocols without it being overloaded with functionality. Logically, withdraw/reinvest belongs in the strategy. |
moved withdrawal logic to strategy manager 92540f4 (and removed comments here 4731a19). refactoring |
} | ||
} | ||
|
||
// ============================================================================================ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move toUserAssetAmount
logic in here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// ============================================================================================ | ||
|
||
/// @inheritdoc IWrapper | ||
function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's change the wrapper as discussed to be called with DELEGATECALL to avoid the double token transfer as discussed
which eliminates forceApproe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not as straight forward as it seemed, since wstETH doesnt allow to specify a sender, it would try to get funds from the tx.origin
. discussing options on Discord.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here i think you mean msg.sender not tx.origin
And what happens in this scenario is the Wrapper is msg.sender for wstETH (unlike the case with an external ibrary which still makes the contract that does DELEGATE call be the msg.sender still since the lib is not an actual contract).
And this makes it expect that funds are in there.
I want to follow up on this myself, i did a PoC and it worked for me for some reason.. might have had an error.
src/ynEIGEN/EigenStrategyManager.sol
Outdated
uint256 strategyWithdrawalQueueBalance; | ||
uint256 queuedShares = node.queuedShares(strategies[asset]); | ||
if (queuedShares > 0) { | ||
strategyWithdrawalQueueBalance = toUserAssetAmount(asset, strategies[asset].sharesToUnderlyingView(queuedShares)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change this loop to call toUserAssetAmount only once by summing
the results of queuedShares = node.queuedShares(strategies[asset]); and
uint256 strategyBalance = toUserAssetAmount(
asset,
strategies[asset].userUnderlyingView((address(node)))
);
```
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
} | ||
|
||
emit AssetTransferred(ETH_ASSET, msg.sender, to, amount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the even should have the first value of amount, this one is potentially 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param amount The amount of assets to transfer. | ||
* @dev Requires the caller to be the redeemer and the contract to not be paused. | ||
*/ | ||
function transferRedemptionAssets(address to, uint256 amount, bytes calldata /* data */) public onlyRedeemer whenNotPaused nonReentrant { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WE can move forward with strategy and explain the distribution strategy in the natspec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/ynEIGEN/EigenStrategyManager.sol
Outdated
stakedBalances[j] += strategyBalance; | ||
|
||
uint256 strategyWithdrawalQueueBalance; | ||
uint256 queuedShares = node.queuedShares(strategies[asset]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can do a bundled read for:
queuedShares and withdrawnAsset that returns a tuple so we don't do 2 separate calls to node
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
37d5cf6
to
24b72b5
Compare
src/ynEIGEN/EigenStrategyManager.sol
Outdated
} | ||
|
||
stakedBalances[j] += | ||
wrapper.toUserAssetAmount(asset, strategyBalance + strategyWithdrawalQueueBalance) + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for this optimization to be worthwhile, this contract call needs to be moved outside the inner loop.
Right now we still do 1 call per inner loop to wrapper.toUserAssetAmount(asset, strategyBalance + strategyWithdrawalQueueBalance)
To optimize this you need to rely on the property that
wrapper.toUserAssetAmount(asset, X) + wrapper.toUserAssetAmount(asset, Y) == wrapper.toUserAssetAmount(asset, X + Y)
Therefore you sum the uncoverted amounts inside the inner loop and convert the sum outside of the inner loop exactly once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done here be925af
|
src/ynEIGEN/EigenStrategyManager.sol
Outdated
IRedemptionAssetsVaultExt public redemptionAssetsVault; | ||
IWrapper public wrapper; | ||
|
||
// @todo - struct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed struct also:
we could do the following:
strategiesBalance[strategy] = _strategiesBalance;
strategiesWithdrawalQueueBalance[strategy] = _strategiesWithdrawalQueueBalance
are stored as a sum. of the 2 things since they get converted together
using the Wrapper. This can be stored as as a uint128.
strategiesWithdrawnBalance gets stored as the other uint128.
Thus both packed in the same slot.
Make sure when writing that the slot gets written exactly one time (do the update of both at the same time).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uint256 stakedBalance; | ||
uint256 withdrawnBalance; | ||
} | ||
mapping(IStrategy => StrategyBalance) public strategiesBalance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xhad we cache the staked balances here
src/ynEIGEN/EigenStrategyManager.sol
Outdated
/// @dev On a slashing events, users will have an incentive to call this function, to decrease the exchange rate. | ||
/// @param asset The asset for which the balances are to be updated. | ||
/// @param strategy The strategy for which the balances are to be updated. If not provided, we search for the strategy associated with the asset. | ||
function updateTokenStakingNodesBalances(IERC20 asset, IStrategy strategy) public { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xhad this updates the cached balances
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in theory this can have another optimization done and that is:
take a list of assets as parameter and process them all in a single loop.
and hat woul entail we make versions of getQueuedSharesAndWithdrawn(strategy, asset)
that takes arrays as paramers and returns parameters
let's leave this for later i dont think there's enough time now
156693d
to
4d66d0b
Compare
Deployment/yn ls de withdrawals
81c829b
to
9b4a2d6
Compare
No description provided.