diff --git a/.gitmodules b/.gitmodules index 75cd8bf20..44f0f748e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,4 +17,4 @@ [submodule "lib/eigenlayer-contracts"] path = lib/eigenlayer-contracts url = https://github.com/layr-labs/eigenlayer-contracts - commit = bda003385c5fec59e35196dc14d01f17d1eb7001 \ No newline at end of file + commit = bda003385c5fec59e35196dc14d01f17d1eb7001 diff --git a/README.md b/README.md index 41e30b23d..7d176d63b 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,6 @@ | ynETH Redemption Assets Vault | [`0xF0207Ffa0b793E009DF9Df62fEE95B8FC6c93EcF`](https://etherscan.io/address/0x5D6e53c42E3B37f82F693937BC508940769c5caf) | | WithdrawalsProcessor | [`0x61810a90128ee5c5f5a3730f0449da9e9480f888`](https://etherscan.io/address/0x6d052CdEd3F64aea51f6051F33b68b42016C5FbA) | - - - - ## Holesky ### Contracts diff --git a/deployments/YnLSDe-1.json b/deployments/YnLSDe-1.json index b1b7c43b0..b8ba8d1ce 100644 --- a/deployments/YnLSDe-1.json +++ b/deployments/YnLSDe-1.json @@ -34,4 +34,4 @@ "proxyAdmin-ynEigenViewer": "0x40fBdD124e80fF168574Fa7823440E574345307E", "tokenStakingNodeImplementation": "0x57e1D090B1aF526CD4174019D5b9702EEBE236FA", "upgradeTimelock": "0xbB73f8a5B0074b27c6df026c77fA08B0111D017A" -} \ No newline at end of file +} diff --git a/deployments/v0.0.3-ynEigenViewer-17000-4d0ed054af4e3877bc3f4dc7affd12c843d22f1b.json b/deployments/v0.0.3-ynEigenViewer-17000-4d0ed054af4e3877bc3f4dc7affd12c843d22f1b.json new file mode 100644 index 000000000..0ddc55b2a --- /dev/null +++ b/deployments/v0.0.3-ynEigenViewer-17000-4d0ed054af4e3877bc3f4dc7affd12c843d22f1b.json @@ -0,0 +1,51 @@ +{ + "transactions": [ + { + "hash": "0x6645dc197a9f650c593571384bf632b68982fc986dc0c750f4f9acd5c828f2e4", + "transactionType": "CREATE", + "contractName": "ynEigenViewer", + "contractAddress": "0xbea193a39958bc175b10cc1dfc742e5a3ebaafaf", + "function": null, + "arguments": [ + "0xaD31546AdbfE1EcD7137310508f112039a35b6F7", + "0x071bdC8eDcdD66730f45a3D3A6F794FAA37C75ED", + "0x5c20D1a85C7d9acB503135a498E26Eb55d806552", + "0xd68C29263F6dC2Ff8D9307b3AfAcD6D6fDeFbB3A" + ], + "transaction": { + "from": "0xd7d4a7f14265b0a69ae482b85b0a8496c3f99fb9", + "gas": "0x15c9de", + "value": "0x0", + "input": "0x61010060405234801562000011575f80fd5b506040516200143238038062001432833981016040819052620000349162000073565b6001600160a01b0393841660805291831660a052821660c0521660e052620000cd565b80516001600160a01b03811681146200006e575f80fd5b919050565b5f805f806080858703121562000087575f80fd5b620000928562000057565b9350620000a26020860162000057565b9250620000b26040860162000057565b9150620000c26060860162000057565b905092959194509250565b60805160a05160c05160e0516112de620001545f395f818161015d015261063b01525f8181610136015261076301525f818160ef015281816103a2015281816107ec0152818161086f0152818161093701528181610bee01528181610d110152610d6801525f8181610184015281816101cd015281816102e6015261044c01526112de5ff3fe608060405234801561000f575f80fd5b506004361061009b575f3560e01c80637c0867af116100635780637c0867af14610131578063949db65814610158578063979d7e861461017f5780639d8e2177146101a6578063b8f82b26146101b5575f80fd5b8063075c2b191461009f5780632e0f2625146100bd5780633ff36802146100d557806363f7bad5146100ea578063679aefce14610129575b5f80fd5b6100a76101c8565b6040516100b49190610e2c565b60405180910390f35b6100c7620f424081565b6040519081526020016100b4565b6100dd61075f565b6040516100b49190610ef0565b6101117f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100b4565b6100c76107e8565b6101117f000000000000000000000000000000000000000000000000000000000000000081565b6101117f000000000000000000000000000000000000000000000000000000000000000081565b6101117f000000000000000000000000000000000000000000000000000000000000000081565b6100c7670de0b6b3a764000081565b6100c76101c3366004610f53565b610933565b60605f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166367e4ac2c6040518163ffffffff1660e01b81526004015f60405180830381865afa158015610226573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261024d9190810190610fe5565b80519091508067ffffffffffffffff81111561026b5761026b610f7d565b6040519080825280602002602001820160405280156102e057816020015b6102cd6040518060e001604052805f6001600160a01b0316815260200160608152602001606081526020015f81526020015f81526020015f81526020015f81525090565b8152602001906001900390816102895790505b5092505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663613e85ff6040518163ffffffff1660e01b81526004015f60405180830381865afa15801561033f573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f19168201604052610366919081019061107f565b90508051821461039f578051604051631f4bb7c160e31b8152610396918491600401918252602082015260400190565b60405180910390fd5b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166301e1d1146040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103fc573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104209190611100565b90505f5b83811015610757575f83828151811061043f5761043f611117565b602002602001015190505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316631cbf960d88858151811061048b5761048b611117565b6020026020010151846040518363ffffffff1660e01b81526004016104c59291906001600160a01b03929092168252602082015260400190565b602060405180830381865afa1580156104e0573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906105049190611100565b90506040518060e0016040528088858151811061052357610523611117565b60200260200101516001600160a01b0316815260200188858151811061054b5761054b611117565b60200260200101516001600160a01b03166306fdde036040518163ffffffff1660e01b81526004015f60405180830381865afa15801561058d573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526105b4919081019061112b565b81526020018885815181106105cb576105cb611117565b60200260200101516001600160a01b03166395d89b416040518163ffffffff1660e01b81526004015f60405180830381865afa15801561060d573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f19168201604052610634919081019061112b565b81526020017f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630ba9d8ca8a878151811061067a5761067a611117565b60200260200101516040518263ffffffff1660e01b81526004016106ad91906001600160a01b0391909116815260200190565b602060405180830381865afa1580156106c8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906106ec9190611100565b81526020015f831180156106ff57505f86115b610709575f610721565b85610717620f4240856111ba565b61072191906111dd565b81526020018281526020018381525088848151811061074257610742611117565b60209081029190910101525050600101610424565b505050505090565b60607f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663a1174e7d6040518163ffffffff1660e01b81526004015f60405180830381865afa1580156107bc573d5f803e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526107e391908101906111fc565b905090565b5f807f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166318160ddd6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610846573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061086a9190611100565b90505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166301e1d1146040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108c9573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906108ed9190611100565b90508115806108fa575080155b1561090f57670de0b6b3a76400009250505090565b8161092282670de0b6b3a76400006111ba565b61092c91906111dd565b9250505090565b5f807f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166352f94f986040518163ffffffff1660e01b8152600401602060405180830381865afa158015610991573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906109b59190611286565b90505f816001600160a01b0316634e6c97666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f4573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a189190611286565b90505f826001600160a01b031663c1fe3e486040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a57573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a7b9190611286565b90505f836001600160a01b031663daba667d6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610aba573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ade9190611286565b90505f846001600160a01b0316634aa07e646040518163ffffffff1660e01b8152600401602060405180830381865afa158015610b1d573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610b419190611286565b9050836001600160a01b0316886001600160a01b031603610c67576040516363737ac960e11b8152600481018890525f906001600160a01b0384169063c6e6f59290602401602060405180830381865afa158015610ba1573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610bc59190611100565b604051635c7c159360e11b81526001600160a01b038581166004830152602482018390529192507f00000000000000000000000000000000000000000000000000000000000000009091169063b8f82b26906044015b602060405180830381865afa158015610c36573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610c5a9190611100565b9650505050505050610dd9565b826001600160a01b0316886001600160a01b031603610d425760405162b0e38960e81b8152600481018890525f906001600160a01b0383169063b0e3890090602401602060405180830381865afa158015610cc4573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ce89190611100565b604051635c7c159360e11b81526001600160a01b038481166004830152602482018390529192507f00000000000000000000000000000000000000000000000000000000000000009091169063b8f82b2690604401610c1b565b604051635c7c159360e11b81526001600160a01b038981166004830152602482018990527f0000000000000000000000000000000000000000000000000000000000000000169063b8f82b2690604401602060405180830381865afa158015610dad573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610dd19190611100565b955050505050505b92915050565b5f5b83811015610df9578181015183820152602001610de1565b50505f910152565b5f8151808452610e18816020860160208601610ddf565b601f01601f19169290920160200192915050565b5f60208083018184528085518083526040925060408601915060408160051b8701018488015f5b83811015610ee257888303603f19018552815180516001600160a01b031684528781015160e089860181905290610e8c82870182610e01565b9150508782015185820389870152610ea48282610e01565b606084810151908801526080808501519088015260a0808501519088015260c093840151939096019290925250509386019390860190600101610e53565b509098975050505050505050565b602080825282518282018190525f9190848201906040850190845b81811015610f305783516001600160a01b031683529284019291840191600101610f0b565b50909695505050505050565b6001600160a01b0381168114610f50575f80fd5b50565b5f8060408385031215610f64575f80fd5b8235610f6f81610f3c565b946020939093013593505050565b634e487b7160e01b5f52604160045260245ffd5b604051601f8201601f1916810167ffffffffffffffff81118282101715610fba57610fba610f7d565b604052919050565b5f67ffffffffffffffff821115610fdb57610fdb610f7d565b5060051b60200190565b5f6020808385031215610ff6575f80fd5b825167ffffffffffffffff81111561100c575f80fd5b8301601f8101851361101c575f80fd5b805161102f61102a82610fc2565b610f91565b81815260059190911b8201830190838101908783111561104d575f80fd5b928401925b8284101561107457835161106581610f3c565b82529284019290840190611052565b979650505050505050565b5f6020808385031215611090575f80fd5b825167ffffffffffffffff8111156110a6575f80fd5b8301601f810185136110b6575f80fd5b80516110c461102a82610fc2565b81815260059190911b820183019083810190878311156110e2575f80fd5b928401925b82841015611074578351825292840192908401906110e7565b5f60208284031215611110575f80fd5b5051919050565b634e487b7160e01b5f52603260045260245ffd5b5f6020828403121561113b575f80fd5b815167ffffffffffffffff80821115611152575f80fd5b818401915084601f830112611165575f80fd5b81518181111561117757611177610f7d565b61118a601f8201601f1916602001610f91565b91508082528560208285010111156111a0575f80fd5b6111b1816020840160208601610ddf565b50949350505050565b8082028115828204841417610dd957634e487b7160e01b5f52601160045260245ffd5b5f826111f757634e487b7160e01b5f52601260045260245ffd5b500490565b5f602080838503121561120d575f80fd5b825167ffffffffffffffff811115611223575f80fd5b8301601f81018513611233575f80fd5b805161124161102a82610fc2565b81815260059190911b8201830190838101908783111561125f575f80fd5b928401925b8284101561107457835161127781610f3c565b82529284019290840190611264565b5f60208284031215611296575f80fd5b81516112a181610f3c565b939250505056fea2646970667358221220e13239e732eae37dd9a6fb584eb86ca47b27fbdc7f5b4ca923c907d0704f12f464736f6c63430008180033000000000000000000000000ad31546adbfe1ecd7137310508f112039a35b6f7000000000000000000000000071bdc8edcdd66730f45a3d3a6f794faa37c75ed0000000000000000000000005c20d1a85c7d9acb503135a498e26eb55d806552000000000000000000000000d68c29263f6dc2ff8d9307b3afacd6d6fdefbb3a", + "nonce": "0x177", + "chainId": "0x4268" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x2bb913", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0x6645dc197a9f650c593571384bf632b68982fc986dc0c750f4f9acd5c828f2e4", + "transactionIndex": "0x13", + "blockHash": "0x9014d974e965c1a7c95b79dc6b6381b0ce49b5ba682420eb76af13d53aed6a41", + "blockNumber": "0x23eec4", + "gasUsed": "0x10c2ab", + "effectiveGasPrice": "0xa301bb", + "from": "0xd7d4a7f14265b0a69ae482b85b0a8496c3f99fb9", + "to": null, + "contractAddress": "0xbea193a39958bc175b10cc1dfc742e5a3ebaafaf" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1726598187, + "chain": 17000, + "commit": "aedbbd9" +} \ No newline at end of file diff --git a/deployments/ynLSDe-17000-ynFoo.json b/deployments/ynLSDe-17000-ynFoo.json new file mode 100644 index 000000000..4a3d155f3 --- /dev/null +++ b/deployments/ynLSDe-17000-ynFoo.json @@ -0,0 +1,46 @@ +{ + "ADMIN": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "DEFAULT_SIGNER": "0x72fdBD51085bDa5eEEd3b55D1a46E2e92f0837a5", + "EIGEN_STRATEGY_ADMIN": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "PAUSE_ADMIN": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "PROXY_ADMIN_OWNER": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "STAKING_ADMIN": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "STAKING_NODES_OPERATOR": "0x9Dd8F69b62ddFd990241530F47dcEd0Dad7f7d39", + "STRATEGY_CONTROLLER": "0x1234567890123456789012345678901234567890", + "TOKEN_STAKING_NODE_CREATOR": "0x9Dd8F69b62ddFd990241530F47dcEd0Dad7f7d39", + "UNPAUSE_ADMIN": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "YNDev": "0x9Dd8F69b62ddFd990241530F47dcEd0Dad7f7d39", + "YnSecurityCouncil": "0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913", + "implementation-assetRegistry": "0x156e0382068C3f96a629f51dcF99cEA5250B9eda", + "implementation-eigenStrategyManager": "0x251D14a2FB3cf3E35CE2355fBC7943257bcCcA08", + "implementation-lsdWrapper": "0xD0F77441B70c84aa3366a9F79F2fD16618739aB0", + "implementation-rateProvider": "0x17969afC774ec28802F1316cd4474c537114600A", + "implementation-redemptionAssetsVault": "0xB510d24856795F4B7Bb6e977E14D3157Ceeaa7FE", + "implementation-tokenStakingNodesManager": "0xeD44B98Bd50cd5e128B420495bd9DA384A291B9A", + "implementation-withdrawalQueueManager": "0x108D8d8d89B3459ec37c0E71A51C8a6771aC488c", + "implementation-ynEigenDepositAdapter": "0x11830FF94221F2Ef34176AD85A6cCAa2f41C95fa", + "implementation-ynEigenViewer": "0xe30c24d5E6Cf5320a9ad550853b80d6e506fcb5b", + "implementation-ynLSDe": "0x552f4b850d4963D914E6B2C011B5FE475468d3B8", + "proxy-assetRegistry": "0x726cc095C893c2ff6d572C1B2F26e0Ed08484302", + "proxy-eigenStrategyManager": "0x6e18EfFF496b108522bdCF5db92e97c5EE8a2DEd", + "proxy-lsdWrapper": "0xb94FA8D53313b4B6c69DC20489C78E2AF54C5B61", + "proxy-rateProvider": "0x0E68FedE865158D7501e5Ce5033B4fca9123CA98", + "proxy-redemptionAssetsVault": "0xAF920e9D61b91e1b8e39AE6bFBa5Fa73C2f32A34", + "proxy-tokenStakingNodesManager": "0x8c1717166F02911960bD4A31A8C725909A2de39e", + "proxy-withdrawalQueueManager": "0x34354efDF0931d37020e7331363403930c9e251e", + "proxy-ynEigenDepositAdapter": "0x6196F39D95805Fc6186c191E640e8fad156633Ff", + "proxy-ynEigenViewer": "0xb80EecB2904DAd7410B3af6bE897E01dca8CF475", + "proxy-ynLSDe": "0x73961FEcac088A52DdCe286E6935b6bd9Bf391E5", + "proxyAdmin-assetRegistry": "0xBBD351d0137c2C5f44CB8FE1DF4f5398FC8711df", + "proxyAdmin-eigenStrategyManager": "0x44377f05b1b9521F10320319B01C2D781f624719", + "proxyAdmin-lsdWrapper": "0x0432DD28B89b28911b90808861E68885646B9E6E", + "proxyAdmin-rateProvider": "0x65b32cBEcfb849Aa6fD89e6DA1a894Ee7899acD7", + "proxyAdmin-redemptionAssetsVault": "0x890c0367eB82FF4114FcA95CdEbEbD12b61a2a9d", + "proxyAdmin-tokenStakingNodesManager": "0x5a153929A995950B72F4606dfAECb60fBDDaB4Af", + "proxyAdmin-withdrawalQueueManager": "0x84376C30FC35a69C22271ECBA4ca26037a2f8e4E", + "proxyAdmin-ynEigenDepositAdapter": "0x14a87CA41091ddBa399e41b4bC174330dF378dCE", + "proxyAdmin-ynEigenViewer": "0x121dAa9722E2C8e56C35693d7c6913fEd57D5552", + "proxyAdmin-ynLSDe": "0x1745fAAf07167B2D33DE831C1E4e3344090138b4", + "tokenStakingNodeImplementation": "0xc7Fb9046f1db5A36D635Ad1E70D425b779b29840", + "upgradeTimelock": "0x56664FFcCfF6BB282CcA96808AF03d9042e1f799" +} \ No newline at end of file diff --git a/script/upgrades/GenerateYnLSDeUpgradeTxData.s.sol b/script/upgrades/GenerateYnLSDeUpgradeTxData.s.sol index 6043210d5..83f94fccb 100644 --- a/script/upgrades/GenerateYnLSDeUpgradeTxData.s.sol +++ b/script/upgrades/GenerateYnLSDeUpgradeTxData.s.sol @@ -7,19 +7,62 @@ import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.s import {BaseYnEigenScript} from "script/ynEigen/BaseYnEigenScript.s.sol"; +/** + * @title GenerateYnLSDeUpgradeTxData + * @dev This script generates the transaction data needed to upgrade a specific contract in the YnLSDe system. + * + * USAGE: +* -------- + * To run this script, use the following command in your terminal: + * + * TOKEN=[token name] CONTRACT_TO_UPGRADE=[contract identifier] NEW_IMPLEMENTATION=[implementation address] forge script script/upgrades/GenerateYnLSDeUpgradeTxData.s.sol --legacy --rpc-url [rpc url] + * + * + * Where: + * - TOKEN: The name of the token (e.g., ynLSDe) + * - CONTRACT_TO_UPGRADE: The name of the contract to be upgraded (e.g., rateProvider) + * - NEW_IMPLEMENTATION: The address of the new implementation contract + * + * + * + * EXAMPLE: + * -------- + * # Upgrade rateProvider of ynLSDe on Holesky to the given implementation + * TOKEN=ynLSDe CONTRACT_TO_UPGRADE=rateProvider NEW_IMPLEMENTATION=0x48c3dfd4d14e7899c4adbf8e2d5aef6af585d305 forge script script/upgrades/GenerateYnLSDeUpgradeTxData.s.sol --legacy --rpc-url https://rpc.ankr.com/eth_holesky + * + * This command will: + * 1. Set the token name to 'ynLSDe' + * 2. Specify 'rateProvider' as the contract to upgrade + * 3. Set the new implementation address to 0x48c3dfd4d14e7899c4adbf8e2d5aef6af585d305 + * 4. Use the Holesky testnet RPC URL for execution + * + * The script will then generate and display the necessary transaction data for the upgrade process. + * -------- + */ + contract GenerateYnLSDeUpgradeTxData is BaseYnEigenScript { Deployment deployment; + string internal _tokenName; + + function tokenName() internal view override returns (string memory) { + return _tokenName; + } + function run() external { - deployment = loadDeployment(); console.log("=== Upgrade Information ==="); console.log("Current Block Number: %s", block.number); console.log("Current Chain ID: %s", block.chainid); + _tokenName = vm.envString("TOKEN"); string memory contractToUpgrade = vm.envString("CONTRACT_TO_UPGRADE"); address newImplementation = vm.envAddress("NEW_IMPLEMENTATION"); + console.log("Token Name: %s", _tokenName); + + deployment = loadDeployment(); + console.log("=== Contract Upgrade Details ==="); console.log("Contract to upgrade: %s", contractToUpgrade); console.log("Contract address: %s", vm.toString(getProxyAddress(contractToUpgrade))); diff --git a/script/ynEigen/BaseYnEigenScript.s.sol b/script/ynEigen/BaseYnEigenScript.s.sol index a7af630b8..c7360701b 100644 --- a/script/ynEigen/BaseYnEigenScript.s.sol +++ b/script/ynEigen/BaseYnEigenScript.s.sol @@ -10,6 +10,9 @@ import {TokenStakingNode} from "src/ynEIGEN/TokenStakingNode.sol"; import {ynEigenDepositAdapter} from "src/ynEIGEN/ynEigenDepositAdapter.sol"; import {IRateProvider} from "src/interfaces/IRateProvider.sol"; import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; import {ActorAddresses} from "script/Actors.sol"; import {ContractAddresses} from "script/ContractAddresses.sol"; @@ -42,6 +45,9 @@ contract BaseYnEigenScript is BaseScript { IRateProvider rateProvider; TimelockController upgradeTimelock; ynEigenViewer viewer; + RedemptionAssetsVault redemptionAssetsVault; + WithdrawalQueueManager withdrawalQueueManager; + LSDWrapper lsdWrapper; DeploymentProxies proxies; } @@ -99,7 +105,7 @@ contract BaseYnEigenScript is BaseScript { if (!isSupportedChainId(inputs.chainId)) revert UnsupportedChainId(inputs.chainId); } - function tokenName() internal view returns (string memory) { + function tokenName() internal virtual view returns (string memory) { return inputs.symbol; } @@ -107,6 +113,7 @@ contract BaseYnEigenScript is BaseScript { string memory root = vm.projectRoot(); return string.concat(root, "/deployments/", tokenName(), "-", vm.toString(block.chainid), ".json"); + // return string.concat(root, "/deployments/", tokenName(), "-", vm.toString(block.chainid), "-ynFoo", ".json"); } function saveDeployment(Deployment memory deployment) public virtual { @@ -122,6 +129,10 @@ contract BaseYnEigenScript is BaseScript { serializeProxyElements(json, "rateProvider", address(deployment.rateProvider)); serializeProxyElements(json, "ynEigenViewer", address(deployment.viewer)); vm.serializeAddress(json, "upgradeTimelock", address(deployment.upgradeTimelock)); + serializeProxyElements(json, "redemptionAssetsVault", address(deployment.redemptionAssetsVault)); + serializeProxyElements(json, "withdrawalQueueManager", address(deployment.withdrawalQueueManager)); + serializeProxyElements(json, "lsdWrapper", address(deployment.lsdWrapper)); + // actors vm.serializeAddress(json, "PROXY_ADMIN_OWNER", address(actors.admin.PROXY_ADMIN_OWNER)); diff --git a/script/ynEigen/YnEigenDeployer.s.sol b/script/ynEigen/YnEigenDeployer.s.sol index 445ebbdd8..a05d682ac 100644 --- a/script/ynEigen/YnEigenDeployer.s.sol +++ b/script/ynEigen/YnEigenDeployer.s.sol @@ -19,11 +19,14 @@ import {AssetRegistry} from "src/ynEIGEN/AssetRegistry.sol"; import {TokenStakingNodesManager} from "src/ynEIGEN/TokenStakingNodesManager.sol"; import {ynEigenDepositAdapter} from "src/ynEIGEN/ynEigenDepositAdapter.sol"; import {ynEigenViewer} from "src/ynEIGEN/ynEigenViewer.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; import {BaseYnEigenScript} from "script/ynEigen/BaseYnEigenScript.s.sol"; import {YnEigenFactory} from "./YnEigenFactory.sol"; -import {YnEigenInit, YnEigenActors, YnEigenChainAddresses, YnEigenImplementations} from "./YnEigenStructs.sol"; +import {YnEigenInit, YnEigenActors, YnEigenChainAddresses, YnEigenImplementations, YnEigenProxies} from "./YnEigenStructs.sol"; import {ynEigen} from "src/ynEIGEN/ynEigen.sol"; import {EigenStrategyManager} from "src/ynEIGEN/EigenStrategyManager.sol"; @@ -124,7 +127,15 @@ contract YnEigenDeployer is BaseYnEigenScript { tokenStakingNodesManager: address(new TokenStakingNodesManager()), tokenStakingNode: address(new TokenStakingNode()), assetRegistry: address(new AssetRegistry()), - depositAdapter: address(new ynEigenDepositAdapter()) + depositAdapter: address(new ynEigenDepositAdapter()), + redemptionAssetsVault: address(new RedemptionAssetsVault()), + withdrawalQueueManager: address(new WithdrawalQueueManager()), + lsdWrapper: address(new LSDWrapper( + chainAddresses.lsd.WSTETH_ADDRESS, + chainAddresses.lsd.WOETH_ADDRESS, + chainAddresses.lsd.OETH_ADDRESS, + chainAddresses.lsd.STETH_ADDRESS + )) }); } @@ -156,17 +167,7 @@ contract YnEigenDeployer is BaseYnEigenScript { { YnEigenFactory factory = new YnEigenFactory(); - ( - ynEigen ynToken, - EigenStrategyManager eigenStrategyManager, - TokenStakingNodesManager tokenStakingNodesManager, - TokenStakingNode tokenStakingNode, - AssetRegistry assetRegistry, - ynEigenDepositAdapter ynEigenDepositAdapterInstance, - IRateProvider rateProvider, - TimelockController timelock, - ynEigenViewer viewer - ) = factory.deploy(init); + YnEigenProxies memory proxies = factory.deploy(init); vm.stopBroadcast(); @@ -176,22 +177,25 @@ contract YnEigenDeployer is BaseYnEigenScript { address(0) ); Deployment memory deployment = Deployment({ - ynEigen: ynToken, - assetRegistry: assetRegistry, - eigenStrategyManager: eigenStrategyManager, - tokenStakingNodesManager: tokenStakingNodesManager, - tokenStakingNodeImplementation: tokenStakingNode, - ynEigenDepositAdapterInstance: ynEigenDepositAdapterInstance, - rateProvider: rateProvider, - upgradeTimelock: timelock, - viewer: viewer, + ynEigen: proxies.ynToken, + assetRegistry: proxies.assetRegistry, + eigenStrategyManager: proxies.eigenStrategyManager, + tokenStakingNodesManager: proxies.tokenStakingNodesManager, + tokenStakingNodeImplementation: proxies.tokenStakingNode, + ynEigenDepositAdapterInstance: proxies.ynEigenDepositAdapterInstance, + rateProvider: proxies.rateProvider, + upgradeTimelock: proxies.timelock, + viewer: proxies.viewer, + redemptionAssetsVault: proxies.redemptionAssetsVault, + withdrawalQueueManager: proxies.withdrawalQueueManager, + lsdWrapper: proxies.lsdWrapper, proxies: DeploymentProxies({ ynEigen: proxyAddressesEmpty, assetRegistry: proxyAddressesEmpty, eigenStrategyManager: proxyAddressesEmpty, tokenStakingNodesManager: proxyAddressesEmpty, - rateProvider: proxyAddressesEmpty, ynEigenDepositAdapter: proxyAddressesEmpty, + rateProvider: proxyAddressesEmpty, ynEigenViewer: proxyAddressesEmpty }) }); diff --git a/script/ynEigen/YnEigenFactory.sol b/script/ynEigen/YnEigenFactory.sol index e8c759826..75566da1b 100644 --- a/script/ynEigen/YnEigenFactory.sol +++ b/script/ynEigen/YnEigenFactory.sol @@ -7,7 +7,7 @@ import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfa import {IStrategyManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; - +import {IRedeemableAsset} from "src/interfaces/IRedeemableAsset.sol"; import {IynEigen} from "src/interfaces/IynEigen.sol"; import {IRateProvider} from "src/interfaces/IRateProvider.sol"; import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; @@ -21,7 +21,10 @@ import {TokenStakingNode} from "src/ynEIGEN/TokenStakingNode.sol"; import {AssetRegistry} from "src/ynEIGEN/AssetRegistry.sol"; import {ynEigenDepositAdapter} from "src/ynEIGEN/ynEigenDepositAdapter.sol"; import {ynEigenViewer} from "src/ynEIGEN/ynEigenViewer.sol"; -import {YnEigenInit} from "./YnEigenStructs.sol"; +import {YnEigenInit, YnEigenProxies} from "./YnEigenStructs.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; import {IwstETH} from "src/external/lido/IwstETH.sol"; import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; @@ -47,56 +50,44 @@ contract YnEigenFactory is IYnEigenFactory { proxy = address(new TransparentUpgradeableProxy(implementation, controller, "")); } - function deploy(YnEigenInit calldata init) - external - returns ( - ynEigen ynToken, - EigenStrategyManager eigenStrategyManager, - TokenStakingNodesManager tokenStakingNodesManager, - TokenStakingNode tokenStakingNode, - AssetRegistry assetRegistry, - ynEigenDepositAdapter ynEigenDepositAdapterInstance, - IRateProvider rateProvider, - TimelockController timelock, - ynEigenViewer viewer - ) - { + function deploy(YnEigenInit calldata init) external returns (YnEigenProxies memory _proxies) { + // Deploy proxies - ynToken = ynEigen(_deployProxy(init.implementations.ynEigen, init.timelock)); - eigenStrategyManager = - EigenStrategyManager(_deployProxy(init.implementations.eigenStrategyManager, init.timelock)); - tokenStakingNodesManager = - TokenStakingNodesManager(_deployProxy(init.implementations.tokenStakingNodesManager, init.timelock)); - tokenStakingNode = TokenStakingNode(init.implementations.tokenStakingNode); - assetRegistry = AssetRegistry(_deployProxy(init.implementations.assetRegistry, init.timelock)); - ynEigenDepositAdapterInstance = - ynEigenDepositAdapter(_deployProxy(init.implementations.depositAdapter, init.timelock)); - rateProvider = IRateProvider(_deployProxy(init.implementations.rateProvider, init.timelock)); - timelock = TimelockController(payable(init.timelock)); + _proxies.ynToken = ynEigen(_deployProxy(init.implementations.ynEigen, init.timelock)); + _proxies.eigenStrategyManager = EigenStrategyManager(_deployProxy(init.implementations.eigenStrategyManager, init.timelock)); + _proxies.tokenStakingNodesManager = TokenStakingNodesManager(_deployProxy(init.implementations.tokenStakingNodesManager, init.timelock)); + _proxies.tokenStakingNode = TokenStakingNode(init.implementations.tokenStakingNode); + _proxies.assetRegistry = AssetRegistry(_deployProxy(init.implementations.assetRegistry, init.timelock)); + _proxies.ynEigenDepositAdapterInstance = ynEigenDepositAdapter(_deployProxy(init.implementations.depositAdapter, init.timelock)); + _proxies.rateProvider = IRateProvider(_deployProxy(init.implementations.rateProvider, init.timelock)); + _proxies.redemptionAssetsVault = RedemptionAssetsVault(_deployProxy(init.implementations.redemptionAssetsVault, init.timelock)); + _proxies.withdrawalQueueManager = WithdrawalQueueManager(_deployProxy(init.implementations.withdrawalQueueManager, init.timelock)); + _proxies.lsdWrapper = LSDWrapper(_deployProxy(init.implementations.lsdWrapper, init.timelock)); + _proxies.timelock = TimelockController(payable(init.timelock)); // Initialize ynToken - ynToken.initialize( + _proxies.ynToken.initialize( ynEigen.Init({ name: init.name, symbol: init.symbol, admin: init.actors.ADMIN, pauser: init.actors.PAUSE_ADMIN, unpauser: init.actors.UNPAUSE_ADMIN, - yieldNestStrategyManager: address(eigenStrategyManager), - assetRegistry: IAssetRegistry(address(assetRegistry)), + yieldNestStrategyManager: address(_proxies.eigenStrategyManager), + assetRegistry: IAssetRegistry(address(_proxies.assetRegistry)), pauseWhitelist: new address[](0) }) ); // Initialize eigenStrategyManager - eigenStrategyManager.initialize( + _proxies.eigenStrategyManager.initialize( EigenStrategyManager.Init({ assets: init.assets, strategies: init.strategies, - ynEigen: IynEigen(address(ynToken)), + ynEigen: IynEigen(address(_proxies.ynToken)), strategyManager: IStrategyManager(init.chainAddresses.STRATEGY_MANAGER), delegationManager: IDelegationManager(init.chainAddresses.DELEGATION_MANAGER), - tokenStakingNodesManager: ITokenStakingNodesManager(address(tokenStakingNodesManager)), + tokenStakingNodesManager: ITokenStakingNodesManager(address(_proxies.tokenStakingNodesManager)), admin: init.actors.ADMIN, pauser: init.actors.PAUSE_ADMIN, unpauser: init.actors.UNPAUSE_ADMIN, @@ -108,12 +99,12 @@ contract YnEigenFactory is IYnEigenFactory { ); // Initialize assetRegistry - assetRegistry.initialize( + _proxies.assetRegistry.initialize( AssetRegistry.Init({ assets: init.assets, - rateProvider: IRateProvider(address(rateProvider)), - yieldNestStrategyManager: IYieldNestStrategyManager(address(eigenStrategyManager)), - ynEigen: IynEigen(address(ynToken)), + rateProvider: IRateProvider(address(_proxies.rateProvider)), + yieldNestStrategyManager: IYieldNestStrategyManager(address(_proxies.eigenStrategyManager)), + ynEigen: IynEigen(address(_proxies.ynToken)), admin: init.actors.ADMIN, pauser: init.actors.PAUSE_ADMIN, unpauser: init.actors.UNPAUSE_ADMIN, @@ -122,13 +113,13 @@ contract YnEigenFactory is IYnEigenFactory { ); // Initialize tokenStakingNodesManager - tokenStakingNodesManager.initialize( + _proxies.tokenStakingNodesManager.initialize( TokenStakingNodesManager.Init({ admin: address(this), // Placeholder; changed post tokenStakingNode registration stakingAdmin: address(this), // Placeholder; changed post tokenStakingNode registration strategyManager: IStrategyManager(init.chainAddresses.STRATEGY_MANAGER), delegationManager: IDelegationManager(init.chainAddresses.DELEGATION_MANAGER), - yieldNestStrategyManager: address(eigenStrategyManager), + yieldNestStrategyManager: address(_proxies.eigenStrategyManager), maxNodeCount: init.maxNodeCount, pauser: init.actors.PAUSE_ADMIN, unpauser: init.actors.UNPAUSE_ADMIN, @@ -138,19 +129,53 @@ contract YnEigenFactory is IYnEigenFactory { }) ); + // initialize eigenStrategyManager + { + _proxies.eigenStrategyManager.initializeV2(address(_proxies.redemptionAssetsVault), address(_proxies.lsdWrapper), init.actors.ADMIN); + } + + // initialize RedemptionAssetsVault + { + RedemptionAssetsVault.Init memory _init = RedemptionAssetsVault.Init({ + admin: init.actors.ADMIN, + redeemer: address(_proxies.withdrawalQueueManager), + ynEigen: _proxies.ynToken, + assetRegistry: _proxies.assetRegistry + }); + _proxies.redemptionAssetsVault.initialize(_init); + } + + // initialize WithdrawalQueueManager + { + WithdrawalQueueManager.Init memory _init = WithdrawalQueueManager.Init({ + name: "ynLSDe Withdrawal Manager", + symbol: "ynLSDeWM", + redeemableAsset: IRedeemableAsset(address(_proxies.ynToken)), + redemptionAssetsVault: _proxies.redemptionAssetsVault, + admin: init.actors.ADMIN, + withdrawalQueueAdmin: init.actors.ADMIN, + redemptionAssetWithdrawer: init.actors.ADMIN, + requestFinalizer: init.actors.ADMIN, + // withdrawalFee: 500, // 0.05% + withdrawalFee: 0, + feeReceiver: init.actors.ADMIN + }); + _proxies.withdrawalQueueManager.initialize(_init); + } + // Register tokenStakingNode - tokenStakingNodesManager.registerTokenStakingNode(address(tokenStakingNode)); + _proxies.tokenStakingNodesManager.registerTokenStakingNode(address(_proxies.tokenStakingNode)); // Set roles post tokenStakingNode registration - tokenStakingNodesManager.grantRole(tokenStakingNodesManager.DEFAULT_ADMIN_ROLE(), init.actors.ADMIN); - tokenStakingNodesManager.grantRole(tokenStakingNodesManager.STAKING_ADMIN_ROLE(), init.timelock); - tokenStakingNodesManager.revokeRole(tokenStakingNodesManager.STAKING_ADMIN_ROLE(), address(this)); - tokenStakingNodesManager.revokeRole(tokenStakingNodesManager.DEFAULT_ADMIN_ROLE(), address(this)); + _proxies.tokenStakingNodesManager.grantRole(_proxies.tokenStakingNodesManager.DEFAULT_ADMIN_ROLE(), init.actors.ADMIN); + _proxies.tokenStakingNodesManager.grantRole(_proxies.tokenStakingNodesManager.STAKING_ADMIN_ROLE(), init.timelock); + _proxies.tokenStakingNodesManager.revokeRole(_proxies.tokenStakingNodesManager.STAKING_ADMIN_ROLE(), address(this)); + _proxies.tokenStakingNodesManager.revokeRole(_proxies.tokenStakingNodesManager.DEFAULT_ADMIN_ROLE(), address(this)); // ynEigenDepositAdapter - ynEigenDepositAdapterInstance.initialize( + _proxies.ynEigenDepositAdapterInstance.initialize( ynEigenDepositAdapter.Init({ - ynEigen: address(ynToken), + ynEigen: address(_proxies.ynToken), wstETH: init.chainAddresses.WSTETH_ADDRESS, woETH: init.chainAddresses.WOETH_ADDRESS, admin: init.actors.ADMIN @@ -160,21 +185,21 @@ contract YnEigenFactory is IYnEigenFactory { // ynEigenViewer { ynEigenViewer viewerImplementation = new ynEigenViewer( - address(assetRegistry), address(ynToken), address(tokenStakingNodesManager), address(rateProvider) + address(_proxies.assetRegistry), address(_proxies.ynToken), address(_proxies.tokenStakingNodesManager), address(_proxies.rateProvider) ); - viewer = ynEigenViewer(_deployProxy(address(viewerImplementation), init.actors.YN_SECURITY_COUNCIL)); + _proxies.viewer = ynEigenViewer(_deployProxy(address(viewerImplementation), init.actors.YN_SECURITY_COUNCIL)); } emit YnEigenDeployed( - ynToken, - eigenStrategyManager, - tokenStakingNodesManager, - tokenStakingNode, - assetRegistry, - ynEigenDepositAdapterInstance, - rateProvider, - timelock, - viewer + _proxies.ynToken, + _proxies.eigenStrategyManager, + _proxies.tokenStakingNodesManager, + _proxies.tokenStakingNode, + _proxies.assetRegistry, + _proxies.ynEigenDepositAdapterInstance, + _proxies.rateProvider, + _proxies.timelock, + _proxies.viewer ); } } diff --git a/script/ynEigen/YnEigenScript.s.sol b/script/ynEigen/YnEigenScript.s.sol index f8ee44338..e6d0d7555 100644 --- a/script/ynEigen/YnEigenScript.s.sol +++ b/script/ynEigen/YnEigenScript.s.sol @@ -7,6 +7,8 @@ import {console} from "lib/forge-std/src/console.sol"; contract YnEigenScript is YnEigenDeployer, YnEigenVerifier { function run(string memory _filePath) public { + // function run(string memory _filePath) public { + // string memory _filePath = "script/ynEigen/input/lsd-holesky.json"; _initDeployer(); _loadJson(_filePath); _validateNetwork(); diff --git a/script/ynEigen/YnEigenStructs.sol b/script/ynEigen/YnEigenStructs.sol index b6924338f..8cf4802d5 100644 --- a/script/ynEigen/YnEigenStructs.sol +++ b/script/ynEigen/YnEigenStructs.sol @@ -1,6 +1,27 @@ // SPDX-License-Identifier: BSD-3-Clause License pragma solidity ^0.8.24; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; +import {IRedeemableAsset} from "src/interfaces/IRedeemableAsset.sol"; +import {IynEigen} from "src/interfaces/IynEigen.sol"; +import {IRateProvider} from "src/interfaces/IRateProvider.sol"; +import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; +import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; +import {ynEigen} from "src/ynEIGEN/ynEigen.sol"; +import {TokenStakingNode} from "src/ynEIGEN/TokenStakingNode.sol"; +import {EigenStrategyManager} from "src/ynEIGEN/EigenStrategyManager.sol"; +import {AssetRegistry} from "src/ynEIGEN/AssetRegistry.sol"; +import {TokenStakingNodesManager} from "src/ynEIGEN/TokenStakingNodesManager.sol"; +import {TokenStakingNode} from "src/ynEIGEN/TokenStakingNode.sol"; +import {AssetRegistry} from "src/ynEIGEN/AssetRegistry.sol"; +import {ynEigenDepositAdapter} from "src/ynEIGEN/ynEigenDepositAdapter.sol"; +import {ynEigenViewer} from "src/ynEIGEN/ynEigenViewer.sol"; +import {YnEigenInit, YnEigenImplementations} from "./YnEigenStructs.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; + import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; @@ -32,6 +53,24 @@ struct YnEigenImplementations { address tokenStakingNode; address assetRegistry; address depositAdapter; + address redemptionAssetsVault; + address withdrawalQueueManager; + address lsdWrapper; +} + +struct YnEigenProxies { + ynEigen ynToken; + EigenStrategyManager eigenStrategyManager; + TokenStakingNodesManager tokenStakingNodesManager; + TokenStakingNode tokenStakingNode; + AssetRegistry assetRegistry; + ynEigenDepositAdapter ynEigenDepositAdapterInstance; + IRateProvider rateProvider; + TimelockController timelock; + ynEigenViewer viewer; + RedemptionAssetsVault redemptionAssetsVault; + WithdrawalQueueManager withdrawalQueueManager; + LSDWrapper lsdWrapper; } struct YnEigenInit { diff --git a/script/ynEigen/YnUpgrader.s.sol b/script/ynEigen/YnUpgrader.s.sol new file mode 100644 index 000000000..d0fb98973 --- /dev/null +++ b/script/ynEigen/YnUpgrader.s.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; +import {IRedeemableAsset} from "src/interfaces/IRedeemableAsset.sol"; +import {IRedemptionAssetsVault} from "src/interfaces/IRedemptionAssetsVault.sol"; +import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; +import {IynEigen} from "src/interfaces/IynEigen.sol"; +import {ynEigen} from "src/ynEIGEN/ynEigen.sol"; + +import "forge-std/console.sol"; + +import "./BaseYnEigenScript.s.sol"; + +// ---- Usage ---- + +// deploy: +// forge script script/ynEigen/YnUpgrader.s.sol:YnUpgrader --verify --slow --legacy --etherscan-api-key $KEY --rpc-url $RPC_URL --broadcast + +// verify: +// --constructor-args $(cast abi-encode "constructor(address)" 0x5C1E6bA712e9FC3399Ee7d5824B6Ec68A0363C02) +// forge verify-contract --etherscan-api-key $KEY --watch --chain-id $CHAIN_ID --compiler-version $FULL_COMPILER_VER --verifier-url $VERIFIER_URL $ADDRESS $PATH:$FILE_NAME + +contract YnUpgrader is BaseYnEigenScript { + + address yneigen = 0xcc40b0BB00199Cdd15f7df9dC4E2B60AB273b56E; + address assetRegistry = 0x32Df3aC2fFD2CD53a4AAb8b7EB68798033B41EAF; + address tokenStakingNodeImpl = 0x63f01b695c67B764e823F972bc61fcAFbac5102b; + + function run() public { + + vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY")); + + // deploy RedemptionAssetsVault + TransparentUpgradeableProxy _redemptionAssetsVaultProxy; + address _redemptionAssetsVaultImpl; + { + _redemptionAssetsVaultImpl = address(new RedemptionAssetsVault()); + _redemptionAssetsVaultProxy = new TransparentUpgradeableProxy( + _redemptionAssetsVaultImpl, + actors.admin.PROXY_ADMIN_OWNER, + "" + ); + } + + // deploy WithdrawalQueueManager + TransparentUpgradeableProxy _withdrawalQueueManagerProxy; + address _withdrawalQueueManagerImpl; + { + _withdrawalQueueManagerImpl = address(new WithdrawalQueueManager()); + _withdrawalQueueManagerProxy = new TransparentUpgradeableProxy( + _withdrawalQueueManagerImpl, + actors.admin.PROXY_ADMIN_OWNER, + "" + ); + } + + // deploy wrapper + TransparentUpgradeableProxy _wrapperProxy; + address _wrapperImpl; + { + _wrapperImpl = address(new LSDWrapper( + chainAddresses.lsd.WSTETH_ADDRESS, + chainAddresses.lsd.WOETH_ADDRESS, + chainAddresses.lsd.OETH_ADDRESS, + chainAddresses.lsd.STETH_ADDRESS + )); + _wrapperProxy = new TransparentUpgradeableProxy( + _wrapperImpl, + actors.admin.PROXY_ADMIN_OWNER, + abi.encodeWithSignature("initialize()") + ); + } + + // initialize RedemptionAssetsVault + { + RedemptionAssetsVault.Init memory _init = RedemptionAssetsVault.Init({ + admin: actors.admin.PROXY_ADMIN_OWNER, + redeemer: address(_withdrawalQueueManagerProxy), + ynEigen: IynEigen(address(yneigen)), + assetRegistry: IAssetRegistry(assetRegistry) + }); + RedemptionAssetsVault(address(_redemptionAssetsVaultProxy)).initialize(_init); + } + + // initialize WithdrawalQueueManager + { + WithdrawalQueueManager.Init memory _init = WithdrawalQueueManager.Init({ + name: "ynLSDe Withdrawal Manager", + symbol: "ynLSDeWM", + redeemableAsset: IRedeemableAsset(yneigen), + redemptionAssetsVault: IRedemptionAssetsVault(address(_redemptionAssetsVaultProxy)), + admin: actors.admin.PROXY_ADMIN_OWNER, + withdrawalQueueAdmin: actors.ops.WITHDRAWAL_MANAGER, + redemptionAssetWithdrawer: actors.ops.REDEMPTION_ASSET_WITHDRAWER, + requestFinalizer: actors.ops.REQUEST_FINALIZER, + withdrawalFee: 0, + feeReceiver: actors.admin.FEE_RECEIVER + }); + WithdrawalQueueManager(address(_withdrawalQueueManagerProxy)).initialize(_init); + } + + vm.stopBroadcast(); + + console.log("====================================="); + console.log("====================================="); + // console.log("_redemptionAssetsVaultProxy: ", address(_redemptionAssetsVaultProxy)); + // console.log("_redemptionAssetsVaultImpl: ", address(_redemptionAssetsVaultImpl)); + // console.log("_withdrawalQueueManagerProxy: ", address(_withdrawalQueueManagerProxy)); + // console.log("_withdrawalQueueManagerImpl: ", address(_withdrawalQueueManagerImpl)); + // console.log("_wrapperProxy: ", address(_wrapperProxy)); + // console.log("_wrapperImpl: ", address(_wrapperImpl)); + _printUpgradeTokenStakingNodeImplementationData(); + _printBurnerRole(); + console.log("====================================="); + console.log("====================================="); + + } + + function _printUpgradeTokenStakingNodeImplementationData() private view { + bytes memory _data = abi.encodeWithSignature( + "upgradeTokenStakingNode(address)", + tokenStakingNodeImpl + ); + console.log("====================================="); + console.log("_printUpgradeTokenStakingNodeImplementationData"); + console.logBytes(_data); + console.log("====================================="); + } + + function _printBurnerRole() private view { + console.log("====================================="); + console.log("ynEigen(yneigen).BURNER_ROLE(): "); + console.logBytes32(ynEigen(yneigen).BURNER_ROLE()); + console.log("====================================="); + } + +// ===================================== +// ===================================== +// _redemptionAssetsVaultProxy: 0xF5efA92F85457Ed22722783917B55318b75815bf +// _redemptionAssetsVaultImpl: 0xD82d61C2905595f554233De5cDb947979802267C +// _withdrawalQueueManagerProxy: 0xeCA746232f297bBD968B4eF240fb28c40BE5CCB7 +// _withdrawalQueueManagerImpl: 0xcADC2A8Ccf396088f8D7520Ae8fD249CcfCC20Db +// _wrapperProxy: 0x63Bc6100DF15a5553715d453570d07f62B400D85 +// _wrapperImpl: 0x2E66Fc93a48877cAdE526dB164afA04EBe9be449 +} \ No newline at end of file diff --git a/src/interfaces/IAssetRegistry.sol b/src/interfaces/IAssetRegistry.sol index 2e759f4ba..41c879ef4 100644 --- a/src/interfaces/IAssetRegistry.sol +++ b/src/interfaces/IAssetRegistry.sol @@ -15,11 +15,13 @@ interface IAssetRegistry { AssetStatus status; } + function getAssets() external view returns (IERC20[] memory); function assetData(IERC20 asset) external view returns (AssetData memory); function addAsset(IERC20 asset) external; function disableAsset(IERC20 asset) external; function deleteAsset(IERC20 asset) external; function totalAssets() external view returns (uint256); function convertToUnitOfAccount(IERC20 asset, uint256 amount) external view returns (uint256); + function convertFromUnitOfAccount(IERC20 asset, uint256 amount) external view returns (uint256); function assetIsSupported(IERC20 asset) external view returns (bool); } diff --git a/src/interfaces/IEigenStrategyManager.sol b/src/interfaces/IEigenStrategyManager.sol index c79372011..a9912c2b7 100644 --- a/src/interfaces/IEigenStrategyManager.sol +++ b/src/interfaces/IEigenStrategyManager.sol @@ -5,7 +5,8 @@ import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStra import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IwstETH} from "src/external/lido/IwstETH.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; - +import {IynEigen} from "src/interfaces/IynEigen.sol"; +import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; interface IEigenStrategyManager { @@ -21,4 +22,6 @@ interface IEigenStrategyManager { function woETH() external view returns (IERC4626); function oETH() external view returns (IERC20); function stETH() external view returns (IERC20); + function ynEigen() external view returns (IynEigen); + function tokenStakingNodesManager() external view returns (ITokenStakingNodesManager); } \ No newline at end of file diff --git a/src/interfaces/IRedemptionAssetsVault.sol b/src/interfaces/IRedemptionAssetsVault.sol index 343b2fb32..9a7493e69 100644 --- a/src/interfaces/IRedemptionAssetsVault.sol +++ b/src/interfaces/IRedemptionAssetsVault.sol @@ -9,6 +9,7 @@ interface IRedemptionAssetsVault { event AssetsDeposited( address indexed asset, address indexed depositor, uint256 amount); event AssetTransferred(address indexed asset, address indexed redeemer, address indexed to, uint256 amount); event AssetWithdrawn(address indexed asset, address indexed redeemer, address indexed to, uint256 amount); + event TotalAssetsTransferred(address indexed asset, address indexed from, address indexed to, uint256 amount); /// @notice Transfers redemption assets to a specified address based on redemption. /// @dev This is only for INTERNAL USE diff --git a/src/interfaces/ITokenStakingNode.sol b/src/interfaces/ITokenStakingNode.sol index 2145c4970..e68f57f99 100644 --- a/src/interfaces/ITokenStakingNode.sol +++ b/src/interfaces/ITokenStakingNode.sol @@ -5,6 +5,7 @@ import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.so import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces/ISignatureUtils.sol"; import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; interface ITokenStakingNode { @@ -37,4 +38,20 @@ interface ITokenStakingNode { ) external; function undelegate() external; + + function getQueuedSharesAndWithdrawn(IStrategy _strategy, IERC20 _asset) external view returns (uint256, uint256); + function queueWithdrawals(IStrategy _strategy, uint256 _shares) external returns (bytes32[] memory _fullWithdrawalRoots); + function completeQueuedWithdrawals( + uint256 _nonce, + uint32 _startBlock, + uint256 _shares, + IStrategy _strategy, + uint256[] memory _middlewareTimesIndexes, + bool updateTokenStakingNodesBalances + ) external; + + function deallocateTokens(IERC20 _token, uint256 _amount) external; + + function queuedShares(IStrategy _strategy) external view returns (uint256); + function withdrawn(IERC20 _token) external view returns (uint256); } diff --git a/src/interfaces/ITokenStakingNodesManager.sol b/src/interfaces/ITokenStakingNodesManager.sol index e6c2797a5..5dfb5f26c 100644 --- a/src/interfaces/ITokenStakingNodesManager.sol +++ b/src/interfaces/ITokenStakingNodesManager.sol @@ -24,4 +24,6 @@ interface ITokenStakingNodesManager { function hasYieldNestStrategyManagerRole(address) external view returns (bool); function getNodeById(uint256 nodeId) external view returns (ITokenStakingNode); + + function yieldNestStrategyManager() external view returns (address); } diff --git a/src/interfaces/IWrapper.sol b/src/interfaces/IWrapper.sol new file mode 100644 index 000000000..025d5ee31 --- /dev/null +++ b/src/interfaces/IWrapper.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +interface IWrapper { + + /// @notice Wraps the given amount of the given token. + /// @param _amount The amount to wrap. + /// @param _token The token to wrap. + /// @return The amount of wrapped tokens and the wrapped token. + function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20); + + /// @notice Unwraps the given amount of the given token. + /// @param _amount The amount to unwrap. + /// @param _token The token to unwrap. + /// @return The amount of unwrapped tokens and the unwrapped token. + function unwrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20); + + /// @notice Converts the user's underlying asset amount to the equivalent user asset amount. + /// @dev This function handles the conversion for wrapped staked ETH (wstETH) and wrapped other ETH (woETH), + /// returning the equivalent amount in the respective wrapped token. + /// @param _asset The ERC20 token for which the conversion is being made. + /// @param _userUnderlyingView The amount of the underlying asset. + /// @return The equivalent amount in the user asset denomination. + function toUserAssetAmount(IERC20 _asset, uint256 _userUnderlyingView) external view returns (uint256); +} \ No newline at end of file diff --git a/src/interfaces/IYieldNestStrategyManager.sol b/src/interfaces/IYieldNestStrategyManager.sol index 5f2aaf448..f690a8ca0 100644 --- a/src/interfaces/IYieldNestStrategyManager.sol +++ b/src/interfaces/IYieldNestStrategyManager.sol @@ -2,9 +2,26 @@ pragma solidity ^0.8.24; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; +import {IRedemptionAssetsVault} from "src/interfaces/IRedemptionAssetsVault.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; + + +interface IRedemptionAssetsVaultExt is IRedemptionAssetsVault { + function deposit(uint256 amount, address asset) external; + function balances(address asset) external view returns (uint256 amount); + function assetBalances(IERC20[] calldata assetsArray) external view returns (uint256[] memory balances); +} interface IYieldNestStrategyManager { + struct WithdrawalAction { + uint256 nodeId; + uint256 amountToReinvest; + uint256 amountToQueue; + address asset; + } + function getStakedAssetsBalances( IERC20[] calldata assets ) external view returns (uint256[] memory stakedBalances); @@ -13,4 +30,15 @@ interface IYieldNestStrategyManager { function supportsAsset(IERC20 asset) external view returns (bool); + function tokenStakingNodesManager() external view returns (ITokenStakingNodesManager); + + function processPrincipalWithdrawals(WithdrawalAction[] calldata _actions) external; + + function redemptionAssetsVault() external view returns (IRedemptionAssetsVaultExt); + + function wrapper() external view returns (IWrapper); + + function isStakingNodesWithdrawer(address _address) external view returns (bool); + + function updateTokenStakingNodesBalances(IERC20 asset) external; } \ No newline at end of file diff --git a/src/interfaces/IynEigen.sol b/src/interfaces/IynEigen.sol index 5a29131bb..d6d7737b5 100644 --- a/src/interfaces/IynEigen.sol +++ b/src/interfaces/IynEigen.sol @@ -17,11 +17,20 @@ interface IynEigen is IERC20 { function previewDeposit(IERC20 asset, uint256 amount) external view returns (uint256); + function burn(uint256 amount) external; + + function previewRedeem(IERC20 asset, uint256 shares) external view returns (uint256 assets); + function previewRedeem(uint256 shares) external view returns (uint256); + + function convertToAssets(IERC20 asset, uint256 shares) external view returns (uint256); + function retrieveAssets( IERC20[] calldata assetsToRetrieve, uint256[] calldata amounts ) external; + function processWithdrawn(uint256 _amount, address _asset) external; + function assetBalances(IERC20[] calldata assetsArray) external view returns (uint256[] memory balances); function assetBalance(IERC20 asset) external view returns (uint256 balance); diff --git a/src/ynEIGEN/AssetRegistry.sol b/src/ynEIGEN/AssetRegistry.sol index 9e5fff958..a3c10c8fc 100644 --- a/src/ynEIGEN/AssetRegistry.sol +++ b/src/ynEIGEN/AssetRegistry.sol @@ -10,7 +10,7 @@ import {IRateProvider} from "src/interfaces/IRateProvider.sol"; import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; import "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; - +import {IRedemptionAssetsVaultExt} from "src/interfaces/IYieldNestStrategyManager.sol"; interface IAssetRegistryEvents { event AssetAdded(address indexed asset); @@ -29,7 +29,6 @@ interface IAssetRegistryEvents { //---------------------------------- ERRORS ------------------------------------------ //-------------------------------------------------------------------------------------- - error UnsupportedAsset(IERC20 asset); error Paused(); error AssetNotActive(address inactiveAsset); error AssetAlreadyActive(address asset); @@ -37,7 +36,6 @@ interface IAssetRegistryEvents { error AssetBalanceNonZeroInPool(uint256 balanceInPool); error AssetBalanceNonZeroInStrategyManager(uint256 balanceInStrategyManager); error AssetNotFound(address absentAsset); - error ZeroAmount(); error ZeroAddress(); error LengthMismatch(uint256 length1, uint256 length2); error AssetAlreadyAvailable(address asset); @@ -103,29 +101,13 @@ interface IAssetRegistryEvents { _grantRole(ASSET_MANAGER_ROLE, init.assetManagerRole); strategyManager = init.yieldNestStrategyManager; - + rateProvider = init.rateProvider; + ynEigen = init.ynEigen; + uint256 assetsLength = init.assets.length; for (uint256 i = 0; i < assetsLength; i++) { - IERC20 asset = init.assets[i]; - if (address(asset) == address(0)) { - revert ZeroAddress(); - } - if (_assetData[asset].status == AssetStatus.Active) { - revert AssetAlreadyActive(address(asset)); - } - - if (!strategyManager.supportsAsset(asset)) { - revert NoStrategyDefinedForAsset(asset); - } - - assets.push(asset); - _assetData[asset] = AssetData({ - status: AssetStatus.Active - }); + _addAsset(init.assets[i]); } - - rateProvider = init.rateProvider; - ynEigen = init.ynEigen; } //-------------------------------------------------------------------------------------- @@ -142,6 +124,10 @@ interface IAssetRegistryEvents { onlyRole(ASSET_MANAGER_ROLE) notZeroAddress(address(asset)) whenNotPaused { + _addAsset(asset); + } + + function _addAsset(IERC20 asset) private { if (_assetData[asset].status != AssetStatus.Unavailable) { revert AssetAlreadyAvailable(address(asset)); } @@ -286,8 +272,15 @@ interface IAssetRegistryEvents { revert LengthMismatch(assetsCount, stakedAssetBalances.length); } + IRedemptionAssetsVaultExt redemptionAssetsVault = strategyManager.redemptionAssetsVault(); + uint256[] memory redemptionAssetBalances = new uint256[](assetsCount); + if (address(redemptionAssetsVault) != address(0)) { + redemptionAssetBalances = redemptionAssetsVault.assetBalances(assets); + } + for (uint256 i = 0; i < assetsCount; i++) { assetBalances[i] += stakedAssetBalances[i]; + assetBalances[i] += redemptionAssetBalances[i]; } } @@ -315,6 +308,14 @@ interface IAssetRegistryEvents { : assetRate * amount / 1e18; } + function convertFromUnitOfAccount(IERC20 asset, uint256 amount) public view returns (uint256) { + uint256 assetRate = rateProvider.rate(address(asset)); + uint8 assetDecimals = IERC20Metadata(address(asset)).decimals(); + return assetDecimals != 18 + ? amount * (10 ** assetDecimals) / assetRate + : amount * 1e18 / assetRate; + } + //-------------------------------------------------------------------------------------- //---------------------------------- PAUSING ----------------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/EigenStrategyManager.sol b/src/ynEIGEN/EigenStrategyManager.sol index 86599085d..3d8bfb5e2 100644 --- a/src/ynEIGEN/EigenStrategyManager.sol +++ b/src/ynEIGEN/EigenStrategyManager.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: BSD 3-Clause License pragma solidity ^0.8.24; +import {SafeCast} from "lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import {AccessControlUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; import {IStrategyManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfaces/IDelegationManager.sol"; import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; -import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; +import {IYieldNestStrategyManager, IRedemptionAssetsVaultExt} from "src/interfaces/IYieldNestStrategyManager.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; import {ITokenStakingNode} from "src/interfaces/ITokenStakingNode.sol"; @@ -15,12 +16,19 @@ import {IynEigen} from "src/interfaces/IynEigen.sol"; import {IwstETH} from "src/external/lido/IwstETH.sol"; import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - +import {IWrapper} from "src/interfaces/IWrapper.sol"; +import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; interface IEigenStrategyManagerEvents { event StrategyAdded(address indexed asset, address indexed strategy); event StakedAssetsToNode(uint256 indexed nodeId, IERC20[] assets, uint256[] amounts); event DepositedToEigenlayer(IERC20[] depositAssets, uint256[] depositAmounts, IStrategy[] strategiesForNode); + event PrincipalWithdrawalProcessed(uint256 indexed nodeId, address indexed asset, uint256 amountToReinvest, uint256 amountToQueue); + event StrategyBalanceUpdated(address indexed asset, address indexed strategy, uint256 nodeCount, uint128 stakedBalance, uint128 withdrawnBalance); +} + +interface IynEigenVars { + function assetRegistry() external view returns (IAssetRegistry); } /** @title EigenStrategyManager @@ -78,6 +86,12 @@ contract EigenStrategyManager is /// @notice Role allowed to manage strategies bytes32 public constant STRATEGY_ADMIN_ROLE = keccak256("STRATEGY_ADMIN_ROLE"); + /// @notice Role allowed to manage withdrawals + bytes32 public constant WITHDRAWAL_MANAGER_ROLE = keccak256("WITHDRAWAL_MANAGER_ROLE"); + + /// @notice Role allowed to withdraw from staking nodes + bytes32 public constant STAKING_NODES_WITHDRAWER_ROLE = keccak256("STAKING_NODES_WITHDRAWER_ROLE"); + //-------------------------------------------------------------------------------------- //---------------------------------- CONSTANTS --------------------------------------- //-------------------------------------------------------------------------------------- @@ -100,10 +114,23 @@ contract EigenStrategyManager is IERC20 public oETH; IERC20 public stETH; + IRedemptionAssetsVaultExt public redemptionAssetsVault; + IWrapper public wrapper; + + struct StrategyBalance { + uint128 stakedBalance; + uint128 withdrawnBalance; + } + mapping(IStrategy => StrategyBalance) public strategiesBalance; + //-------------------------------------------------------------------------------------- //---------------------------------- INITIALIZATION ---------------------------------- //-------------------------------------------------------------------------------------- + constructor() { + _disableInitializers(); + } + struct Init { IERC20[] assets; IStrategy[] strategies; @@ -156,8 +183,90 @@ contract EigenStrategyManager is oETH = IERC20(woETH.asset()); } + function initializeV2( + address _redemptionAssetsVault, + address _wrapper, + address _withdrawer + ) external reinitializer(2) notZeroAddress(_redemptionAssetsVault) notZeroAddress(_wrapper) { + __ReentrancyGuard_init(); + + redemptionAssetsVault = IRedemptionAssetsVaultExt(_redemptionAssetsVault); + wrapper = IWrapper(_wrapper); + + _grantRole(STAKING_NODES_WITHDRAWER_ROLE, _withdrawer); + _grantRole(WITHDRAWAL_MANAGER_ROLE, _withdrawer); + + IERC20[] memory assets = IynEigenVars(address(ynEigen)).assetRegistry().getAssets(); + uint256 assetsLength = assets.length; + for (uint256 i = 0; i < assetsLength; i++) { + _updateTokenStakingNodesBalances(assets[i], IStrategy(address(0))); + } + } + //-------------------------------------------------------------------------------------- - //---------------------------------- STRATEGY ---------------------------------------- + //------------------------------------ ACCOUNTING ---------------------------------------- + //-------------------------------------------------------------------------------------- + + /// @notice Updates the staked balances for all nodes for a specific asset's strategy. + /// @dev This function should be called after any operation that changes node balances. + /// @dev In case of slashing events, users are incentivized to call this function to adjust the exchange rate. + /// @param asset The ERC20 token for which the balances are to be updated. + function updateTokenStakingNodesBalances(IERC20 asset) public { + _updateTokenStakingNodesBalances(asset, strategies[asset]); + } + + /// @notice Updates the staked balances for all nodes for a strategies. + /// @dev Should be called atomically after any node-balance-changing operation. + /// @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) internal { + + ITokenStakingNode[] memory nodes = tokenStakingNodesManager.getAllNodes(); + uint256 nodesCount = nodes.length; + + uint256 _strategiesBalance; + uint256 _strategiesWithdrawalQueueBalance; + uint256 _strategiesWithdrawnBalance; + if (address(strategy) == address(0)) strategy = strategies[asset]; + for (uint256 i; i < nodesCount; i++ ) { + ITokenStakingNode node = nodes[i]; + + _strategiesBalance += strategy.userUnderlyingView((address(node))); + + (uint256 queuedShares, uint256 strategyWithdrawnBalance) = node.getQueuedSharesAndWithdrawn(strategy, asset); + + if (queuedShares > 0) { + _strategiesWithdrawalQueueBalance += strategy.sharesToUnderlyingView(queuedShares); + } + + _strategiesWithdrawnBalance += strategyWithdrawnBalance; + } + + StrategyBalance memory _strategyBalance = StrategyBalance({ + stakedBalance: SafeCast.toUint128(_strategiesBalance + _strategiesWithdrawalQueueBalance), + withdrawnBalance: SafeCast.toUint128(_strategiesWithdrawnBalance) + }); + + + // update only if it changed + StrategyBalance memory previousStrategyBalance = strategiesBalance[strategy]; + if (previousStrategyBalance.stakedBalance != _strategyBalance.stakedBalance || + previousStrategyBalance.withdrawnBalance != _strategyBalance.withdrawnBalance) { + strategiesBalance[strategy] = _strategyBalance; + + emit StrategyBalanceUpdated( + address(asset), + address(strategy), + nodesCount, + _strategyBalance.stakedBalance, + _strategyBalance.withdrawnBalance + ); + } + } + + //-------------------------------------------------------------------------------------- + //------------------------------------ DEPOSIT ---------------------------------------- //-------------------------------------------------------------------------------------- /** @@ -215,14 +324,21 @@ contract EigenStrategyManager is } strategiesForNode[i] = strategy; } - // Transfer assets to node + + // Transfer assets to address(this) ynEigen.retrieveAssets(assets, amounts); IERC20[] memory depositAssets = new IERC20[](assetsLength); uint256[] memory depositAmounts = new uint256[](amountsLength); + IWrapper _wrapper = wrapper; for (uint256 i = 0; i < assetsLength; i++) { - (IERC20 depositAsset, uint256 depositAmount) = toEigenLayerDeposit(assets[i], amounts[i]); + + (bool success, bytes memory result) = address(_wrapper).delegatecall( + abi.encodeWithSignature("unwrap(uint256,address)", amounts[i], address(assets[i])) + ); + require(success, "Delegatecall failed"); + (uint256 depositAmount, IERC20 depositAsset) = abi.decode(result, (uint256, IERC20)); depositAssets[i] = depositAsset; depositAmounts[i] = depositAmount; @@ -234,27 +350,48 @@ contract EigenStrategyManager is node.depositAssetsToEigenlayer(depositAssets, depositAmounts, strategiesForNode); + for (uint256 i = 0; i < assetsLength; i++) { + _updateTokenStakingNodesBalances(assets[i], IStrategy(address(0))); + } + emit DepositedToEigenlayer(depositAssets, depositAmounts, strategiesForNode); } - function toEigenLayerDeposit( - IERC20 asset, - uint256 amount - ) internal returns (IERC20 depositAsset, uint256 depositAmount) { - if (address(asset) == address(wstETH)) { - // Adjust for wstETH - depositAsset = stETH; - depositAmount = wstETH.unwrap(amount); - } else if (address(asset) == address(woETH)) { - // Adjust for woeth - depositAsset = oETH; - // calling redeem with receiver and owner as address(this) - depositAmount = woETH.redeem(amount, address(this), address(this)); - } else { - // No adjustment needed - depositAsset = asset; - depositAmount = amount; - } + //-------------------------------------------------------------------------------------- + //---------------------------------- WITHDRAWALS ------------------------------------- + //-------------------------------------------------------------------------------------- + + function processPrincipalWithdrawals( + WithdrawalAction[] calldata _actions + ) public onlyRole(WITHDRAWAL_MANAGER_ROLE) { + uint256 _len = _actions.length; + for (uint256 i = 0; i < _len; ++i) { + _processPrincipalWithdrawalForNode(_actions[i]); + } + } + + function _processPrincipalWithdrawalForNode(WithdrawalAction calldata _action) internal { + + uint256 _totalAmount = _action.amountToReinvest + _action.amountToQueue; + + ITokenStakingNode _node = tokenStakingNodesManager.getNodeById(_action.nodeId); + _node.deallocateTokens(IERC20(_action.asset), _totalAmount); + + if (_action.amountToReinvest > 0) { + IynEigen _ynEigen = ynEigen; + IERC20(_action.asset).forceApprove(address(_ynEigen), _action.amountToReinvest); + _ynEigen.processWithdrawn(_action.amountToReinvest, _action.asset); + } + + if (_action.amountToQueue > 0) { + IRedemptionAssetsVaultExt _redemptionAssetsVault = redemptionAssetsVault; + IERC20(_action.asset).forceApprove(address(_redemptionAssetsVault), _action.amountToQueue); + _redemptionAssetsVault.deposit(_action.amountToQueue, _action.asset); + } + + _updateTokenStakingNodesBalances(IERC20(_action.asset), IStrategy(address(0))); + + emit PrincipalWithdrawalProcessed(_action.nodeId, _action.asset, _action.amountToReinvest, _action.amountToQueue); } //-------------------------------------------------------------------------------------- @@ -293,42 +430,15 @@ contract EigenStrategyManager is stakedBalances = new uint256[](assets.length); // Add balances contained in each TokenStakingNode, including those managed by strategies. - ITokenStakingNode[] memory nodes = tokenStakingNodesManager.getAllNodes(); - uint256 nodesCount = nodes.length; uint256 assetsCount = assets.length; for (uint256 j = 0; j < assetsCount; j++) { - IERC20 asset = assets[j]; - for (uint256 i; i < nodesCount; i++ ) { - ITokenStakingNode node = nodes[i]; - - uint256 strategyBalance = toUserAssetAmount( - asset, - strategies[asset].userUnderlyingView((address(node))) - ); - stakedBalances[j] += strategyBalance; - } + IStrategy strategy = strategies[asset]; + StrategyBalance memory balance = strategiesBalance[strategy]; + stakedBalances[j] = wrapper.toUserAssetAmount(asset, balance.stakedBalance) + balance.withdrawnBalance; } - } - /** - * @notice Converts the user's underlying asset amount to the equivalent user asset amount. - * @dev This function handles the conversion for wrapped staked ETH (wstETH) and wrapped other ETH (woETH), - * returning the equivalent amount in the respective wrapped token. - * @param asset The ERC20 token for which the conversion is being made. - * @param userUnderlyingView The amount of the underlying asset. - * @return The equivalent amount in the user asset denomination. - */ - function toUserAssetAmount(IERC20 asset, uint256 userUnderlyingView) public view returns (uint256) { - if (address(asset) == address(wstETH)) { - // Adjust for wstETH using view method, converting stETH to wstETH - return wstETH.getWstETHByStETH(userUnderlyingView); - } - if (address(asset) == address(woETH)) { - // Adjust for woETH using view method, converting oETH to woETH - return woETH.previewDeposit(userUnderlyingView); - } - return userUnderlyingView; + return stakedBalances; } /** @@ -336,7 +446,7 @@ contract EigenStrategyManager is * @param asset The ERC20 token for which the staked balance is to be retrieved. * @return stakedBalance The total staked balance of the specified asset. */ - function getStakedAssetBalance(IERC20 asset) public view returns (uint256 stakedBalance) { + function getStakedAssetBalance(IERC20 asset) external view returns (uint256 stakedBalance) { if (address(strategies[asset]) == address(0)) { revert NoStrategyDefinedForAsset(address(asset)); } @@ -358,7 +468,7 @@ contract EigenStrategyManager is function getStakedAssetBalanceForNode( IERC20 asset, uint256 nodeId - ) public view returns (uint256 stakedBalance) { + ) external view returns (uint256 stakedBalance) { if (address(strategies[asset]) == address(0)) { revert NoStrategyDefinedForAsset(address(asset)); } @@ -372,11 +482,14 @@ contract EigenStrategyManager is ITokenStakingNode node ) internal view returns (uint256 stakedBalance) { - uint256 strategyBalance = toUserAssetAmount( + IStrategy strategy = strategies[asset]; + (uint256 queuedShares, uint256 strategyWithdrawnBalance) = node.getQueuedSharesAndWithdrawn(strategy, asset); + uint256 strategyBalance = wrapper.toUserAssetAmount( asset, - strategies[asset].userUnderlyingView((address(node))) + strategy.userUnderlyingView((address(node))) + strategy.sharesToUnderlyingView(queuedShares) ); - stakedBalance += strategyBalance; + + stakedBalance += strategyBalance + strategyWithdrawnBalance; } /** @@ -388,6 +501,15 @@ contract EigenStrategyManager is return address(strategies[asset]) != address(0); } + /** + * @notice Checks if the given address has the STAKING_NODES_WITHDRAWER_ROLE. + * @param _address The address to check. + * @return True if the address has the STAKING_NODES_WITHDRAWER_ROLE, false otherwise. + */ + function isStakingNodesWithdrawer(address _address) public view returns (bool) { + return hasRole(STAKING_NODES_WITHDRAWER_ROLE, _address); + } + //-------------------------------------------------------------------------------------- //---------------------------------- MODIFIERS --------------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/LSDWrapper.sol b/src/ynEIGEN/LSDWrapper.sol new file mode 100644 index 000000000..f2b917172 --- /dev/null +++ b/src/ynEIGEN/LSDWrapper.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {IERC20, SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IwstETH} from "src/external/lido/IwstETH.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; + +contract LSDWrapper is IWrapper, Initializable { + + using SafeERC20 for IERC20; + + IERC20 public immutable wstETH; + IERC20 public immutable woETH; + IERC20 public immutable oETH; + IERC20 public immutable stETH; + + // ============================================================================================ + // Constructor + // ============================================================================================ + + constructor(address _wstETH, address _woETH, address _oETH, address _stETH) { + _disableInitializers(); + + if (_wstETH == address(0) || _woETH == address(0) || _oETH == address(0) || _stETH == address(0)) { + revert ZeroAddress(); + } + + wstETH = IERC20(_wstETH); + woETH = IERC20(_woETH); + oETH = IERC20(_oETH); + stETH = IERC20(_stETH); + } + + function initialize() external initializer { + stETH.forceApprove(address(wstETH), type(uint256).max); + oETH.forceApprove(address(woETH), type(uint256).max); + } + + // ============================================================================================ + // External functions + // ============================================================================================ + + // /// @inheritdoc IWrapper + // function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + // if (_token == stETH) { + // stETH.safeTransferFrom(msg.sender, address(this), _amount); + // _amount = IwstETH(address(wstETH)).wrap(_amount); + // wstETH.safeTransfer(msg.sender, _amount); + // return (_amount, wstETH); + // } else if (_token == oETH) { + // oETH.safeTransferFrom(msg.sender, address(this), _amount); + // return (IERC4626(address(woETH)).deposit(_amount, msg.sender), woETH); + // } else { + // return (_amount, _token); + // } + // } + + // /// @inheritdoc IWrapper + // function unwrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + // if (_token == wstETH) { + // wstETH.safeTransferFrom(msg.sender, address(this), _amount); + // _amount = IwstETH(address(wstETH)).unwrap(_amount); + // stETH.safeTransfer(msg.sender, _amount); + // return (_amount, stETH); + // } else if (_token == woETH) { + // return (IERC4626(address(woETH)).redeem(_amount, msg.sender, msg.sender), oETH); + // } else { + // return (_amount, _token); + // } + // } + + + /// @inheritdoc IWrapper + function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + if (_token == stETH) { + _amount = IwstETH(address(wstETH)).wrap(_amount); + return (_amount, wstETH); + } else if (_token == oETH) { + return (IERC4626(address(woETH)).deposit(_amount, msg.sender), woETH); + } else { + return (_amount, _token); + } + } + + /// @inheritdoc IWrapper + function unwrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + if (_token == wstETH) { + _amount = IwstETH(address(wstETH)).unwrap(_amount); + return (_amount, stETH); + } else if (_token == woETH) { + return (IERC4626(address(woETH)).redeem(_amount, address(this), address(this)), oETH); + } else { + return (_amount, _token); + } + } + + + /// @inheritdoc IWrapper + function toUserAssetAmount(IERC20 _asset, uint256 _userUnderlyingView) external view returns (uint256) { + if (_asset == wstETH) { + // Adjust for wstETH using view method, converting stETH to wstETH + return IwstETH(address(wstETH)).getWstETHByStETH(_userUnderlyingView); + } + if (_asset == woETH) { + // Adjust for woETH using view method, converting oETH to woETH + return IERC4626(address(woETH)).previewDeposit(_userUnderlyingView); + } + return _userUnderlyingView; + } + + // ============================================================================================ + // Errors + // ============================================================================================ + + error ZeroAddress(); +} \ No newline at end of file diff --git a/src/ynEIGEN/LSDWrapperModule.sol b/src/ynEIGEN/LSDWrapperModule.sol new file mode 100644 index 000000000..4f4cd2c88 --- /dev/null +++ b/src/ynEIGEN/LSDWrapperModule.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {IERC20, SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IwstETH} from "src/external/lido/IwstETH.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; + +contract LSDWrapperModule is IWrapper, Initializable { + + using SafeERC20 for IERC20; + + IERC20 public immutable wstETH; + IERC20 public immutable woETH; + IERC20 public immutable oETH; + IERC20 public immutable stETH; + + // ============================================================================================ + // Constructor + // ============================================================================================ + + constructor(address _wstETH, address _woETH, address _oETH, address _stETH) { + _disableInitializers(); + + if (_wstETH == address(0) || _woETH == address(0) || _oETH == address(0) || _stETH == address(0)) { + revert ZeroAddress(); + } + + wstETH = IERC20(_wstETH); + woETH = IERC20(_woETH); + oETH = IERC20(_oETH); + stETH = IERC20(_stETH); + } + + // ============================================================================================ + // External functions + // ============================================================================================ + + /// @inheritdoc IWrapper + function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + if (_token == stETH) { + _amount = IwstETH(address(wstETH)).wrap(_amount); + return (_amount, wstETH); + } else if (_token == oETH) { + return (IERC4626(address(woETH)).deposit(_amount, msg.sender), woETH); + } else { + return (_amount, _token); + } + } + + /// @inheritdoc IWrapper + function unwrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + if (_token == wstETH) { + _amount = IwstETH(address(wstETH)).unwrap(_amount); + return (_amount, stETH); + } else if (_token == woETH) { + return (IERC4626(address(woETH)).redeem(_amount, address(this), address(this)), oETH); + } else { + return (_amount, _token); + } + } + + /// @inheritdoc IWrapper + function toUserAssetAmount(IERC20 _asset, uint256 _userUnderlyingView) external view returns (uint256) { + if (_asset == wstETH) { + // Adjust for wstETH using view method, converting stETH to wstETH + return IwstETH(address(wstETH)).getWstETHByStETH(_userUnderlyingView); + } + if (_asset == woETH) { + // Adjust for woETH using view method, converting oETH to woETH + return IERC4626(address(woETH)).previewDeposit(_userUnderlyingView); + } + return _userUnderlyingView; + } + + // ============================================================================================ + // Errors + // ============================================================================================ + + error ZeroAddress(); +} \ No newline at end of file diff --git a/src/ynEIGEN/RedemptionAssetsVault.sol b/src/ynEIGEN/RedemptionAssetsVault.sol new file mode 100644 index 000000000..f5aa15699 --- /dev/null +++ b/src/ynEIGEN/RedemptionAssetsVault.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {AccessControlUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; +import {IynEigen} from "src/interfaces/IynEigen.sol"; +import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; +import {IRedemptionAssetsVault} from "src/interfaces/IRedemptionAssetsVault.sol"; +import {ETH_ASSET, YNETH_UNIT} from "src/Constants.sol"; + +contract RedemptionAssetsVault is IRedemptionAssetsVault, Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { + + using SafeERC20 for IERC20; + + //-------------------------------------------------------------------------------------- + //---------------------------------- ERRORS ------------------------------------------ + //-------------------------------------------------------------------------------------- + + error ZeroAddress(); + error InsufficientAssetBalance(address asset, uint256 requestedAmount, uint256 balance); + error ContractPaused(); + error ContractUnpaused(); + error NotRedeemer(address caller); + error AssetNotSupported(); + event Paused(); + event Unpaused(); + + //-------------------------------------------------------------------------------------- + //---------------------------------- EVENTS ------------------------------------------ + //-------------------------------------------------------------------------------------- + + event AssetDeposited(address indexed asset, address indexed sender, uint256 amount); + + //-------------------------------------------------------------------------------------- + //---------------------------------- ROLES ------------------------------------------- + //-------------------------------------------------------------------------------------- + + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); + + //-------------------------------------------------------------------------------------- + //---------------------------------- VARIABLES --------------------------------------- + //-------------------------------------------------------------------------------------- + + IynEigen public ynEigen; + IAssetRegistry public assetRegistry; + bool public paused; + address public redeemer; + + mapping(address asset => uint256 balance) public balances; + + constructor() { + _disableInitializers(); + } + + // Initializer with Init struct and roles + struct Init { + address admin; + address redeemer; + IynEigen ynEigen; + IAssetRegistry assetRegistry; + } + + function initialize(Init memory init) + external + notZeroAddress(init.admin) + notZeroAddress(init.redeemer) + notZeroAddress(address(init.ynEigen)) + notZeroAddress(address(init.assetRegistry)) + initializer { + __AccessControl_init(); + __ReentrancyGuard_init(); + _grantRole(DEFAULT_ADMIN_ROLE, init.admin); + _grantRole(PAUSER_ROLE, init.admin); + _grantRole(UNPAUSER_ROLE, init.admin); + + redeemer = init.redeemer; + ynEigen = init.ynEigen; + assetRegistry = init.assetRegistry; + paused = false; + } + + //-------------------------------------------------------------------------------------- + //------------------------------------- VIEW ------------------------------------------ + //-------------------------------------------------------------------------------------- + + function assetBalances(IERC20[] calldata assetsArray) public view returns (uint256[] memory _balances) { + uint256 len = assetsArray.length; + _balances = new uint256[](len); + for (uint256 i = 0; i < len; ++i) { + _balances[i] = balances[address(assetsArray[i])]; + } + } + + function previewClaim(uint256 amount) external view returns (IERC20[] memory assets, uint256[] memory assetsOut) { + uint256 balance = availableRedemptionAssets(); + if (balance < amount) revert InsufficientAssetBalance(ETH_ASSET, amount, balance); + + assets = assetRegistry.getAssets(); + assetsOut = new uint256[](assets.length); + for (uint256 i = 0; i < assets.length; ++i) { + IERC20 asset = assets[i]; + uint256 assetBalance = balances[address(asset)]; + if (assetBalance > 0) { + uint256 assetBalanceInUnit = assetRegistry.convertToUnitOfAccount(asset, assetBalance); + if (assetBalanceInUnit >= amount) { + uint256 reqAmountInAsset = assetRegistry.convertFromUnitOfAccount(asset, amount); + assetsOut[i] = reqAmountInAsset; + break; + } else { + amount -= assetBalanceInUnit; + assetsOut[i] = assetBalance; + } + } + } + } + + //-------------------------------------------------------------------------------------- + //---------------------------------- REDEMPTION -------------------------------------- + //-------------------------------------------------------------------------------------- + + function deposit(uint256 amount, address asset) external { + if (!assetRegistry.assetIsSupported(IERC20(asset))) revert AssetNotSupported(); + + balances[asset] += amount; + IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); + + emit AssetDeposited(asset, msg.sender, amount); + } + + /** + * @notice Calculates the current redemption rate of ynETH to ETH. + * @return The current redemption rate as a uint256. + */ + function redemptionRate() public view returns (uint256) { + return ynEigen.previewRedeem(YNETH_UNIT); + } + + /** + * @notice Returns the total amount of assets available for redemption. + * @return _availableRedemptionAssets The available unit-of-account-denominated balance as a uint256. + */ + function availableRedemptionAssets() public view returns (uint256 _availableRedemptionAssets) { + + IERC20[] memory assets = assetRegistry.getAssets(); + + uint256 len = assets.length; + for (uint256 i = 0; i < len; ++i) { + IERC20 asset = assets[i]; + uint256 balance = balances[address(asset)]; + if (balance > 0) _availableRedemptionAssets += assetRegistry.convertToUnitOfAccount(asset, balance); + } + } + + /** + * @notice Transfers a specified amount of redemption assets to a given address. + * @param to The recipient address of the assets. + * @param amount The amount of assets to transfer. + * @dev Requires the caller to be the redeemer and the contract to not be paused. + * @dev Iterates over the supported assets, transferring each asset's balance to the user until fully depleted. + */ + function transferRedemptionAssets(address to, uint256 amount, bytes calldata /* data */) public onlyRedeemer whenNotPaused nonReentrant { + uint256 balance = availableRedemptionAssets(); + if (balance < amount) revert InsufficientAssetBalance(ETH_ASSET, amount, balance); + + uint256 amountTransferred = amount; + IERC20[] memory assets = assetRegistry.getAssets(); + for (uint256 i = 0; i < assets.length; ++i) { + IERC20 asset = assets[i]; + uint256 assetBalance = balances[address(asset)]; + if (assetBalance > 0) { + uint256 assetBalanceInUnit = assetRegistry.convertToUnitOfAccount(asset, assetBalance); + if (assetBalanceInUnit >= amount) { + uint256 reqAmountInAsset = assetRegistry.convertFromUnitOfAccount(asset, amount); + IERC20(asset).safeTransfer(to, reqAmountInAsset); + balances[address(asset)] -= reqAmountInAsset; + emit AssetTransferred(address(asset), msg.sender, to, reqAmountInAsset); + break; + } else { + IERC20(asset).safeTransfer(to, assetBalance); + balances[address(asset)] = 0; + amount -= assetBalanceInUnit; + emit AssetTransferred(address(asset), msg.sender, to, assetBalance); + } + } + } + emit TotalAssetsTransferred(ETH_ASSET, msg.sender, to, amountTransferred); + } + + /** + * @notice Withdraws a specified amount of redemption assets and processes them through ynETH. + * @param amount The amount of ETH to withdraw and process. + * @dev Requires the caller to be the redeemer and the contract to not be paused. + */ + function withdrawRedemptionAssets(uint256 amount) public onlyRedeemer whenNotPaused nonReentrant { + IERC20[] memory assets = assetRegistry.getAssets(); + for (uint256 i = 0; i < assets.length; ++i) { + IERC20 asset = assets[i]; + uint256 assetBalance = balances[address(asset)]; + if (assetBalance > 0) { + uint256 assetBalanceInUnit = assetRegistry.convertToUnitOfAccount(asset, assetBalance); + if (assetBalanceInUnit >= amount) { + uint256 reqAmountInAsset = assetRegistry.convertFromUnitOfAccount(asset, amount); + ynEigen.processWithdrawn(reqAmountInAsset, address(asset)); + balances[address(asset)] -= reqAmountInAsset; + emit AssetWithdrawn(address(asset), msg.sender, address(ynEigen), reqAmountInAsset); + break; + } else { + ynEigen.processWithdrawn(assetBalance, address(asset)); + balances[address(asset)] = 0; + amount -= assetBalanceInUnit; + emit AssetWithdrawn(address(asset), msg.sender, address(ynEigen), assetBalance); + } + } + } + emit TotalAssetsTransferred(ETH_ASSET, msg.sender, address(ynEigen), amount); + } + + //-------------------------------------------------------------------------------------- + //---------------------------------- MODIFIERS --------------------------------------- + //-------------------------------------------------------------------------------------- + + /** + * @notice Ensure that the given address is not the zero address. + * @param _address The address to check. + */ + modifier notZeroAddress(address _address) { + if (_address == address(0)) { + revert ZeroAddress(); + } + _; + } + + /** + * @notice Checks if the contract is not paused. + */ + modifier whenNotPaused() { + if (paused) { + revert ContractPaused(); + } + _; + } + + /** + * @notice Ensures that the caller has the REDEEMER_ROLE. + */ + modifier onlyRedeemer() { + if (msg.sender != redeemer) { + revert NotRedeemer(msg.sender); + } + _; + } + + //-------------------------------------------------------------------------------------- + //---------------------------------- PAUSE FUNCTIONS --------------------------------- + //-------------------------------------------------------------------------------------- + + /** + * @notice Pauses the contract, preventing certain actions. + */ + function pause() external onlyRole(PAUSER_ROLE) { + if (paused) revert ContractPaused(); + + paused = true; + + emit Paused(); + } + + /** + * @notice Unpauses the contract, allowing certain actions. + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + if (!paused) revert ContractUnpaused(); + + paused = false; + + emit Unpaused(); + } +} diff --git a/src/ynEIGEN/TokenStakingNode.sol b/src/ynEIGEN/TokenStakingNode.sol index 084ca071b..c68f892a8 100644 --- a/src/ynEIGEN/TokenStakingNode.sol +++ b/src/ynEIGEN/TokenStakingNode.sol @@ -12,6 +12,9 @@ import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfa import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {ITokenStakingNode} from "src/interfaces/ITokenStakingNode.sol"; import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; +import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; + interface ITokenStakingNodeEvents { event DepositToEigenlayer( @@ -22,6 +25,9 @@ interface ITokenStakingNodeEvents { ); event Delegated(address indexed operator, bytes32 approverSalt); event Undelegated(bytes32[] withdrawalRoots); + event QueuedWithdrawals(IStrategy strategies, uint256 shares, bytes32[] fullWithdrawalRoots); + event CompletedQueuedWithdrawals(uint256 shares, uint256 amountOut, address strategy); + event DeallocatedTokens(uint256 amount, IERC20 token); } /** @@ -45,6 +51,8 @@ contract TokenStakingNode is error NotTokenStakingNodeOperator(); error NotStrategyManager(); error NotTokenStakingNodeDelegator(); + error NotTokenStakingNodesWithdrawer(); + error ArrayLengthMismatch(); //-------------------------------------------------------------------------------------- //---------------------------------- VARIABLES --------------------------------------- @@ -53,6 +61,9 @@ contract TokenStakingNode is ITokenStakingNodesManager public override tokenStakingNodesManager; uint256 public nodeId; + mapping(IStrategy => uint256) public queuedShares; + mapping(IERC20 => uint256) public withdrawn; + //-------------------------------------------------------------------------------------- //---------------------------------- INITIALIZATION ---------------------------------- //-------------------------------------------------------------------------------------- @@ -88,10 +99,15 @@ contract TokenStakingNode is uint256[] calldata amounts, IStrategy[] calldata strategies ) external nonReentrant onlyYieldNestStrategyManager { + + uint256 assetsLength = assets.length; + if (assetsLength != amounts.length || assetsLength != strategies.length) { + revert ArrayLengthMismatch(); + } + IStrategyManager strategyManager = tokenStakingNodesManager .strategyManager(); - uint256 assetsLength = assets.length; for (uint256 i = 0; i < assetsLength; i++) { IERC20 asset = assets[i]; uint256 amount = amounts[i]; @@ -108,6 +124,134 @@ contract TokenStakingNode is } } + //-------------------------------------------------------------------------------------- + //-------------------------------- EIGENLAYER WITHDRAWALS ----------------------------- + //-------------------------------------------------------------------------------------- + + function getQueuedSharesAndWithdrawn(IStrategy _strategy, IERC20 _asset) external view returns (uint256, uint256) { + return (queuedShares[_strategy], withdrawn[_asset]); + } + + /** + * @notice Queues withdrawals for a specific strategy + * @param _strategy The strategy from which to withdraw + * @param _shares The number of shares to withdraw + * @return _fullWithdrawalRoots An array of withdrawal roots generated by the queueWithdrawals operation + */ + function queueWithdrawals( + IStrategy _strategy, + uint256 _shares + ) external onlyTokenStakingNodesWithdrawer returns (bytes32[] memory _fullWithdrawalRoots) { + + IStrategy[] memory _strategiesArray = new IStrategy[](1); + _strategiesArray[0] = _strategy; + uint256[] memory _sharesArray = new uint256[](1); + _sharesArray[0] = _shares; + IDelegationManager.QueuedWithdrawalParams[] memory _params = new IDelegationManager.QueuedWithdrawalParams[](1); + _params[0] = IDelegationManager.QueuedWithdrawalParams({ + strategies: _strategiesArray, + shares: _sharesArray, + withdrawer: address(this) + }); + + queuedShares[_strategy] += _shares; + + _fullWithdrawalRoots = tokenStakingNodesManager.delegationManager().queueWithdrawals(_params); + + emit QueuedWithdrawals(_strategy, _shares, _fullWithdrawalRoots); + } + + /** + * @notice Completes queued withdrawals for a specific strategy + * @param _nonce The nonce of the withdrawal + * @param _startBlock The block number when the withdrawal was queued + * @param _shares The number of shares to withdraw + * @param _strategy The strategy from which to withdraw + * @param _middlewareTimesIndexes The indexes of middleware times to use for the withdrawal + * @param updateTokenStakingNodesBalances If true calls updateTokenStakingNodesBalances for yieldNestStrategyManager + */ + function completeQueuedWithdrawals( + uint256 _nonce, + uint32 _startBlock, + uint256 _shares, + IStrategy _strategy, + uint256[] memory _middlewareTimesIndexes, + bool updateTokenStakingNodesBalances + ) public onlyTokenStakingNodesWithdrawer { + + IDelegationManager _delegationManager = tokenStakingNodesManager.delegationManager(); + + IDelegationManager.Withdrawal[] memory _withdrawals = new IDelegationManager.Withdrawal[](1); + { + IStrategy[] memory _strategiesArray = new IStrategy[](1); + _strategiesArray[0] = _strategy; + uint256[] memory _sharesArray = new uint256[](1); + _sharesArray[0] = _shares; + _withdrawals[0] = IDelegationManager.Withdrawal({ + staker: address(this), + delegatedTo: _delegationManager.delegatedTo(address(this)), + withdrawer: address(this), + nonce: _nonce, + startBlock: _startBlock, + strategies: _strategiesArray, + shares: _sharesArray + }); + } + + IERC20 _token = _strategy.underlyingToken(); + uint256 _balanceBefore = _token.balanceOf(address(this)); + + { + bool[] memory _receiveAsTokens = new bool[](1); + _receiveAsTokens[0] = true; + IERC20[][] memory _tokens = new IERC20[][](1); + _tokens[0] = new IERC20[](1); + _tokens[0][0] = _token; + + _delegationManager.completeQueuedWithdrawals( + _withdrawals, + _tokens, + _middlewareTimesIndexes, + _receiveAsTokens + ); + } + + uint256 _actualAmountOut = _token.balanceOf(address(this)) - _balanceBefore; + IWrapper _wrapper = IYieldNestStrategyManager(tokenStakingNodesManager.yieldNestStrategyManager()).wrapper(); + IERC20(_token).forceApprove(address(_wrapper), _actualAmountOut); // NOTE: approving also token that will not be transferred + + (bool success, bytes memory result) = address(_wrapper).delegatecall( + abi.encodeWithSignature("wrap(uint256,address)", _actualAmountOut, _token) + ); + require(success, "Delegatecall failed"); + (_actualAmountOut, _token) = abi.decode(result, (uint256, IERC20)); + + queuedShares[_strategy] -= _shares; + withdrawn[_token] += _actualAmountOut; + + if (updateTokenStakingNodesBalances) { + // Actual balance changes only if slashing occured. choose to update here + // only if the off-chain considers it necessary to save gas + IYieldNestStrategyManager(tokenStakingNodesManager.yieldNestStrategyManager()).updateTokenStakingNodesBalances( + _token + ); + } + + emit CompletedQueuedWithdrawals(_shares, _actualAmountOut, address(_strategy)); + } + + /** + * @notice Deallocates tokens from the withdrawn balance and approves them for transfer. + * @param _token The ERC20 token to deallocate. + * @param _amount The amount of tokens to deallocate. + */ + function deallocateTokens(IERC20 _token, uint256 _amount) external onlyYieldNestStrategyManager { + withdrawn[_token] -= _amount; + _token.safeTransfer(msg.sender, _amount); + + emit DeallocatedTokens(_amount, _token); + } + //-------------------------------------------------------------------------------------- //---------------------------------- DELEGATION -------------------------------------- //-------------------------------------------------------------------------------------- @@ -169,6 +313,13 @@ contract TokenStakingNode is _; } + modifier onlyTokenStakingNodesWithdrawer() { + if ( + !IYieldNestStrategyManager(tokenStakingNodesManager.yieldNestStrategyManager()).isStakingNodesWithdrawer(msg.sender) + ) revert NotTokenStakingNodesWithdrawer(); + _; + } + //-------------------------------------------------------------------------------------- //---------------------------------- BEACON IMPLEMENTATION --------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/TokenStakingNodesManager.sol b/src/ynEIGEN/TokenStakingNodesManager.sol index 992682933..d9eabd977 100644 --- a/src/ynEIGEN/TokenStakingNodesManager.sol +++ b/src/ynEIGEN/TokenStakingNodesManager.sol @@ -30,11 +30,8 @@ contract TokenStakingNodesManager is AccessControlUpgradeable, ITokenStakingNode //---------------------------------- ERRORS ------------------------------------------ //-------------------------------------------------------------------------------------- - error UnsupportedAsset(IERC20 asset); error Unauthorized(); error InsufficientFunds(); - error Paused(); - error ZeroAmount(); error ZeroAddress(); error BeaconImplementationAlreadyExists(); error NoBeaconImplementationExists(); @@ -126,7 +123,6 @@ contract TokenStakingNodesManager is AccessControlUpgradeable, ITokenStakingNode maxNodeCount = init.maxNodeCount; } - //-------------------------------------------------------------------------------------- //---------------------------------- STAKING NODE CREATION --------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/ynEigen.sol b/src/ynEIGEN/ynEigen.sol index 57924de17..cdaba1570 100644 --- a/src/ynEIGEN/ynEigen.sol +++ b/src/ynEIGEN/ynEigen.sol @@ -7,6 +7,8 @@ import {ReentrancyGuardUpgradeable} from "lib/openzeppelin-contracts-upgradeable import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IynEigen} from "src/interfaces/IynEigen.sol"; import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; +import {IEigenStrategyManager} from "src/interfaces/IEigenStrategyManager.sol"; +import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; import {ynBase} from "src/ynBase.sol"; @@ -17,6 +19,7 @@ interface IynEigenEvents { event LSDStakingNodeCreated(uint256 nodeId, address nodeAddress); event MaxNodeCountUpdated(uint256 maxNodeCount); event DepositsPausedUpdated(bool paused); + event WithdrawProcessed(uint256 amount, uint256 newBalance, address asset); } contract ynEigen is IynEigen, ynBase, ReentrancyGuardUpgradeable, IynEigenEvents { @@ -40,6 +43,13 @@ contract ynEigen is IynEigen, ynBase, ReentrancyGuardUpgradeable, IynEigenEvents error NotStrategyManager(address msgSender); error InsufficientAssetBalance(IERC20 asset, uint256 balance, uint256 requestedAmount); error ZeroShares(); + error CallerNotAuthorized(); + + //-------------------------------------------------------------------------------------- + //---------------------------------- ROLES -------------------------------------------- + //-------------------------------------------------------------------------------------- + + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); //-------------------------------------------------------------------------------------- //---------------------------------- VARIABLES --------------------------------------- @@ -203,6 +213,36 @@ contract ynEigen is IynEigen, ynBase, ReentrancyGuardUpgradeable, IynEigenEvents } } + //-------------------------------------------------------------------------------------- + //---------------------------------- WITHDRAWALS -------------------------------------- + //-------------------------------------------------------------------------------------- + + function burn(uint256 amount) external onlyRole(BURNER_ROLE) { + _burn(msg.sender, amount); + } + + function previewRedeem(IERC20 asset, uint256 shares) external view returns (uint256) { + return convertToAssets(asset, shares); + } + + function previewRedeem(uint256 shares) external view returns (uint256) { + return convertToAssets(IERC20(address(0)), shares); + } + + function convertToAssets(IERC20 asset, uint256 shares) public view returns (uint256) { + return assetIsSupported(asset) ? + assetRegistry.convertFromUnitOfAccount(asset, _convertToAssets(shares, Math.Rounding.Floor)) : + _convertToAssets(shares, Math.Rounding.Floor); + } + + function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view returns (uint256) { + + uint256 supply = totalSupply(); + if (supply == 0) return shares; + + return Math.mulDiv(shares, totalAssets(), supply, rounding); + } + //-------------------------------------------------------------------------------------- //---------------------------------- TOTAL ASSETS ----------------------------------- //-------------------------------------------------------------------------------------- @@ -280,6 +320,23 @@ contract ynEigen is IynEigen, ynBase, ReentrancyGuardUpgradeable, IynEigenEvents } } + function processWithdrawn(uint256 _amount, address _asset) public { + if (_amount == 0) revert ZeroAmount(); + + IYieldNestStrategyManager _yieldNestStrategyManager = IYieldNestStrategyManager(yieldNestStrategyManager); + if ( + msg.sender != address(_yieldNestStrategyManager) && + msg.sender != address(_yieldNestStrategyManager.redemptionAssetsVault()) + ) revert CallerNotAuthorized(); + + uint256 _newBalance = assets[_asset].balance + _amount; + assets[_asset].balance = _newBalance; + + IERC20(_asset).safeTransferFrom(msg.sender, address(this), _amount); + + emit WithdrawProcessed(_amount, _newBalance, _asset); + } + //-------------------------------------------------------------------------------------- //---------------------------------- PAUSING ----------------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/ynEigenDepositAdapter.sol b/src/ynEIGEN/ynEigenDepositAdapter.sol index 2e5a709f9..1e29f2aa5 100644 --- a/src/ynEIGEN/ynEigenDepositAdapter.sol +++ b/src/ynEIGEN/ynEigenDepositAdapter.sol @@ -10,6 +10,8 @@ import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/ import {AccessControlUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; + interface IynEigenDepositAdapterEvents { event ReferralDepositProcessed( address sender, @@ -54,6 +56,7 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A IERC4626 public woETH; IERC20 public stETH; IERC20 public oETH; + IWrapper public wrapper; //-------------------------------------------------------------------------------------- //---------------------------------- INITIALIZATION ---------------------------------- @@ -84,6 +87,10 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A oETH = IERC20(woETH.asset()); } + function initializeV2(address _wrapper) external reinitializer(2) notZeroAddress(_wrapper) { + wrapper = IWrapper(_wrapper); + } + /** * @notice Handles the deposit of assets into the ynEigen system. It supports all assets supported by ynEigen @@ -159,8 +166,13 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A function depositStETH(uint256 amount, address receiver) internal returns (uint256 shares) { stETH.safeTransferFrom(msg.sender, address(this), amount); - stETH.forceApprove(address(wstETH), amount); - uint256 wstETHAmount = wstETH.wrap(amount); + + (bool success, bytes memory result) = address(wrapper).delegatecall( + abi.encodeWithSignature("wrap(uint256,address)", amount, stETH) + ); + require(success, "Delegatecall failed"); + (uint256 wstETHAmount,) = abi.decode(result, (uint256, IERC20)); + wstETH.forceApprove(address(ynEigen), wstETHAmount); shares = ynEigen.deposit(IERC20(address(wstETH)), wstETHAmount, receiver); @@ -170,8 +182,13 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A function depositOETH(uint256 amount, address receiver) internal returns (uint256 shares) { oETH.safeTransferFrom(msg.sender, address(this), amount); - oETH.forceApprove(address(woETH), amount); - uint256 woETHShares = woETH.deposit(amount, address(this)); + + (bool success, bytes memory result) = address(wrapper).delegatecall( + abi.encodeWithSignature("wrap(uint256,address)", amount, oETH) + ); + require(success, "Delegatecall failed"); + (uint256 woETHShares,) = abi.decode(result, (uint256, IERC20)); + woETH.forceApprove(address(ynEigen), woETHShares); shares = ynEigen.deposit(IERC20(address(woETH)), woETHShares, receiver); diff --git a/src/ynEIGEN/ynEigenViewer.sol b/src/ynEIGEN/ynEigenViewer.sol index 32a528b9e..9b36485ed 100644 --- a/src/ynEIGEN/ynEigenViewer.sol +++ b/src/ynEIGEN/ynEigenViewer.sol @@ -12,14 +12,37 @@ import {IEigenStrategyManager} from "../interfaces/IEigenStrategyManager.sol"; contract ynEigenViewer { + + //-------------------------------------------------------------------------------------- + //---------------------------------- STRUCTS ----------------------------------------- + //-------------------------------------------------------------------------------------- struct AssetInfo { address asset; string name; string symbol; + uint256 rate; uint256 ratioOfTotalAssets; - uint256 totalBalance; + uint256 totalBalanceInUnitOfAccount; + uint256 totalBalanceInAsset; } + + //-------------------------------------------------------------------------------------- + //---------------------------------- ERRORS ------------------------------------------ + //-------------------------------------------------------------------------------------- + + error ArrayLengthMismatch(uint256 expected, uint256 actual); + + //-------------------------------------------------------------------------------------- + //---------------------------------- CONSTANTS --------------------------------------- + //-------------------------------------------------------------------------------------- + + uint256 public constant DECIMALS = 1_000_000; + uint256 public constant UNIT = 1 ether; + + //-------------------------------------------------------------------------------------- + //---------------------------------- VARIABLES --------------------------------------- + //-------------------------------------------------------------------------------------- /* solhint-disable immutable-vars-naming */ AssetRegistry public immutable assetRegistry; @@ -28,8 +51,9 @@ contract ynEigenViewer { IRateProvider public immutable rateProvider; /* solhint-enable immutable-vars-naming */ - uint256 public constant DECIMALS = 1_000_000; - uint256 public constant UNIT = 1 ether; + //-------------------------------------------------------------------------------------- + //---------------------------------- INITIALIZATION ---------------------------------- + //-------------------------------------------------------------------------------------- constructor(address _assetRegistry, address _ynEIGEN, address _tokenStakingNodesManager, address _rateProvider) { assetRegistry = AssetRegistry(_assetRegistry); @@ -38,24 +62,45 @@ contract ynEigenViewer { rateProvider = IRateProvider(_rateProvider); } + /** + * @notice Retrieves all staking nodes from the TokenStakingNodesManager + * @dev This function calls the getAllNodes() function of the tokenStakingNodesManager contract + * @return An array of ITokenStakingNode interfaces representing all staking nodes + */ function getAllStakingNodes() external view returns (ITokenStakingNode[] memory) { return tokenStakingNodesManager.getAllNodes(); } + /** + * @notice Retrieves information about all assets in the ynEigen system + * @dev This function fetches asset data from the asset registry and ynEigen system + * and computes various metrics for each asset + * @return _assetsInfo An array of AssetInfo structs containing detailed information about each asset + */ function getYnEigenAssets() external view returns (AssetInfo[] memory _assetsInfo) { IERC20[] memory _assets = assetRegistry.getAssets(); uint256 _assetsLength = _assets.length; _assetsInfo = new AssetInfo[](_assetsLength); + uint256[] memory assetBalances = assetRegistry.getAllAssetBalances(); + // Assert that the lengths of _assets and assetBalances are the same + if (_assetsLength != assetBalances.length) { + revert ArrayLengthMismatch(_assetsLength, assetBalances.length); + } + uint256 _totalAssets = ynEIGEN.totalAssets(); + for (uint256 i = 0; i < _assetsLength; ++i) { - uint256 _balance = assetRegistry.convertToUnitOfAccount(_assets[i], ynEIGEN.assetBalance(_assets[i])); + uint256 assetBalance = assetBalances[i]; + uint256 _balance = assetRegistry.convertToUnitOfAccount(_assets[i], assetBalance); _assetsInfo[i] = AssetInfo({ asset: address(_assets[i]), name: IERC20Metadata(address(_assets[i])).name(), symbol: IERC20Metadata(address(_assets[i])).symbol(), + rate: rateProvider.rate(address(_assets[i])), ratioOfTotalAssets: (_balance > 0 && _totalAssets > 0) ? _balance * DECIMALS / _totalAssets : 0, - totalBalance: _balance + totalBalanceInUnitOfAccount: _balance, + totalBalanceInAsset: assetBalance }); } } diff --git a/test/integration/ynEIGEN/AssetRegistry.t.sol b/test/integration/ynEIGEN/AssetRegistry.t.sol index 47f261ec5..bb3b4fa05 100644 --- a/test/integration/ynEIGEN/AssetRegistry.t.sol +++ b/test/integration/ynEIGEN/AssetRegistry.t.sol @@ -174,6 +174,48 @@ contract AssetRegistryTest is ynEigenIntegrationBaseTest { assertEq(convertedAmount, expectedConvertedAmount, "Converted amount should match expected value based on real rate"); } + function testWSTETHconvertFromUnitOfAccountFuzz(uint256 amount) public { + vm.assume(amount < 1000000 ether); + + IERC20 asset = IERC20(chainAddresses.lsd.WSTETH_ADDRESS); // Using wstETH as the asset + uint256 realRate = IstETH(chainAddresses.lsd.STETH_ADDRESS).getPooledEthByShares(1e18); + uint256 expectedConvertedAmount = amount * 1e18 / realRate; // Calculating the expected converted amount based on the real rate + uint256 convertedAmount = assetRegistry.convertFromUnitOfAccount(asset, amount); + assertEq(convertedAmount, expectedConvertedAmount, "Converted amount should match expected value based on real rate"); + } + + function testsfrxETHconvertFromUnitOfAccountFuzz(uint256 amount) public { + vm.assume(amount < 1000000 ether); + + IERC20 asset = IERC20(chainAddresses.lsd.SFRXETH_ADDRESS); // Using wstETH as the asset + address FRAX_ASSET = chainAddresses.lsd.SFRXETH_ADDRESS; + IFrxEthWethDualOracle FRX_ETH_WETH_DUAL_ORACLE = IFrxEthWethDualOracle(testAssetUtils.FRX_ETH_WETH_DUAL_ORACLE()); + uint256 realRate = IsfrxETH(FRAX_ASSET).pricePerShare() * FRX_ETH_WETH_DUAL_ORACLE.getCurveEmaEthPerFrxEth() / 1e18; + uint256 expectedConvertedAmount = amount * 1e18 / realRate; // Calculating the expected converted amount based on the real rate + uint256 convertedAmount = assetRegistry.convertFromUnitOfAccount(asset, amount); + assertEq(convertedAmount, expectedConvertedAmount, "Converted amount should match expected value based on real rate"); + } + + function testRETHconvertFromUnitOfAccountFuzz(uint256 amount) public { + vm.assume(amount < 1000000 ether); + + IERC20 asset = IERC20(chainAddresses.lsd.RETH_ADDRESS); // Using rETH as the asset + uint256 realRate = IrETH(chainAddresses.lsd.RETH_ADDRESS).getExchangeRate(); + uint256 expectedConvertedAmount = amount * 1e18 / realRate; // Calculating the expected converted amount based on the real rate + uint256 convertedAmount = assetRegistry.convertFromUnitOfAccount(asset, amount); + assertEq(convertedAmount, expectedConvertedAmount, "Converted amount should match expected value based on real rate"); + } + + function testWOETHconvertFromUnitOfAccountFuzz(uint256 amount) public { + vm.assume(amount < 1000000 ether); + + IERC20 asset = IERC20(chainAddresses.lsd.WOETH_ADDRESS); // Using woETH as the asset + uint256 realRate = IERC4626(chainAddresses.lsd.WOETH_ADDRESS).previewRedeem(1e18); + uint256 expectedConvertedAmount = amount * 1e18 / realRate; // Calculating the expected converted amount based on the real rate + uint256 convertedAmount = assetRegistry.convertFromUnitOfAccount(asset, amount); + assertEq(convertedAmount, expectedConvertedAmount, "Converted amount should match expected value based on real rate"); + } + function testPauseActions() public { vm.prank(actors.ops.PAUSE_ADMIN); assetRegistry.pauseActions(); diff --git a/test/integration/ynEIGEN/LSDWrapperModule.t.sol b/test/integration/ynEIGEN/LSDWrapperModule.t.sol new file mode 100644 index 000000000..c5d75d1dc --- /dev/null +++ b/test/integration/ynEIGEN/LSDWrapperModule.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {LSDWrapperModule} from "src/ynEIGEN/LSDWrapperModule.sol"; +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {IwstETH} from "src/external/lido/IwstETH.sol"; +import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; +import {console} from "forge-std/console.sol"; +import {TransparentUpgradeableProxy} from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + + +contract LSDWrapperModuleTest is Test { + LSDWrapperModule public wrapper; + address public constant WSTETH = address(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); + address public constant WOETH = address(0xDcEe70654261AF21C44c093C300eD3Bb97b78192); + address public constant OETH = address(0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3); + address public constant STETH = address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); + TestAssetUtils public testAssetUtils; + + function setUp() public { + // Deploy the implementation contract + LSDWrapperModule implementation = new LSDWrapperModule(WSTETH, WOETH, OETH, STETH); + + + + // Deploy the TransparentUpgradeableProxy + wrapper = LSDWrapperModule( + address(new TransparentUpgradeableProxy( + address(implementation), + address(this), + "" + )) + ); + testAssetUtils = new TestAssetUtils(); + } + + function testWrapStETH( + // uint256 amount + ) public { + + uint256 amount = 10 ether; + //vm.assume(amount > 0 && amount <= 1000 ether); + + IERC20 stETH = IERC20(STETH); + uint256 balance = testAssetUtils.get_stETH(address(this), amount); + stETH.approve(WSTETH, balance); + + (bool success, bytes memory result) = address(wrapper).delegatecall( + abi.encodeWithSignature("wrap(uint256,address)", balance, STETH) + ); + require(success, "Delegatecall failed"); + (uint256 wrappedAmount, IERC20 wrappedToken) = abi.decode(result, (uint256, IERC20)); + + assertEq(address(wrappedToken), WSTETH, "Wrapped token should be wstETH"); + assertGt(wrappedAmount, 0, "Wrapped amount should be greater than 0"); + assertEq(IERC20(WSTETH).balanceOf(address(this)), wrappedAmount, "Wrapped amount should be received"); + } + + function testUnwrapWstETH( + // uint256 amount + ) public { + uint256 amount = 100 ether; + // vm.assume(amount > 0 && amount <= 1000 ether); + + IERC20 wstETH = IERC20(WSTETH); + uint256 balance = testAssetUtils.get_wstETH(address(this), amount); + + (bool success, bytes memory result) = address(wrapper).delegatecall( + abi.encodeWithSignature("unwrap(uint256,address)", balance, WSTETH) + ); + require(success, "Delegatecall failed"); + (uint256 unwrappedAmount, IERC20 unwrappedToken) = abi.decode(result, (uint256, IERC20)); + + assertEq(address(unwrappedToken), STETH, "Unwrapped token should be stETH"); + assertGt(unwrappedAmount, 0, "Unwrapped amount should be greater than 0"); + assertApproxEqRel(IERC20(STETH).balanceOf(address(this)), unwrappedAmount, 1e15, "Unwrapped amount should be approximately received"); + } + + function testWrapOETH() public { + uint256 amount = 10 ether; + + IERC20 oETH = IERC20(OETH); + uint256 balance = testAssetUtils.get_OETH(address(this), amount); + oETH.approve(WOETH, balance); + + (bool success, bytes memory result) = address(wrapper).delegatecall( + abi.encodeWithSignature("wrap(uint256,address)", balance, OETH) + ); + require(success, "Delegatecall failed"); + (uint256 wrappedAmount, IERC20 wrappedToken) = abi.decode(result, (uint256, IERC20)); + + assertEq(address(wrappedToken), WOETH, "Wrapped token should be woETH"); + assertGt(wrappedAmount, 0, "Wrapped amount should be greater than 0"); + assertEq(IERC20(WOETH).balanceOf(address(this)), wrappedAmount, "Wrapped amount should be received"); + } + + function testUnwrapWoETH() public { + uint256 amount = 100 ether; + + IERC20 woETH = IERC20(WOETH); + uint256 balance = testAssetUtils.get_wOETH(address(this), amount); + + // Print maxRedeem(owner) for address(this) + uint256 maxRedeemAmount = IERC4626(WOETH).maxRedeem(address(this)); + console.log("Max redeemable amount for this address:", maxRedeemAmount); + + (bool success, bytes memory result) = address(wrapper).delegatecall( + abi.encodeWithSignature("unwrap(uint256,address)", balance, WOETH) + ); + require(success, "Delegatecall failed"); + (uint256 unwrappedAmount, IERC20 unwrappedToken) = abi.decode(result, (uint256, IERC20)); + + assertEq(address(unwrappedToken), OETH, "Unwrapped token should be oETH"); + assertGt(unwrappedAmount, 0, "Unwrapped amount should be greater than 0"); + assertApproxEqRel(IERC20(OETH).balanceOf(address(this)), unwrappedAmount, 1e15, "Unwrapped amount should be approximately received"); + } + + function testToUserAssetAmount() public { + uint256 stEthAmount = 100 ether; + uint256 oEthAmount = 100 ether; + uint256 ethAmount = 100 ether; + + // Test wstETH conversion + uint256 wstEthAmount = wrapper.toUserAssetAmount(IERC20(WSTETH), stEthAmount); + assertGt(wstEthAmount, 0, "wstETH amount should be greater than 0"); + assertLt(wstEthAmount, stEthAmount, "wstETH amount should be less than stETH amount"); + + // Verify conversion accuracy + uint256 backToStEth = IwstETH(WSTETH).getStETHByWstETH(wstEthAmount); + assertApproxEqRel(backToStEth, stEthAmount, 1e15, "stETH conversion should be approximately equal"); + + } + +} diff --git a/test/integration/ynEIGEN/TokenStakingNode.t.sol b/test/integration/ynEIGEN/TokenStakingNode.t.sol index 1f63d8194..a25a0a61a 100644 --- a/test/integration/ynEIGEN/TokenStakingNode.t.sol +++ b/test/integration/ynEIGEN/TokenStakingNode.t.sol @@ -11,10 +11,99 @@ import {ISignatureUtils} from "lib/eigenlayer-contracts/src/contracts/interfaces import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; import {ITokenStakingNode} from "src/interfaces/ITokenStakingNode.sol"; import {IwstETH} from "src/external/lido/IwstETH.sol"; +import {EigenStrategyManager} from "src/ynEIGEN/EigenStrategyManager.sol"; +import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; +import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; + + import "forge-std/console.sol"; +interface ITestState { + function ynEigenToken() external view returns (IynEigen); + function tokenStakingNodesManager() external view returns (ITokenStakingNodesManager); + function assetRegistry() external view returns (IAssetRegistry); + function eigenStrategyManager() external view returns (EigenStrategyManager); + function eigenLayerStrategyManager() external view returns (IStrategyManager); + function eigenLayerDelegationManager() external view returns (IDelegationManager); +} + +interface ITokenStakingNodesManager { + function nodes(uint256 nodeId) external view returns (ITokenStakingNode); + function createTokenStakingNode() external returns (ITokenStakingNode); +} + + +struct StateSnapshot { + uint256 totalAssets; + uint256 totalSupply; + mapping(IStrategy => uint256) strategyQueuedShares; + mapping(address => uint256) withdrawnByToken; + mapping(address => uint256) stakedAssetBalanceForNode; + mapping(address => uint256) strategySharesForNode; +} + +contract NodeStateSnapshot { + StateSnapshot public snapshot; + + constructor() {} + + function takeSnapshot(address testAddress, uint256 nodeId) external { + + ITestState state = ITestState(testAddress); + ITokenStakingNode node = state.tokenStakingNodesManager().nodes(nodeId); + + snapshot.totalAssets = state.ynEigenToken().totalAssets(); + snapshot.totalSupply = state.ynEigenToken().totalSupply(); + + IERC20[] memory assets = state.assetRegistry().getAssets(); + + for (uint256 i = 0; i < assets.length; i++) { + IERC20 asset = assets[i]; + IStrategy strategy = state.eigenStrategyManager().strategies(asset); + + // Store queued shares for each strategy + snapshot.strategyQueuedShares[strategy] = node.queuedShares(strategy); + + // Store withdrawn amount for each token + snapshot.withdrawnByToken[address(asset)] = node.withdrawn(asset); + + // Store staked asset balance for each token + snapshot.stakedAssetBalanceForNode[address(asset)] + = state.eigenStrategyManager().getStakedAssetBalanceForNode(asset, nodeId); + + // Store strategy shares for each token + snapshot.strategySharesForNode[address(asset)] = + state.eigenStrategyManager().strategyManager().stakerStrategyShares(address(node), strategy); + } + } + + function totalAssets() external view returns (uint256) { + return snapshot.totalAssets; + } + + function totalSupply() external view returns (uint256) { + return snapshot.totalSupply; + } + + function getStrategyQueuedShares(IStrategy strategy) external view returns (uint256) { + return snapshot.strategyQueuedShares[strategy]; + } + + function getWithdrawnByToken(address token) external view returns (uint256) { + return snapshot.withdrawnByToken[token]; + } + + function getStakedAssetBalanceForNode(address token) external view returns (uint256) { + return snapshot.stakedAssetBalanceForNode[token]; + } + + function getStrategySharesForNode(address token) external view returns (uint256) { + return snapshot.strategySharesForNode[token]; + } +} + contract TokenStakingNodeTest is ynEigenIntegrationBaseTest { ITokenStakingNode tokenStakingNode; @@ -104,6 +193,180 @@ contract TokenStakingNodeTest is ynEigenIntegrationBaseTest { vm.expectRevert("Pausable: index is paused"); eigenStrategyManager.stakeAssetsToNode(nodeId, assets, amounts); } + + + function testTokenQueueWithdrawals() public { + + uint256 wstethAmount = 100 ether; + IERC20 wstETH = IERC20(chainAddresses.lsd.WSTETH_ADDRESS); + uint256 nodeId = tokenStakingNode.nodeId(); + { + // 1. Obtain wstETH and Deposit assets to ynEigen by User + testAssetUtils.depositAsset(ynEigenToken, address(wstETH), wstethAmount, address(this)); + + // 2. Deposit assets to Eigenlayer by Token Staking Node + + IERC20[] memory assets = new IERC20[](1); + assets[0] = wstETH; + uint256[] memory amounts = new uint256[](1); + amounts[0] = wstethAmount; + vm.prank(actors.ops.STRATEGY_CONTROLLER); + eigenStrategyManager.stakeAssetsToNode(nodeId, assets, amounts); + } + + NodeStateSnapshot before = new NodeStateSnapshot(); + before.takeSnapshot(address(this), nodeId); + + uint256 withdrawnShares = 50 ether; + + // 3. Queue withdrawals + vm.startPrank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = tokenStakingNode.queueWithdrawals( + eigenStrategyManager.strategies(wstETH), + withdrawnShares + ); + vm.stopPrank(); + + NodeStateSnapshot afterQueued = new NodeStateSnapshot(); + afterQueued.takeSnapshot(address(this), nodeId); + + // Assert queuedShares increased + assertEq( + afterQueued.getStrategyQueuedShares(eigenStrategyManager.strategies(wstETH)), + before.getStrategyQueuedShares(eigenStrategyManager.strategies(wstETH)) + withdrawnShares, + "Queued shares should have increased" + ); + + // Assert everything else stayed the same + assertEq( + afterQueued.totalAssets(), + before.totalAssets(), + "Total assets should not have changed" + ); + assertEq( + afterQueued.totalSupply(), + before.totalSupply(), + "Total supply should not have changed" + ); + assertEq( + afterQueued.getWithdrawnByToken(address(wstETH)), + before.getWithdrawnByToken(address(wstETH)), + "Withdrawn amount should not have changed" + ); + + // Assert staked asset balance decreased + assertApproxEqAbs( + afterQueued.getStakedAssetBalanceForNode(address(wstETH)), + before.getStakedAssetBalanceForNode(address(wstETH)), + 10, + "Staked asset balance should have decreased by withdrawn shares" + ); + + // Assert strategy shares decreased + assertEq( + afterQueued.getStrategySharesForNode(address(wstETH)), + before.getStrategySharesForNode(address(wstETH)) - withdrawnShares, + "Strategy shares should have decreased by withdrawn shares" + ); + } + + function testQueueAndCompleteWithdrawals() public { + + // Stake some wstETH to the node + uint256 stakeAmount = 100 ether; + IERC20 wstETH = IERC20(chainAddresses.lsd.WSTETH_ADDRESS); + uint256 nodeId = tokenStakingNode.nodeId(); + uint256 sharesToWithdraw; + uint256 withdrawAmount; + IStrategy wstETHStrategy = eigenStrategyManager.strategies(wstETH); + uint32 _startBlock; + { + // 1. Obtain wstETH and Deposit assets to ynEigen by User + testAssetUtils.depositAsset(ynEigenToken, address(wstETH), stakeAmount, address(this)); + + IERC20[] memory assets = new IERC20[](1); + assets[0] = IERC20(address(wstETH)); + uint256[] memory amounts = new uint256[](1); + amounts[0] = stakeAmount; + + vm.prank(actors.ops.STRATEGY_CONTROLLER); + eigenStrategyManager.stakeAssetsToNode(nodeId, assets, amounts); + + // Prepare for withdrawal + withdrawAmount = stakeAmount / 2; + sharesToWithdraw = wstETHStrategy.underlyingToShares( + IwstETH(address(wstETH)).getStETHByWstETH(withdrawAmount) + ); + + // Queue withdrawal + vm.prank(actors.ops.STAKING_NODES_WITHDRAWER); + bytes32[] memory withdrawalRoots = tokenStakingNode.queueWithdrawals(wstETHStrategy, sharesToWithdraw); + + _startBlock = uint32(block.number); + + IStrategy[] memory _strategies = new IStrategy[](1); + _strategies[0] = wstETHStrategy; + vm.roll(block.number + eigenLayer.delegationManager.getWithdrawalDelay(_strategies)); + } + + // Capture state before completing withdrawal + NodeStateSnapshot before = new NodeStateSnapshot(); + before.takeSnapshot(address(this), nodeId); + + // Complete queued withdrawal + vm.startPrank(actors.ops.STAKING_NODES_WITHDRAWER); + tokenStakingNode.completeQueuedWithdrawals( + eigenLayer.delegationManager.cumulativeWithdrawalsQueued(address(tokenStakingNode)) - 1, // _nonce + _startBlock, // _startBlock + sharesToWithdraw, // _shares + wstETHStrategy, // _strategy + new uint256[](1), // _middlewareTimesIndexes + false + ); + vm.stopPrank(); + + NodeStateSnapshot afterCompletion = new NodeStateSnapshot(); + afterCompletion.takeSnapshot(address(this), nodeId); + + // Assert withdrawn amount increased + assertApproxEqAbs( + afterCompletion.getWithdrawnByToken(address(wstETH)), + before.getWithdrawnByToken(address(wstETH)) + withdrawAmount, + 3, + "Withdrawn amount should have increased by withdrawAmount" + ); + + // Assert queued shares decreased + assertEq( + afterCompletion.getStrategyQueuedShares(wstETHStrategy), + before.getStrategyQueuedShares(wstETHStrategy) - sharesToWithdraw, + "Queued shares should have decreased by sharesToWithdraw" + ); + + // Assert total supply remained unchanged + assertEq( + afterCompletion.totalSupply(), + before.totalSupply(), + "Total supply should remain unchanged" + ); + + // Assert total assets remained approximately unchanged + assertApproxEqAbs( + afterCompletion.totalAssets(), + before.totalAssets(), + 3, + "Total assets should remain approximately unchanged" + ); + + eigenStrategyManager.updateTokenStakingNodesBalances(wstETH); + + assertApproxEqAbs( + afterCompletion.totalAssets(), + ynEigenToken.totalAssets(), + 3, + "Total assets should have decreased by approximately the withdraw amount" + ); + } } @@ -189,4 +452,5 @@ contract TokenStakingNodeDelegate is ynEigenIntegrationBaseTest { address delegatedAddress = delegationManager.delegatedTo(address(tokenStakingNodeInstance)); assertEq(delegatedAddress, address(0), "Delegation should be cleared after undelegation."); } -} \ No newline at end of file +} + diff --git a/test/integration/ynEIGEN/ynEigen.t.sol b/test/integration/ynEIGEN/ynEigen.t.sol index ab9af3085..1fa7673a8 100644 --- a/test/integration/ynEIGEN/ynEigen.t.sol +++ b/test/integration/ynEIGEN/ynEigen.t.sol @@ -320,6 +320,70 @@ contract ynEigenTest is ynEigenIntegrationBaseTest { uint256 ethAmount = assetRegistry.convertToUnitOfAccount(asset, amount); assertEq(ethAmount, expectedETHAmount, "convertToEth does not match expected value"); } + + function testPreviewRedeemWSTETH(uint256 amount) public { + vm.assume( + amount < 10000 ether && amount >= 10 wei + ); + + IERC20 asset = IERC20(chainAddresses.lsd.WSTETH_ADDRESS); + + uint256 wstethPrice = rateProvider.rate(chainAddresses.lsd.WSTETH_ADDRESS); + + uint256 expectedRedeemPreview = amount * 1e18 / wstethPrice; + uint256 previewRedeem = ynEigenToken.previewRedeem(asset, amount); + assertEq(previewRedeem, expectedRedeemPreview, "testPreviewRedeemWSTETH: E0"); + } + + function testPreviewRedeemWOETH(uint256 amount) public { + vm.assume( + amount < 10000 ether && amount >= 10 wei + ); + + IERC20 asset = IERC20(chainAddresses.lsd.WOETH_ADDRESS); + + uint256 woethPrice = rateProvider.rate(chainAddresses.lsd.WOETH_ADDRESS); + + uint256 expectedRedeemPreview = amount * 1e18 / woethPrice; + uint256 previewRedeem = ynEigenToken.previewRedeem(asset, amount); + assertEq(previewRedeem, expectedRedeemPreview, "testPreviewRedeemWOETH: E0"); + } + + function testPreviewRedeemSFRXETH(uint256 amount) public { + vm.assume( + amount < 10000 ether && amount >= 10 wei + ); + + IERC20 asset = IERC20(chainAddresses.lsd.SFRXETH_ADDRESS); + + uint256 sfrxETHPrice = rateProvider.rate(chainAddresses.lsd.SFRXETH_ADDRESS); + + uint256 expectedRedeemPreview = amount * 1e18 / sfrxETHPrice; + uint256 previewRedeem = ynEigenToken.previewRedeem(asset, amount); + assertEq(previewRedeem, expectedRedeemPreview, "testPreviewRedeemSFRXETH: E0"); + } + + function testPreviewRedeemRETH(uint256 amount) public { + vm.assume( + amount < 10000 ether && amount >= 10 wei + ); + + IERC20 asset = IERC20(chainAddresses.lsd.RETH_ADDRESS); + + uint256 rethPrice = rateProvider.rate(chainAddresses.lsd.RETH_ADDRESS); + + uint256 expectedRedeemPreview = amount * 1e18 / rethPrice; + uint256 previewRedeem = ynEigenToken.previewRedeem(asset, amount); + assertEq(previewRedeem, expectedRedeemPreview, "testPreviewRedeemRETH: E0"); + } + + function testPreviewRedeemNoAssets(uint256 amount) public { + vm.assume( + amount < 10000 ether && amount >= 10 wei + ); + + assertEq(amount, ynEigenToken.previewRedeem(amount), "testPreviewRedeemNoAssets: E0"); + } } contract ynTransferPauseTest is ynEigenIntegrationBaseTest { diff --git a/test/integration/ynEIGEN/ynEigenIntegrationBaseTest.sol b/test/integration/ynEIGEN/ynEigenIntegrationBaseTest.sol index 2a442b343..d31ecb23f 100644 --- a/test/integration/ynEIGEN/ynEigenIntegrationBaseTest.sol +++ b/test/integration/ynEIGEN/ynEigenIntegrationBaseTest.sol @@ -22,7 +22,11 @@ import {Utils} from "script/Utils.sol"; import {ActorAddresses} from "script/Actors.sol"; import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; import {LSDRateProvider} from "src/ynEIGEN/LSDRateProvider.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; +import {IRedeemableAsset} from "src/interfaces/IRedeemableAsset.sol"; import {IynEigen} from "src/interfaces/IynEigen.sol"; import {IRateProvider} from "src/interfaces/IRateProvider.sol"; import {IAssetRegistry} from "src/interfaces/IAssetRegistry.sol"; @@ -62,9 +66,12 @@ contract ynEigenIntegrationBaseTest is Test, Utils { AssetRegistry public assetRegistry; LSDRateProvider public rateProvider; ynEigenDepositAdapter public ynEigenDepositAdapterInstance; + RedemptionAssetsVault public redemptionAssetsVault; + WithdrawalQueueManager public withdrawalQueueManager; + LSDWrapper public wrapper; // Strategy - EigenStrategyManager eigenStrategyManager; + EigenStrategyManager public eigenStrategyManager; // Eigen struct EigenLayer { @@ -92,7 +99,7 @@ contract ynEigenIntegrationBaseTest is Test, Utils { setupEigenLayer(); setupTokenStakingNodesManager(); setupYnEigen(); - setupEigenStrategyManagerAndAssetRegistry(); + setupYieldNestAssets(); setupYnEigenDepositAdapter(); } @@ -209,7 +216,7 @@ contract ynEigenIntegrationBaseTest is Test, Utils { tokenStakingNodesManager.registerTokenStakingNode(address(tokenStakingNodeImplementation)); } - function setupEigenStrategyManagerAndAssetRegistry() public { + function setupYieldNestAssets() public { IERC20[] memory lsdAssets = new IERC20[](5); IStrategy[] memory strategies = new IStrategy[](5); @@ -270,6 +277,77 @@ contract ynEigenIntegrationBaseTest is Test, Utils { assetManagerRole: actors.admin.ASSET_MANAGER }); assetRegistry.initialize(assetRegistryInit); + + // deploy RedemptionAssetsVault + { + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address(new RedemptionAssetsVault()), + actors.admin.PROXY_ADMIN_OWNER, + "" + ); + redemptionAssetsVault = RedemptionAssetsVault(payable(address(_proxy))); + } + + // deploy WithdrawalQueueManager + { + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address(new WithdrawalQueueManager()), + actors.admin.PROXY_ADMIN_OWNER, + "" + ); + withdrawalQueueManager = WithdrawalQueueManager(address(_proxy)); + } + + // deploy wrapper + { + // call `initialize` on LSDWrapper + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address( + new LSDWrapper( + chainAddresses.lsd.WSTETH_ADDRESS, + chainAddresses.lsd.WOETH_ADDRESS, + chainAddresses.lsd.OETH_ADDRESS, + chainAddresses.lsd.STETH_ADDRESS) + ), + actors.admin.PROXY_ADMIN_OWNER, + abi.encodeWithSignature("initialize()") + ); + wrapper = LSDWrapper(address(_proxy)); + } + + // initialize eigenStrategyManager + { + eigenStrategyManager.initializeV2(address(redemptionAssetsVault), address(wrapper), actors.ops.WITHDRAWAL_MANAGER); + } + + // initialize RedemptionAssetsVault + { + RedemptionAssetsVault.Init memory _init = RedemptionAssetsVault.Init({ + admin: actors.admin.PROXY_ADMIN_OWNER, + redeemer: address(withdrawalQueueManager), + ynEigen: ynEigenToken, + assetRegistry: assetRegistry + }); + redemptionAssetsVault.initialize(_init); + } + + // initialize WithdrawalQueueManager + { + WithdrawalQueueManager.Init memory _init = WithdrawalQueueManager.Init({ + name: "ynLSDe Withdrawal Manager", + symbol: "ynLSDeWM", + redeemableAsset: IRedeemableAsset(address(ynEigenToken)), + redemptionAssetsVault: redemptionAssetsVault, + admin: actors.admin.PROXY_ADMIN_OWNER, + withdrawalQueueAdmin: actors.ops.WITHDRAWAL_MANAGER, + redemptionAssetWithdrawer: actors.ops.REDEMPTION_ASSET_WITHDRAWER, + requestFinalizer: actors.ops.REQUEST_FINALIZER, + // withdrawalFee: 500, // 0.05% + withdrawalFee: 0, + feeReceiver: actors.admin.FEE_RECEIVER + }); + withdrawalQueueManager.initialize(_init); + } } function setupYnEigenDepositAdapter() public { @@ -281,6 +359,7 @@ contract ynEigenIntegrationBaseTest is Test, Utils { }); vm.prank(actors.admin.PROXY_ADMIN_OWNER); ynEigenDepositAdapterInstance.initialize(ynEigenDepositAdapterInit); + ynEigenDepositAdapterInstance.initializeV2(address(wrapper)); } } diff --git a/test/integration/ynEIGEN/ynEigenViewer.t.sol b/test/integration/ynEIGEN/ynEigenViewer.t.sol index c8d2992ae..bc7e66df3 100644 --- a/test/integration/ynEIGEN/ynEigenViewer.t.sol +++ b/test/integration/ynEIGEN/ynEigenViewer.t.sol @@ -4,17 +4,29 @@ pragma solidity 0.8.24; import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {ITokenStakingNodesManager,ITokenStakingNode} from "../../../src/interfaces/ITokenStakingNodesManager.sol"; - +import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; import {ynEigenViewer} from "../../../src/ynEIGEN/ynEigenViewer.sol"; import {console} from "forge-std/console.sol"; import "./ynEigenIntegrationBaseTest.sol"; + + interface IAssetRegistryView { function assets() external view returns (IERC20Metadata[] memory); } contract ynEigenViewerTest is ynEigenIntegrationBaseTest { + TestAssetUtils testAssetUtils; + address[10] public depositors; + + constructor() { + testAssetUtils = new TestAssetUtils(); + for (uint i = 0; i < 10; i++) { + depositors[i] = address(uint160(uint256(keccak256(abi.encodePacked("depositor", i))))); + } + } + ynEigenViewer private _ynEigenViewer; function setUp() public override { @@ -42,10 +54,138 @@ contract ynEigenViewerTest is ynEigenIntegrationBaseTest { assertEq(_assetsInfo[i].name, IERC20Metadata(address(_assets[i])).name(), "testGetYnEigenAssets: E2"); assertEq(_assetsInfo[i].symbol, IERC20Metadata(address(_assets[i])).symbol(), "testGetYnEigenAssets: E3"); assertEq(_assetsInfo[i].ratioOfTotalAssets, 0, "testGetYnEigenAssets: E4"); - assertEq(_assetsInfo[i].totalBalance, 0, "testGetYnEigenAssets: E5"); + assertEq(_assetsInfo[i].totalBalanceInUnitOfAccount, 0, "testGetYnEigenAssets: E5"); + assertEq(_assetsInfo[i].totalBalanceInAsset, 0, "testGetYnEigenAssets: E5"); } } + function testGetYnEigenAssetsAfterDeposits() public { + // Define deposit amounts + uint256 sfrxEthAmount = 1 ether; + uint256 wstEthAmount = 0.5 ether; + uint256 rEthAmount = 0.75 ether; + + // Create a user for deposits + address user = makeAddr("userXYZ"); + + // Make deposits to the user + deal(address(chainAddresses.lsd.SFRXETH_ADDRESS), user, sfrxEthAmount); + deal(address(chainAddresses.lsd.WSTETH_ADDRESS), user, wstEthAmount); + deal(address(chainAddresses.lsd.RETH_ADDRESS), user, rEthAmount); + + // Switch to user context + vm.startPrank(user); + + // Approve and deposit tokens + IERC20(chainAddresses.lsd.SFRXETH_ADDRESS).approve(address(ynEigenToken), sfrxEthAmount); + IERC20(chainAddresses.lsd.WSTETH_ADDRESS).approve(address(ynEigenToken), wstEthAmount); + IERC20(chainAddresses.lsd.RETH_ADDRESS).approve(address(ynEigenToken), rEthAmount); + + ynEigenToken.deposit(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS), sfrxEthAmount, user); + ynEigenToken.deposit(IERC20(chainAddresses.lsd.WSTETH_ADDRESS), wstEthAmount, user); + ynEigenToken.deposit(IERC20(chainAddresses.lsd.RETH_ADDRESS), rEthAmount, user); + + // End user context + vm.stopPrank(); + + + // Get asset info after deposits + ynEigenViewer.AssetInfo[] memory assetsInfo = _ynEigenViewer.getYnEigenAssets(); + + { + vm.startPrank(actors.ops.STAKING_NODE_CREATOR); + tokenStakingNodesManager.createTokenStakingNode(); + tokenStakingNodesManager.createTokenStakingNode(); + vm.stopPrank(); + + EigenStrategyManager.NodeAllocation[] memory allocations = new EigenStrategyManager.NodeAllocation[](2); + IERC20[] memory assets1 = new IERC20[](1); + uint256[] memory amounts1 = new uint256[](1); + assets1[0] = IERC20(chainAddresses.lsd.WSTETH_ADDRESS); + amounts1[0] = wstEthAmount / 4; + + testAssetUtils.depositAsset(ynEigenToken, address(assets1[0]), amounts1[0], depositors[0]); + + IERC20[] memory assets2 = new IERC20[](1); + uint256[] memory amounts2 = new uint256[](1); + assets2[0] = IERC20(chainAddresses.lsd.RETH_ADDRESS); + amounts2[0] = rEthAmount / 4; + + testAssetUtils.depositAsset(ynEigenToken, address(assets2[0]), amounts2[0], depositors[1]); + + allocations[0] = EigenStrategyManager.NodeAllocation(0, assets1, amounts1); + allocations[1] = EigenStrategyManager.NodeAllocation(1, assets2, amounts2); + + uint256 totalAssetsBefore = ynEigenToken.totalAssets(); + + vm.startPrank(actors.ops.STRATEGY_CONTROLLER); + eigenStrategyManager.stakeAssetsToNodes(allocations); + vm.stopPrank(); + + assertApproxEqRel(ynEigenToken.totalAssets(), totalAssetsBefore, 1e16, "Total assets should not change significantly after staking"); + } + + // Calculate the value of each deposit in ETH and its expected ratio + uint256 sfrxEthValueInEth = assetRegistry.convertToUnitOfAccount(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS), sfrxEthAmount); + uint256 wstEthValueInEth = assetRegistry.convertToUnitOfAccount(IERC20(chainAddresses.lsd.WSTETH_ADDRESS), wstEthAmount); + uint256 rEthValueInEth = assetRegistry.convertToUnitOfAccount(IERC20(chainAddresses.lsd.RETH_ADDRESS), rEthAmount); + + uint256 totalValueInEth = sfrxEthValueInEth + wstEthValueInEth + rEthValueInEth; + + uint256 expectedSfrxEthRatio = (sfrxEthValueInEth * 1e6) / totalValueInEth; + uint256 expectedWstEthRatio = (wstEthValueInEth * 1e6) / totalValueInEth; + uint256 expectedREthRatio = (rEthValueInEth * 1e6) / totalValueInEth; + + // Verify that the sum of total values in ETH matches totalAssets + uint256 summedTotalValueInEth = sfrxEthValueInEth + wstEthValueInEth + rEthValueInEth; + assertApproxEqRel( + summedTotalValueInEth, + ynEigenToken.totalAssets(), + 1e18, + "Sum of asset values in ETH should approximately equal totalAssets" + ); + + // Verify asset info + for (uint256 i = 0; i < assetsInfo.length; i++) { + if (assetsInfo[i].asset == address(chainAddresses.lsd.SFRXETH_ADDRESS)) { + assertEq(assetsInfo[i].name, "Staked Frax Ether", "Incorrect sfrxETH name"); + assertEq(assetsInfo[i].symbol, "sfrxETH", "Incorrect sfrxETH symbol"); + assertEq(assetsInfo[i].totalBalanceInUnitOfAccount, sfrxEthValueInEth, "Incorrect sfrxETH balance in unit of account"); + assertEq(assetsInfo[i].totalBalanceInAsset, sfrxEthAmount, "Incorrect sfrxETH balance in asset"); + assertApproxEqRel(assetsInfo[i].ratioOfTotalAssets, expectedSfrxEthRatio, 1e16, "Incorrect sfrxETH ratio"); + assertEq(assetsInfo[i].rate, rateProvider.rate(address(chainAddresses.lsd.SFRXETH_ADDRESS)), "sfrxETH rate mismatch with rateProvider"); + } else if (assetsInfo[i].asset == address(chainAddresses.lsd.WSTETH_ADDRESS)) { + assertEq(assetsInfo[i].name, "Wrapped liquid staked Ether 2.0", "Incorrect wstETH name"); + assertEq(assetsInfo[i].symbol, "wstETH", "Incorrect wstETH symbol"); + assertEq(assetsInfo[i].totalBalanceInUnitOfAccount, wstEthValueInEth, "Incorrect wstETH balance in unit of account"); + assertEq(assetsInfo[i].totalBalanceInAsset, wstEthAmount, "Incorrect wstETH balance in asset"); + assertApproxEqRel(assetsInfo[i].ratioOfTotalAssets, expectedWstEthRatio, 1e16, "Incorrect wstETH ratio"); + assertEq(assetsInfo[i].rate, rateProvider.rate(address(chainAddresses.lsd.WSTETH_ADDRESS)), "wstETH rate mismatch with rateProvider"); + } else if (assetsInfo[i].asset == address(chainAddresses.lsd.RETH_ADDRESS)) { + assertEq(assetsInfo[i].name, "Rocket Pool ETH", "Incorrect rETH name"); + assertEq(assetsInfo[i].symbol, "rETH", "Incorrect rETH symbol"); + assertEq(assetsInfo[i].totalBalanceInUnitOfAccount, rEthValueInEth, "Incorrect rETH balance in unit of account"); + assertEq(assetsInfo[i].totalBalanceInAsset, rEthAmount, "Incorrect rETH balance in asset"); + assertApproxEqRel(assetsInfo[i].ratioOfTotalAssets, expectedREthRatio, 1e16, "Incorrect rETH ratio"); + assertEq(assetsInfo[i].rate, rateProvider.rate(address(chainAddresses.lsd.RETH_ADDRESS)), "rETH rate mismatch with rateProvider"); + } else { + assertEq(assetsInfo[i].totalBalanceInUnitOfAccount, 0, "Non-zero balance for undeposited asset in unit of account"); + assertEq(assetsInfo[i].totalBalanceInAsset, 0, "Non-zero balance for undeposited asset in asset"); + assertEq(assetsInfo[i].ratioOfTotalAssets, 0, "Non-zero ratio for undeposited asset"); + assertEq(assetsInfo[i].rate, rateProvider.rate(assetsInfo[i].asset), "Rate mismatch with rateProvider for undeposited asset"); + } + } + + // Calculate the sum of all ratios + uint256 totalRatio = 0; + for (uint256 i = 0; i < assetsInfo.length; i++) { + totalRatio += assetsInfo[i].ratioOfTotalAssets; + } + + // Assert that the sum of all ratios is approximately equal to 1e6 (100%) + assertApproxEqRel(totalRatio, 1e6, 1e18, "Sum of all asset ratios should be approximately 1e6 (100%)"); + } + function testPreviewDepositStETH() public { // Set up test amount uint256 testAmount = 1 ether; diff --git a/test/scenarios/ynEIGEN/UpgradeTestTemp.t.sol b/test/scenarios/ynEIGEN/UpgradeTestTemp.t.sol new file mode 100644 index 000000000..987ce4c4b --- /dev/null +++ b/test/scenarios/ynEIGEN/UpgradeTestTemp.t.sol @@ -0,0 +1,59 @@ +// // SPDX-License-Identifier: BSD 3-Clause License +// pragma solidity ^0.8.24; + +// import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +// import "forge-std/Test.sol"; + +// contract SmallTest is Test { + +// address _proxy = 0x58c721D3f6FcdE97ef07df793D359DbCc1fDB36d; +// address _newImplementation = 0xD82d61C2905595f554233De5cDb947979802267C; +// address _ynSecurityCouncil = 0x743b91CDB1C694D4F51bCDA3a4A59DcC0d02b913; +// TimelockController timelockController = TimelockController(payable(0x317f96879FA387aFF6dfFAAc4A09bD2f6e367801)); + +// function setUp() public {} + +// function testSmall() public { +// bytes memory _data = abi.encodeWithSignature( +// "upgradeAndCall(address,address,bytes)", +// _proxy, // proxy +// _newImplementation, // implementation +// "" +// ); +// console.logBytes(_data); +// console.log("15 minutes: ", 15 minutes); +// console.logBytes32(bytes32(0)); +// vm.startPrank(_ynSecurityCouncil); +// timelockController.schedule( +// // getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target +// 0x3BE30C73AF6b1c5d6d13E20B41D89a81FC074211, +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0), // salt +// timelockController.getMinDelay() // delay +// ); +// vm.stopPrank(); + +// uint256 minDelay; +// if (block.chainid == 1) { // Mainnet +// minDelay = 3 days; +// } else if (block.chainid == 17000) { // Holesky +// minDelay = 15 minutes; +// } else { +// revert("Unsupported chain ID"); +// } +// skip(minDelay); + +// vm.startPrank(_ynSecurityCouncil); +// timelockController.execute( +// 0x3BE30C73AF6b1c5d6d13E20B41D89a81FC074211, // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0) // salt +// ); +// vm.stopPrank(); +// } +// } \ No newline at end of file diff --git a/test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol b/test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol new file mode 100644 index 000000000..9ae212edd --- /dev/null +++ b/test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; + +import "./ynLSDeWithdrawals.t.sol"; + +contract ynLSDeDepositAdapterTest is ynLSDeWithdrawalsTest { + + TestAssetUtils public testAssetUtils; + + function setUp() public override { + super.setUp(); + + // upgrade deposit adapter + { + _upgradeContract( + address(ynEigenDepositAdapter_), + address(new ynEigenDepositAdapter()), + abi.encodeWithSignature("initializeV2(address)", address(wrapper)) + ); + } + + // deploy testAssetUtils + { + testAssetUtils = new TestAssetUtils(); + } + } + + function testDepositSTETH(uint256 _amount) public { + vm.assume(_amount > 10_000 && _amount <= 10 ether); + + testAssetUtils.get_stETH(user, _amount); + + vm.startPrank(user); + IERC20(chainAddresses.lsd.STETH_ADDRESS).approve(address(ynEigenDepositAdapter_), _amount); + uint256 _ynOut = ynEigenDepositAdapter_.deposit(IERC20(chainAddresses.lsd.STETH_ADDRESS), _amount, user); + vm.stopPrank(); + + assertEq(IERC20(yneigen).balanceOf(user), _ynOut, "testDepositSTETH"); + } + + function testDepositOETH(uint256 _amount) public { + vm.assume(_amount > 10_000 && _amount <= 10 ether); + + testAssetUtils.get_OETH(user, _amount + 10); + + vm.startPrank(user); + IERC20(chainAddresses.lsd.OETH_ADDRESS).approve(address(ynEigenDepositAdapter_), _amount); + uint256 _ynOut = ynEigenDepositAdapter_.deposit(IERC20(chainAddresses.lsd.OETH_ADDRESS), _amount, user); + vm.stopPrank(); + + assertEq(IERC20(yneigen).balanceOf(user), _ynOut, "testDepositOETH"); + } +} \ No newline at end of file diff --git a/test/scenarios/ynEIGEN/ynLSDeUpgradeScenario.sol b/test/scenarios/ynEIGEN/ynLSDeUpgradeScenario.sol index 5dfc8956a..33c3ee246 100644 --- a/test/scenarios/ynEIGEN/ynLSDeUpgradeScenario.sol +++ b/test/scenarios/ynEIGEN/ynLSDeUpgradeScenario.sol @@ -1,375 +1,380 @@ -// SPDX-License-Identifier: BSD 3-Clause License -pragma solidity ^0.8.24; +// // SPDX-License-Identifier: BSD 3-Clause License +// pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +// import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +// import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {ITokenStakingNode} from "../../../src/interfaces/ITokenStakingNode.sol"; +// import {ITokenStakingNode} from "../../../src/interfaces/ITokenStakingNode.sol"; -import {HoleskyLSDRateProvider} from "../../../src/testnet/HoleksyLSDRateProvider.sol"; +// import {HoleskyLSDRateProvider} from "../../../src/testnet/HoleksyLSDRateProvider.sol"; -import {TestStakingNodesManagerV2} from "../../mocks/TestStakingNodesManagerV2.sol"; -import {TestStakingNodeV2} from "../../mocks/TestStakingNodeV2.sol"; +// import {TestStakingNodesManagerV2} from "../../mocks/TestStakingNodesManagerV2.sol"; +// // import {TestStakingNodeV2} from "../../mocks/TestStakingNodeV2.sol"; +// import {TokenStakingNode} from "../../../src/ynEIGEN/TokenStakingNode.sol"; -import "./ynLSDeScenarioBaseTest.sol"; -import {console} from "forge-std/console.sol"; +// import "./ynLSDeScenarioBaseTest.sol"; +// import {console} from "forge-std/console.sol"; -contract ynLSDeUpgradeScenario is ynLSDeScenarioBaseTest { +// contract ynLSDeUpgradeScenario is ynLSDeScenarioBaseTest { + +// function setUp() public virtual override { +// super.setUp(); +// } - function test_Upgrade_ynLSDe_Scenario() public { +// function test_Upgrade_ynLSDe_Scenario() public { - address previousImplementation = getTransparentUpgradeableProxyImplementationAddress(address(yneigen)); - console.log("Total assets before upgrade:", yneigen.totalAssets()); - address newImplementation = address(new ynEigen()); +// address previousImplementation = getTransparentUpgradeableProxyImplementationAddress(address(yneigen)); +// console.log("Total assets before upgrade:", yneigen.totalAssets()); +// address newImplementation = address(new ynEigen()); - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - upgradeContract(address(yneigen), newImplementation); +// upgradeContract(address(yneigen), newImplementation); - runUpgradeInvariants(address(yneigen), previousImplementation, newImplementation); - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(yneigen)); - } +// runUpgradeInvariants(address(yneigen), previousImplementation, newImplementation); +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(yneigen)); +// } - function test_Upgrade_TokenStakingNodesManager_Scenario() public { +// function test_Upgrade_TokenStakingNodesManager_Scenario() public { - address previousStakingNodesManagerImpl = getTransparentUpgradeableProxyImplementationAddress(address(tokenStakingNodesManager)); - address newStakingNodesManagerImpl = address(new TokenStakingNodesManager()); +// address previousStakingNodesManagerImpl = getTransparentUpgradeableProxyImplementationAddress(address(tokenStakingNodesManager)); +// address newStakingNodesManagerImpl = address(new TokenStakingNodesManager()); - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - upgradeContract(address(tokenStakingNodesManager), newStakingNodesManagerImpl); +// upgradeContract(address(tokenStakingNodesManager), newStakingNodesManagerImpl); - runUpgradeInvariants(address(tokenStakingNodesManager), previousStakingNodesManagerImpl, newStakingNodesManagerImpl); - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(tokenStakingNodesManager)); - } +// runUpgradeInvariants(address(tokenStakingNodesManager), previousStakingNodesManagerImpl, newStakingNodesManagerImpl); +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(tokenStakingNodesManager)); +// } - function test_Upgrade_AssetRegistry() public { +// function test_Upgrade_AssetRegistry() public { - address previousAssetRegistryImpl = getTransparentUpgradeableProxyImplementationAddress(address(assetRegistry)); - address newAssetRegistryImpl = address(new AssetRegistry()); +// address previousAssetRegistryImpl = getTransparentUpgradeableProxyImplementationAddress(address(assetRegistry)); +// address newAssetRegistryImpl = address(new AssetRegistry()); - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - upgradeContract(address(assetRegistry), newAssetRegistryImpl); +// upgradeContract(address(assetRegistry), newAssetRegistryImpl); - runUpgradeInvariants(address(assetRegistry), previousAssetRegistryImpl, newAssetRegistryImpl); - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(assetRegistry)); - } +// runUpgradeInvariants(address(assetRegistry), previousAssetRegistryImpl, newAssetRegistryImpl); +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(assetRegistry)); +// } - function test_Upgrade_EigenStrategyManager() public { +// function test_Upgrade_EigenStrategyManager() public { - address previousEigenStrategyManagerImpl = getTransparentUpgradeableProxyImplementationAddress(address(eigenStrategyManager)); - address newEigenStrategyManagerImpl = address(new EigenStrategyManager()); +// address previousEigenStrategyManagerImpl = getTransparentUpgradeableProxyImplementationAddress(address(eigenStrategyManager)); +// address newEigenStrategyManagerImpl = address(new EigenStrategyManager()); - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - upgradeContract(address(eigenStrategyManager), newEigenStrategyManagerImpl); +// upgradeContract(address(eigenStrategyManager), newEigenStrategyManagerImpl); - runUpgradeInvariants(address(eigenStrategyManager), previousEigenStrategyManagerImpl, newEigenStrategyManagerImpl); - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(eigenStrategyManager)); - } +// runUpgradeInvariants(address(eigenStrategyManager), previousEigenStrategyManagerImpl, newEigenStrategyManagerImpl); +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(eigenStrategyManager)); +// } - function test_Upgrade_LSDRateProvider() public { +// function test_Upgrade_LSDRateProvider() public { - address previousLSDRateProviderImpl = getTransparentUpgradeableProxyImplementationAddress(address(lsdRateProvider)); - address newLSDRateProviderImpl; - if (block.chainid == 17000) { // Holesky - newLSDRateProviderImpl = address(new HoleskyLSDRateProvider()); - } else if (block.chainid == 1) { // Mainnet - newLSDRateProviderImpl = address(new LSDRateProvider()); - } else { - revert("Unsupported chain ID"); - } +// address previousLSDRateProviderImpl = getTransparentUpgradeableProxyImplementationAddress(address(lsdRateProvider)); +// address newLSDRateProviderImpl; +// if (block.chainid == 17000) { // Holesky +// newLSDRateProviderImpl = address(new HoleskyLSDRateProvider()); +// } else if (block.chainid == 1) { // Mainnet +// newLSDRateProviderImpl = address(new LSDRateProvider()); +// } else { +// revert("Unsupported chain ID"); +// } - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - upgradeContract(address(lsdRateProvider), newLSDRateProviderImpl); +// upgradeContract(address(lsdRateProvider), newLSDRateProviderImpl); - runUpgradeInvariants(address(lsdRateProvider), previousLSDRateProviderImpl, newLSDRateProviderImpl); - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(lsdRateProvider)); - } +// runUpgradeInvariants(address(lsdRateProvider), previousLSDRateProviderImpl, newLSDRateProviderImpl); +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(lsdRateProvider)); +// } - function test_Upgrade_ynEigenDepositAdapter() public { +// function test_Upgrade_ynEigenDepositAdapter() public { - address previousYnEigenDepositAdapterImpl = getTransparentUpgradeableProxyImplementationAddress(address(ynEigenDepositAdapter_)); - address newYnEigenDepositAdapterImpl = address(new ynEigenDepositAdapter()); +// address previousYnEigenDepositAdapterImpl = getTransparentUpgradeableProxyImplementationAddress(address(ynEigenDepositAdapter_)); +// address newYnEigenDepositAdapterImpl = address(new ynEigenDepositAdapter()); - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - upgradeContract(address(ynEigenDepositAdapter_), newYnEigenDepositAdapterImpl); +// upgradeContract(address(ynEigenDepositAdapter_), newYnEigenDepositAdapterImpl); - runUpgradeInvariants(address(ynEigenDepositAdapter_), previousYnEigenDepositAdapterImpl, newYnEigenDepositAdapterImpl); - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(ynEigenDepositAdapter_)); - } - - function test_Upgrade_AllContracts_Batch() public { - address[] memory contracts = new address[](6); - address[] memory newImplementations = new address[](6); - - // YnLSDe - contracts[0] = address(yneigen); - newImplementations[0] = address(new ynEigen()); - - // EigenStrategyManager - contracts[1] = address(eigenStrategyManager); - newImplementations[1] = address(new EigenStrategyManager()); - - // LSDRateProvider - if (block.chainid == 17000) { // Holesky - newImplementations[2] = address(new HoleskyLSDRateProvider()); - } else if (block.chainid == 1) { // Mainnet - newImplementations[2] = address(new LSDRateProvider()); - } else { - revert("Unsupported chain ID"); - } - contracts[2] = address(lsdRateProvider); - - // ynEigenDepositAdapter - contracts[3] = address(ynEigenDepositAdapter_); - newImplementations[3] = address(new ynEigenDepositAdapter()); - - // AssetRegistry - contracts[4] = address(assetRegistry); - newImplementations[4] = address(new AssetRegistry()); - - // TokenStakingNodesManager - contracts[5] = address(tokenStakingNodesManager); - newImplementations[5] = address(new TokenStakingNodesManager()); - - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - - address[] memory previousImplementations = new address[](contracts.length); - for (uint i = 0; i < contracts.length; i++) { - previousImplementations[i] = getTransparentUpgradeableProxyImplementationAddress(contracts[i]); - } - - upgradeContractBatch(contracts, newImplementations); - - for (uint i = 0; i < contracts.length; i++) { - runUpgradeInvariants(contracts[i], previousImplementations[i], newImplementations[i]); - runTransferOwnership(contracts[i]); - } - - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - } - - function test_Upgrade_TokenStakingNodeImplementation_Scenario() public { - - ITokenStakingNode[] memory tokenStakingNodesBefore = tokenStakingNodesManager.getAllNodes(); - - uint256 previousTotalAssets = yneigen.totalAssets(); - uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); - - TestStakingNodeV2 testStakingNodeV2 = new TestStakingNodeV2(); - { - bytes memory _data = abi.encodeWithSignature( - "upgradeTokenStakingNode(address)", - payable(testStakingNodeV2) - ); - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.schedule( - address(tokenStakingNodesManager), // target - 0, // value - _data, - bytes32(0), // predecessor - bytes32(0), // salt - timelockController.getMinDelay() // delay - ); - vm.stopPrank(); - - uint256 minDelay; - if (block.chainid == 1) { // Mainnet - minDelay = 3 days; - } else if (block.chainid == 17000) { // Holesky - minDelay = 15 minutes; - } else { - revert("Unsupported chain ID"); - } - skip(minDelay); - - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.execute( - address(tokenStakingNodesManager), // target - 0, // value - _data, - bytes32(0), // predecessor - bytes32(0) // salt - ); - vm.stopPrank(); - } +// runUpgradeInvariants(address(ynEigenDepositAdapter_), previousYnEigenDepositAdapterImpl, newYnEigenDepositAdapterImpl); +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(ynEigenDepositAdapter_)); +// } + +// function test_Upgrade_AllContracts_Batch() public { +// address[] memory contracts = new address[](6); +// address[] memory newImplementations = new address[](6); + +// // YnLSDe +// contracts[0] = address(yneigen); +// newImplementations[0] = address(new ynEigen()); + +// // EigenStrategyManager +// contracts[1] = address(eigenStrategyManager); +// newImplementations[1] = address(new EigenStrategyManager()); + +// // LSDRateProvider +// if (block.chainid == 17000) { // Holesky +// newImplementations[2] = address(new HoleskyLSDRateProvider()); +// } else if (block.chainid == 1) { // Mainnet +// newImplementations[2] = address(new LSDRateProvider()); +// } else { +// revert("Unsupported chain ID"); +// } +// contracts[2] = address(lsdRateProvider); + +// // ynEigenDepositAdapter +// contracts[3] = address(ynEigenDepositAdapter_); +// newImplementations[3] = address(new ynEigenDepositAdapter()); + +// // AssetRegistry +// contracts[4] = address(assetRegistry); +// newImplementations[4] = address(new AssetRegistry()); + +// // TokenStakingNodesManager +// contracts[5] = address(tokenStakingNodesManager); +// newImplementations[5] = address(new TokenStakingNodesManager()); + +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); + +// address[] memory previousImplementations = new address[](contracts.length); +// for (uint i = 0; i < contracts.length; i++) { +// previousImplementations[i] = getTransparentUpgradeableProxyImplementationAddress(contracts[i]); +// } + +// upgradeContractBatch(contracts, newImplementations); + +// for (uint i = 0; i < contracts.length; i++) { +// runUpgradeInvariants(contracts[i], previousImplementations[i], newImplementations[i]); +// runTransferOwnership(contracts[i]); +// } + +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// } + +// function test_Upgrade_TokenStakingNodeImplementation_Scenario() public { + +// ITokenStakingNode[] memory tokenStakingNodesBefore = tokenStakingNodesManager.getAllNodes(); + +// uint256 previousTotalAssets = yneigen.totalAssets(); +// uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); + +// TokenStakingNode testStakingNodeV2 = new TokenStakingNode(); +// { +// bytes memory _data = abi.encodeWithSignature( +// "upgradeTokenStakingNode(address)", +// testStakingNodeV2 +// ); +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.schedule( +// address(tokenStakingNodesManager), // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0), // salt +// timelockController.getMinDelay() // delay +// ); +// vm.stopPrank(); + +// uint256 minDelay; +// if (block.chainid == 1) { // Mainnet +// minDelay = 3 days; +// } else if (block.chainid == 17000) { // Holesky +// minDelay = 15 minutes; +// } else { +// revert("Unsupported chain ID"); +// } +// skip(minDelay); + +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.execute( +// address(tokenStakingNodesManager), // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0) // salt +// ); +// vm.stopPrank(); +// } - UpgradeableBeacon beacon = tokenStakingNodesManager.upgradeableBeacon(); - address upgradedImplementationAddress = beacon.implementation(); - assertEq(upgradedImplementationAddress, payable(testStakingNodeV2)); - - // check tokenStakingNodesManager.getAllNodes is the same as before - ITokenStakingNode[] memory tokenStakingNodesAfter = tokenStakingNodesManager.getAllNodes(); - assertEq(tokenStakingNodesAfter.length, tokenStakingNodesBefore.length, "TokenStakingNodes length mismatch after upgrade"); - for (uint i = 0; i < tokenStakingNodesAfter.length; i++) { - assertEq(address(tokenStakingNodesAfter[i]), address(tokenStakingNodesBefore[i]), "TokenStakingNode address mismatch after upgrade"); - } - - runSystemStateInvariants(previousTotalAssets, previousTotalSupply); - runTransferOwnership(address(tokenStakingNodesManager)); - } - - function runUpgradeInvariants( - address proxyAddress, - address previousImplementation, - address newImplementation - ) internal { - // Check that the new implementation address is correctly set - address currentImplementation = getTransparentUpgradeableProxyImplementationAddress(proxyAddress); - assertEq(currentImplementation, newImplementation, "Invariant: Implementation address should match the new implementation address"); - // Ensure the implementation address has actually changed - assertNotEq(previousImplementation, newImplementation, "Invariant: New implementation should be different from the previous one"); - } - - function runSystemStateInvariants(uint256 previousTotalAssets, uint256 previousTotalSupply) public { - uint256 threshold = previousTotalAssets / 1e3; - assertTrue(compareWithThreshold(yneigen.totalAssets(), previousTotalAssets, threshold), "Total assets integrity check failed"); - assertEq(yneigen.totalSupply(), previousTotalSupply, "Share mint integrity check failed"); - } - - function upgradeContractBatch(address[] memory _proxyAddresses, address[] memory _newImplementations) public { - require(_proxyAddresses.length == _newImplementations.length, "Arrays must have the same length"); - - address[] memory targets = new address[](_proxyAddresses.length); - uint256[] memory values = new uint256[](_proxyAddresses.length); - bytes[] memory payloads = new bytes[](_proxyAddresses.length); - - for (uint i = 0; i < _proxyAddresses.length; i++) { - targets[i] = getTransparentUpgradeableProxyAdminAddress(_proxyAddresses[i]); - values[i] = 0; - payloads[i] = abi.encodeWithSignature( - "upgradeAndCall(address,address,bytes)", - _proxyAddresses[i], - _newImplementations[i], - "" - ); - } - - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.scheduleBatch( - targets, - values, - payloads, - bytes32(0), // predecessor - bytes32(0), // salt - timelockController.getMinDelay() // delay - ); - vm.stopPrank(); - - uint256 minDelay; - if (block.chainid == 1) { // Mainnet - minDelay = 3 days; - } else if (block.chainid == 17000) { // Holesky - minDelay = 15 minutes; - } else { - revert("Unsupported chain ID"); - } - skip(minDelay); - - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.executeBatch( - targets, - values, - payloads, - bytes32(0), // predecessor - bytes32(0) // salt - ); - vm.stopPrank(); - } - - function upgradeContract(address _proxyAddress, address _newImplementation) public { - bytes memory _data = abi.encodeWithSignature( - "upgradeAndCall(address,address,bytes)", - _proxyAddress, // proxy - _newImplementation, // implementation - "" // no data - ); - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.schedule( - getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target - 0, // value - _data, - bytes32(0), // predecessor - bytes32(0), // salt - timelockController.getMinDelay() // delay - ); - vm.stopPrank(); - - uint256 minDelay; - if (block.chainid == 1) { // Mainnet - minDelay = 3 days; - } else if (block.chainid == 17000) { // Holesky - minDelay = 15 minutes; - } else { - revert("Unsupported chain ID"); - } - skip(minDelay); - - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.execute( - getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target - 0, // value - _data, - bytes32(0), // predecessor - bytes32(0) // salt - ); - vm.stopPrank(); - } - - function runTransferOwnership(address _proxy) public { - address _newOwner = address(0x1241242151); - bytes memory _data = abi.encodeWithSignature("transferOwnership(address)", _newOwner); - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.schedule( - getTransparentUpgradeableProxyAdminAddress(address(_proxy)), // target - 0, // value - _data, - bytes32(0), // predecessor - bytes32(0), // salt - timelockController.getMinDelay() // delay - ); - vm.stopPrank(); - - uint256 minDelay; - if (block.chainid == 1) { // Mainnet - minDelay = 3 days; - } else if (block.chainid == 17000) { // Holesky - minDelay = 15 minutes; - } else { - revert("Unsupported chain ID"); - } - skip(minDelay); - - vm.startPrank(actors.wallets.YNSecurityCouncil); - timelockController.execute( - getTransparentUpgradeableProxyAdminAddress(address(_proxy)), // target - 0, // value - _data, - bytes32(0), // predecessor - bytes32(0) // salt - ); - vm.stopPrank(); - assertEq(Ownable(getTransparentUpgradeableProxyAdminAddress(address(_proxy))).owner(), _newOwner, "Ownership transfer failed"); - } -} \ No newline at end of file +// UpgradeableBeacon beacon = tokenStakingNodesManager.upgradeableBeacon(); +// address upgradedImplementationAddress = beacon.implementation(); +// assertEq(upgradedImplementationAddress, address(testStakingNodeV2)); + +// // check tokenStakingNodesManager.getAllNodes is the same as before +// ITokenStakingNode[] memory tokenStakingNodesAfter = tokenStakingNodesManager.getAllNodes(); +// assertEq(tokenStakingNodesAfter.length, tokenStakingNodesBefore.length, "TokenStakingNodes length mismatch after upgrade"); +// for (uint i = 0; i < tokenStakingNodesAfter.length; i++) { +// assertEq(address(tokenStakingNodesAfter[i]), address(tokenStakingNodesBefore[i]), "TokenStakingNode address mismatch after upgrade"); +// } + +// runSystemStateInvariants(previousTotalAssets, previousTotalSupply); +// runTransferOwnership(address(tokenStakingNodesManager)); +// } + +// function runUpgradeInvariants( +// address proxyAddress, +// address previousImplementation, +// address newImplementation +// ) internal { +// // Check that the new implementation address is correctly set +// address currentImplementation = getTransparentUpgradeableProxyImplementationAddress(proxyAddress); +// assertEq(currentImplementation, newImplementation, "Invariant: Implementation address should match the new implementation address"); +// // Ensure the implementation address has actually changed +// assertNotEq(previousImplementation, newImplementation, "Invariant: New implementation should be different from the previous one"); +// } + +// function runSystemStateInvariants(uint256 previousTotalAssets, uint256 previousTotalSupply) public { +// uint256 threshold = previousTotalAssets / 1e3; +// assertTrue(compareWithThreshold(yneigen.totalAssets(), previousTotalAssets, threshold), "Total assets integrity check failed"); +// assertEq(yneigen.totalSupply(), previousTotalSupply, "Share mint integrity check failed"); +// } + +// function upgradeContractBatch(address[] memory _proxyAddresses, address[] memory _newImplementations) public { +// require(_proxyAddresses.length == _newImplementations.length, "Arrays must have the same length"); + +// address[] memory targets = new address[](_proxyAddresses.length); +// uint256[] memory values = new uint256[](_proxyAddresses.length); +// bytes[] memory payloads = new bytes[](_proxyAddresses.length); + +// for (uint i = 0; i < _proxyAddresses.length; i++) { +// targets[i] = getTransparentUpgradeableProxyAdminAddress(_proxyAddresses[i]); +// values[i] = 0; +// payloads[i] = abi.encodeWithSignature( +// "upgradeAndCall(address,address,bytes)", +// _proxyAddresses[i], +// _newImplementations[i], +// "" +// ); +// } + +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.scheduleBatch( +// targets, +// values, +// payloads, +// bytes32(0), // predecessor +// bytes32(0), // salt +// timelockController.getMinDelay() // delay +// ); +// vm.stopPrank(); + +// uint256 minDelay; +// if (block.chainid == 1) { // Mainnet +// minDelay = 3 days; +// } else if (block.chainid == 17000) { // Holesky +// minDelay = 15 minutes; +// } else { +// revert("Unsupported chain ID"); +// } +// skip(minDelay); + +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.executeBatch( +// targets, +// values, +// payloads, +// bytes32(0), // predecessor +// bytes32(0) // salt +// ); +// vm.stopPrank(); +// } + +// function upgradeContract(address _proxyAddress, address _newImplementation) public { +// bytes memory _data = abi.encodeWithSignature( +// "upgradeAndCall(address,address,bytes)", +// _proxyAddress, // proxy +// _newImplementation, // implementation +// "" // no data +// ); +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.schedule( +// getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0), // salt +// timelockController.getMinDelay() // delay +// ); +// vm.stopPrank(); + +// uint256 minDelay; +// if (block.chainid == 1) { // Mainnet +// minDelay = 3 days; +// } else if (block.chainid == 17000) { // Holesky +// minDelay = 15 minutes; +// } else { +// revert("Unsupported chain ID"); +// } +// skip(minDelay); + +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.execute( +// getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0) // salt +// ); +// vm.stopPrank(); +// } + +// function runTransferOwnership(address _proxy) public { +// address _newOwner = address(0x1241242151); +// bytes memory _data = abi.encodeWithSignature("transferOwnership(address)", _newOwner); +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.schedule( +// getTransparentUpgradeableProxyAdminAddress(address(_proxy)), // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0), // salt +// timelockController.getMinDelay() // delay +// ); +// vm.stopPrank(); + +// uint256 minDelay; +// if (block.chainid == 1) { // Mainnet +// minDelay = 3 days; +// } else if (block.chainid == 17000) { // Holesky +// minDelay = 15 minutes; +// } else { +// revert("Unsupported chain ID"); +// } +// skip(minDelay); + +// vm.startPrank(actors.wallets.YNSecurityCouncil); +// timelockController.execute( +// getTransparentUpgradeableProxyAdminAddress(address(_proxy)), // target +// 0, // value +// _data, +// bytes32(0), // predecessor +// bytes32(0) // salt +// ); +// vm.stopPrank(); +// assertEq(Ownable(getTransparentUpgradeableProxyAdminAddress(address(_proxy))).owner(), _newOwner, "Ownership transfer failed"); +// } +// } \ No newline at end of file diff --git a/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol b/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol new file mode 100644 index 000000000..5aed88c9a --- /dev/null +++ b/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol @@ -0,0 +1,608 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; +import {ITokenStakingNode} from "src/interfaces/ITokenStakingNode.sol"; +import {IRedeemableAsset} from "src/interfaces/IRedeemableAsset.sol"; +import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; + +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; +import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; +import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; + +import {console} from "forge-std/console.sol"; + +import "./ynLSDeScenarioBaseTest.sol"; + +contract ynLSDeWithdrawalsTest is ynLSDeScenarioBaseTest { + + bool private _setup = true; + + address public constant user = address(0x42069); + + ITokenStakingNode public tokenStakingNode; + RedemptionAssetsVault public redemptionAssetsVault; + WithdrawalQueueManager public withdrawalQueueManager; + LSDWrapper public wrapper; + + uint256 public constant AMOUNT = 1 ether; + + function setUp() public virtual override { + + super.setUp(); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + // upgrade tokenStakingNode implementation + { + _upgradeTokenStakingNodeImplementation(); + } + + // upgrade ynLSDe + { + _upgradeContract(address(yneigen), address(new ynEigen()), ""); + } + + // upgrade EigenStrategyManager + { + _upgradeContract(address(eigenStrategyManager), address(new EigenStrategyManager()), ""); + } + + // upgrade AssetRegistry + { + _upgradeContract(address(assetRegistry), address(new AssetRegistry()), ""); + } + + // upgrade TokenStakingNodesManager + { + _upgradeContract(address(tokenStakingNodesManager), address(new TokenStakingNodesManager()), ""); + } + + // deal assets to user + { + deal({ token: chainAddresses.lsd.WSTETH_ADDRESS, to: user, give: 1000 ether }); + deal({ token: chainAddresses.lsd.WOETH_ADDRESS, to: user, give: 1000 ether }); + deal({ token: chainAddresses.lsd.SFRXETH_ADDRESS, to: user, give: 1000 ether }); + } + + // deploy RedemptionAssetsVault + { + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address(new RedemptionAssetsVault()), + actors.admin.PROXY_ADMIN_OWNER, + "" + ); + redemptionAssetsVault = RedemptionAssetsVault(payable(address(_proxy))); + } + + // deploy WithdrawalQueueManager + { + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address(new WithdrawalQueueManager()), + actors.admin.PROXY_ADMIN_OWNER, + "" + ); + withdrawalQueueManager = WithdrawalQueueManager(address(_proxy)); + } + + // deploy wrapper + { + // call `initialize` on LSDWrapper + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address( + new LSDWrapper( + chainAddresses.lsd.WSTETH_ADDRESS, + chainAddresses.lsd.WOETH_ADDRESS, + chainAddresses.lsd.OETH_ADDRESS, + chainAddresses.lsd.STETH_ADDRESS) + ), + actors.admin.PROXY_ADMIN_OWNER, + abi.encodeWithSignature("initialize()") + ); + wrapper = LSDWrapper(address(_proxy)); + } + + // initialize eigenStrategyManager + { + eigenStrategyManager.initializeV2(address(redemptionAssetsVault), address(wrapper), actors.ops.WITHDRAWAL_MANAGER); + } + + // initialize RedemptionAssetsVault + { + RedemptionAssetsVault.Init memory _init = RedemptionAssetsVault.Init({ + admin: actors.admin.PROXY_ADMIN_OWNER, + redeemer: address(withdrawalQueueManager), + ynEigen: yneigen, + assetRegistry: assetRegistry + }); + redemptionAssetsVault.initialize(_init); + } + + // initialize WithdrawalQueueManager + { + WithdrawalQueueManager.Init memory _init = WithdrawalQueueManager.Init({ + name: "ynLSDe Withdrawal Manager", + symbol: "ynLSDeWM", + redeemableAsset: IRedeemableAsset(address(yneigen)), + redemptionAssetsVault: redemptionAssetsVault, + admin: actors.admin.PROXY_ADMIN_OWNER, + withdrawalQueueAdmin: actors.ops.WITHDRAWAL_MANAGER, + redemptionAssetWithdrawer: actors.ops.REDEMPTION_ASSET_WITHDRAWER, + requestFinalizer: actors.ops.REQUEST_FINALIZER, + // withdrawalFee: 500, // 0.05% + withdrawalFee: 0, + feeReceiver: actors.admin.FEE_RECEIVER + }); + withdrawalQueueManager.initialize(_init); + } + + // unpause transfers + { + vm.prank(actors.admin.UNPAUSE_ADMIN); + yneigen.unpauseTransfers(); + } + + // grant burner role + { + vm.startPrank(actors.admin.STAKING_ADMIN); + yneigen.grantRole(yneigen.BURNER_ROLE(), address(withdrawalQueueManager)); + vm.stopPrank(); + } + + // top up redemptionAssetsVault + { + address _topper = address(0x420420); + uint256 _amount = 50; // 50 wei + deal({ token: chainAddresses.lsd.WSTETH_ADDRESS, to: _topper, give: _amount }); + deal({ token: chainAddresses.lsd.WOETH_ADDRESS, to: _topper, give: _amount }); + deal({ token: chainAddresses.lsd.SFRXETH_ADDRESS, to: _topper, give: _amount }); + vm.startPrank(_topper); + IERC20(chainAddresses.lsd.WSTETH_ADDRESS).approve(address(redemptionAssetsVault), _amount); + redemptionAssetsVault.deposit(_amount, chainAddresses.lsd.WSTETH_ADDRESS); + IERC20(chainAddresses.lsd.WOETH_ADDRESS).approve(address(redemptionAssetsVault), _amount); + redemptionAssetsVault.deposit(_amount, chainAddresses.lsd.WOETH_ADDRESS); + IERC20(chainAddresses.lsd.SFRXETH_ADDRESS).approve(address(redemptionAssetsVault), _amount); + redemptionAssetsVault.deposit(_amount, chainAddresses.lsd.SFRXETH_ADDRESS); + vm.stopPrank(); + } + + assertApproxEqRel(yneigen.totalAssets(), _totalAssetsBefore, 1e17, "setUp: E0"); // NOTE - not best practice to have it here, but for the time being... + } + + // + // queueWithdrawals + // + + function testQueueWithdrawalSTETH(uint256 _amount) public { + if (_setup) _setupTokenStakingNode(_amount); + + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.STETH_STRATEGY_ADDRESS); + uint256 _shares = _strategy.shares((address(tokenStakingNode))); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + tokenStakingNode.queueWithdrawals(_strategy, _shares); + + assertEq(yneigen.totalAssets(), _totalAssetsBefore, "testQueueWithdrawalSTETH: E0"); + assertEq(tokenStakingNode.queuedShares(_strategy), _shares, "testQueueWithdrawalSTETH: E1"); + } + + function testQueueWithdrawalSFRXETH(uint256 _amount) public { + if (_setup) _setupTokenStakingNode(_amount); + + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.SFRXETH_STRATEGY_ADDRESS); + uint256 _shares = _strategy.shares((address(tokenStakingNode))); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + tokenStakingNode.queueWithdrawals(_strategy, _shares); + + assertEq(yneigen.totalAssets(), _totalAssetsBefore, "testQueueWithdrawalSFRXETH: E0"); + assertEq(tokenStakingNode.queuedShares(_strategy), _shares, "testQueueWithdrawalSFRXETH: E1"); + } + + function testQueueWithdrawalOETH(uint256 _amount) public { + if (_setup) _setupTokenStakingNode(_amount); + + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.OETH_STRATEGY_ADDRESS); + uint256 _shares = _strategy.shares((address(tokenStakingNode))); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + tokenStakingNode.queueWithdrawals(_strategy, _shares); + + assertEq(yneigen.totalAssets(), _totalAssetsBefore, "testQueueWithdrawalOETH: E0"); + assertEq(tokenStakingNode.queuedShares(_strategy), _shares, "testQueueWithdrawalOETH: E1"); + } + + function testQueueWithdrawalsWrongCaller() public { + _setupTokenStakingNode(AMOUNT); + + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.OETH_STRATEGY_ADDRESS); + uint256 _shares = _strategy.shares((address(tokenStakingNode))); + + vm.expectRevert(abi.encodeWithSelector(TokenStakingNode.NotTokenStakingNodesWithdrawer.selector)); + tokenStakingNode.queueWithdrawals(_strategy, _shares); + } + + // + // completeQueuedWithdrawals + // + + function testCompleteQueuedWithdrawalsSTETH(uint256 _amount) public { + if (_setup) _setupTokenStakingNode(_amount); + + _setup = false; + testQueueWithdrawalSTETH(_amount); + + uint256 _nonce = delegationManager.cumulativeWithdrawalsQueued(address(tokenStakingNode)) - 1; + uint32 _startBlock = uint32(block.number); + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.STETH_STRATEGY_ADDRESS); + uint256 _shares = tokenStakingNode.queuedShares(_strategy); + uint256[] memory _middlewareTimesIndexes = new uint256[](1); + _middlewareTimesIndexes[0] = 0; + + IStrategy[] memory _strategies = new IStrategy[](1); + _strategies[0] = _strategy; + vm.roll(block.number + delegationManager.getWithdrawalDelay(_strategies)); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + tokenStakingNode.completeQueuedWithdrawals(_nonce, _startBlock, _shares, _strategy, _middlewareTimesIndexes, true); + + assertApproxEqAbs(yneigen.totalAssets(), _totalAssetsBefore, 10, "testCompleteQueuedWithdrawalsSTETH: E0"); + assertEq(tokenStakingNode.queuedShares(_strategy), 0, "testCompleteQueuedWithdrawalsSTETH: E1"); + assertApproxEqAbs(tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.WSTETH_ADDRESS)), _amount, 100, "testCompleteQueuedWithdrawalsSTETH: E2"); + + } + + function testCompleteQueuedWithdrawalsSFRXETH(uint256 _amount) public { + if (_setup) _setupTokenStakingNode(_amount); + + _setup = false; + testQueueWithdrawalSFRXETH(_amount); + + uint256 _nonce = delegationManager.cumulativeWithdrawalsQueued(address(tokenStakingNode)) - 1; + uint32 _startBlock = uint32(block.number); + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.SFRXETH_STRATEGY_ADDRESS); + uint256 _shares = tokenStakingNode.queuedShares(_strategy); + uint256[] memory _middlewareTimesIndexes = new uint256[](1); + _middlewareTimesIndexes[0] = 0; + + IStrategy[] memory _strategies = new IStrategy[](1); + _strategies[0] = _strategy; + vm.roll(block.number + delegationManager.getWithdrawalDelay(_strategies)); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + tokenStakingNode.completeQueuedWithdrawals(_nonce, _startBlock, _shares, _strategy, _middlewareTimesIndexes, true); + + assertApproxEqAbs(yneigen.totalAssets(), _totalAssetsBefore, 10, "testCompleteQueuedWithdrawalsSFRXETH: E0"); + assertEq(tokenStakingNode.queuedShares(_strategy), 0, "testCompleteQueuedWithdrawalsSFRXETH: E1"); + assertApproxEqAbs(tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS)), _amount, 100, "testCompleteQueuedWithdrawalsSFRXETH: E2"); + } + + function testCompleteQueuedWithdrawalsOETH(uint256 _amount) public { + if (_setup) _setupTokenStakingNode(_amount); + + _setup = false; + testQueueWithdrawalOETH(_amount); + + uint256 _nonce = delegationManager.cumulativeWithdrawalsQueued(address(tokenStakingNode)) - 1; + uint32 _startBlock = uint32(block.number); + IStrategy _strategy = IStrategy(chainAddresses.lsdStrategies.OETH_STRATEGY_ADDRESS); + uint256 _shares = tokenStakingNode.queuedShares(_strategy); + uint256[] memory _middlewareTimesIndexes = new uint256[](1); + _middlewareTimesIndexes[0] = 0; + + IStrategy[] memory _strategies = new IStrategy[](1); + _strategies[0] = _strategy; + vm.roll(block.number + delegationManager.getWithdrawalDelay(_strategies)); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + tokenStakingNode.completeQueuedWithdrawals(_nonce, _startBlock, _shares, _strategy, _middlewareTimesIndexes, true); + + assertApproxEqAbs(yneigen.totalAssets(), _totalAssetsBefore, 10, "testCompleteQueuedWithdrawalsOETH: E0"); + assertEq(tokenStakingNode.queuedShares(_strategy), 0, "testCompleteQueuedWithdrawalsOETH: E1"); + assertApproxEqAbs(tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.WOETH_ADDRESS)), _amount, 100, "testCompleteQueuedWithdrawalsOETH: E2"); + } + + function testCompleteAllWithdrawals(uint256 _amount) public { + _setupTokenStakingNode(_amount); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + _setup = false; + testCompleteQueuedWithdrawalsSTETH(_amount); + testCompleteQueuedWithdrawalsSFRXETH(_amount); + testCompleteQueuedWithdrawalsOETH(_amount); + + assertApproxEqAbs(yneigen.totalAssets(), _totalAssetsBefore, 10, "testCompleteAllWithdrawals: E0"); + } + + // + // processPrincipalWithdrawals + // + + function testProcessPrincipalWithdrawals(uint256 _amount) public { + testCompleteAllWithdrawals(_amount); + + uint256 _availableToWithdraw = tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.WSTETH_ADDRESS)); + IYieldNestStrategyManager.WithdrawalAction[] memory _actions = new IYieldNestStrategyManager.WithdrawalAction[](3); + _actions[0] = IYieldNestStrategyManager.WithdrawalAction({ + nodeId: tokenStakingNode.nodeId(), + amountToReinvest: _availableToWithdraw / 2, + amountToQueue: _availableToWithdraw / 2, + asset: chainAddresses.lsd.WSTETH_ADDRESS + }); + _availableToWithdraw = tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.WOETH_ADDRESS)); + _actions[1] = IYieldNestStrategyManager.WithdrawalAction({ + nodeId: tokenStakingNode.nodeId(), + amountToReinvest: _availableToWithdraw / 2, + amountToQueue: _availableToWithdraw / 2, + asset: chainAddresses.lsd.WOETH_ADDRESS + }); + _availableToWithdraw = tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS)); + _actions[2] = IYieldNestStrategyManager.WithdrawalAction({ + nodeId: tokenStakingNode.nodeId(), + amountToReinvest: _availableToWithdraw / 2, + amountToQueue: _availableToWithdraw / 2, + asset: chainAddresses.lsd.SFRXETH_ADDRESS + }); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + uint256 _ynEigenWSTETHBalanceBefore = yneigen.assets(chainAddresses.lsd.WSTETH_ADDRESS); + uint256 _ynEigenWOETHBalanceBefore = yneigen.assets(chainAddresses.lsd.WOETH_ADDRESS); + uint256 _ynEigenSFRXETHBalanceBefore = yneigen.assets(chainAddresses.lsd.SFRXETH_ADDRESS); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + eigenStrategyManager.processPrincipalWithdrawals(_actions); + + assertEq(yneigen.totalAssets(), _totalAssetsBefore, "testProcessPrincipalWithdrawals: E0"); + assertApproxEqAbs(redemptionAssetsVault.balances(chainAddresses.lsd.WSTETH_ADDRESS), _availableToWithdraw / 2, 50, "testProcessPrincipalWithdrawals: E1"); + assertApproxEqAbs(redemptionAssetsVault.balances(chainAddresses.lsd.WOETH_ADDRESS), _availableToWithdraw / 2, 50, "testProcessPrincipalWithdrawals: E2"); + assertApproxEqAbs(redemptionAssetsVault.balances(chainAddresses.lsd.SFRXETH_ADDRESS), _availableToWithdraw / 2, 50, "testProcessPrincipalWithdrawals: E3"); + assertApproxEqAbs(yneigen.assets(chainAddresses.lsd.WSTETH_ADDRESS), _ynEigenWSTETHBalanceBefore + _availableToWithdraw / 2, 2, "testProcessPrincipalWithdrawals: E4"); + assertApproxEqAbs(yneigen.assets(chainAddresses.lsd.WOETH_ADDRESS), _ynEigenWOETHBalanceBefore + _availableToWithdraw / 2, 2, "testProcessPrincipalWithdrawals: E5"); + assertEq(yneigen.assets(chainAddresses.lsd.SFRXETH_ADDRESS), _ynEigenSFRXETHBalanceBefore + _availableToWithdraw / 2, "testProcessPrincipalWithdrawals: E6"); + } + + function testProcessPrincipalWithdrawalsNoReinvest(uint256 _amount) public { + testCompleteAllWithdrawals(_amount); + + uint256 _availableToWithdraw = tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.WSTETH_ADDRESS)); + IYieldNestStrategyManager.WithdrawalAction[] memory _actions = new IYieldNestStrategyManager.WithdrawalAction[](3); + _actions[0] = IYieldNestStrategyManager.WithdrawalAction({ + nodeId: tokenStakingNode.nodeId(), + amountToReinvest: 0, + amountToQueue: _availableToWithdraw, + asset: chainAddresses.lsd.WSTETH_ADDRESS + }); + _availableToWithdraw = tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.WOETH_ADDRESS)); + _actions[1] = IYieldNestStrategyManager.WithdrawalAction({ + nodeId: tokenStakingNode.nodeId(), + amountToReinvest: 0, + amountToQueue: _availableToWithdraw, + asset: chainAddresses.lsd.WOETH_ADDRESS + }); + _availableToWithdraw = tokenStakingNode.withdrawn(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS)); + _actions[2] = IYieldNestStrategyManager.WithdrawalAction({ + nodeId: tokenStakingNode.nodeId(), + amountToReinvest: 0, + amountToQueue: _availableToWithdraw, + asset: chainAddresses.lsd.SFRXETH_ADDRESS + }); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + uint256 _ynEigenWSTETHBalanceBefore = yneigen.assets(chainAddresses.lsd.WSTETH_ADDRESS); + uint256 _ynEigenWOETHBalanceBefore = yneigen.assets(chainAddresses.lsd.WOETH_ADDRESS); + uint256 _ynEigenSFRXETHBalanceBefore = yneigen.assets(chainAddresses.lsd.SFRXETH_ADDRESS); + + vm.prank(actors.ops.WITHDRAWAL_MANAGER); + eigenStrategyManager.processPrincipalWithdrawals(_actions); + + assertEq(yneigen.totalAssets(), _totalAssetsBefore, "testProcessPrincipalWithdrawalsNoReinvest: E0"); + assertApproxEqAbs(redemptionAssetsVault.balances(chainAddresses.lsd.WSTETH_ADDRESS), _availableToWithdraw, 50, "testProcessPrincipalWithdrawalsNoReinvest: E1"); + assertApproxEqAbs(redemptionAssetsVault.balances(chainAddresses.lsd.WOETH_ADDRESS), _availableToWithdraw, 50, "testProcessPrincipalWithdrawalsNoReinvest: E2"); + assertApproxEqAbs(redemptionAssetsVault.balances(chainAddresses.lsd.SFRXETH_ADDRESS), _availableToWithdraw, 50, "testProcessPrincipalWithdrawalsNoReinvest: E3"); + assertEq(yneigen.assets(chainAddresses.lsd.WSTETH_ADDRESS), _ynEigenWSTETHBalanceBefore, "testProcessPrincipalWithdrawalsNoReinvest: E4"); + assertEq(yneigen.assets(chainAddresses.lsd.WOETH_ADDRESS), _ynEigenWOETHBalanceBefore, "testProcessPrincipalWithdrawalsNoReinvest: E5"); + assertEq(yneigen.assets(chainAddresses.lsd.SFRXETH_ADDRESS), _ynEigenSFRXETHBalanceBefore, "testProcessPrincipalWithdrawalsNoReinvest: E6"); + } + + // + // requestWithdrawal + // + + function testRequestWithdrawal(uint256 _amount) public { + testProcessPrincipalWithdrawalsNoReinvest(_amount); + + uint256 _totalAssetsBefore = yneigen.totalAssets(); + + uint256 _userYnLSDeBalance = yneigen.balanceOf(user); + vm.startPrank(user); + yneigen.approve(address(withdrawalQueueManager), _userYnLSDeBalance); + uint256 _tokenId = withdrawalQueueManager.requestWithdrawal(_userYnLSDeBalance); + vm.stopPrank(); + + assertApproxEqAbs( + withdrawalQueueManager.pendingRequestedRedemptionAmount(), + assetRegistry.convertToUnitOfAccount(IERC20(chainAddresses.lsd.WSTETH_ADDRESS), _amount) + assetRegistry.convertToUnitOfAccount(IERC20(chainAddresses.lsd.WOETH_ADDRESS), _amount) + assetRegistry.convertToUnitOfAccount(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS), _amount), + 1000, + "testRequestWithdrawal: E0" + ); + assertEq(_tokenId, 0, "testRequestWithdrawal: E1"); + assertEq(_totalAssetsBefore, yneigen.totalAssets(), "testRequestWithdrawal: E2"); + } + + // + // claimWithdrawal + // + + function testClaimWithdrawal(uint256 _amount) public { + testRequestWithdrawal(_amount); + + vm.prank(actors.ops.REQUEST_FINALIZER); + withdrawalQueueManager.finalizeRequestsUpToIndex(1); + + uint256 _userWSTETHBalanceBefore = IERC20(chainAddresses.lsd.WSTETH_ADDRESS).balanceOf(user); + uint256 _userWOETHBalanceBefore = IERC20(chainAddresses.lsd.WOETH_ADDRESS).balanceOf(user); + uint256 _userSFRXETHBalanceBefore = IERC20(chainAddresses.lsd.SFRXETH_ADDRESS).balanceOf(user); + uint256 _totalAssetsBefore = yneigen.totalAssets(); + uint256 _redemptionRateBefore = redemptionAssetsVault.redemptionRate(); + + vm.prank(user); + withdrawalQueueManager.claimWithdrawal(0, user); + + assertApproxEqRel(IERC20(chainAddresses.lsd.WSTETH_ADDRESS).balanceOf(user), _userWSTETHBalanceBefore + _amount, 1, "testClaimWithdrawal: E0"); + assertApproxEqRel(IERC20(chainAddresses.lsd.WOETH_ADDRESS).balanceOf(user), _userWOETHBalanceBefore + _amount, 1, "testClaimWithdrawal: E1"); + assertApproxEqRel(IERC20(chainAddresses.lsd.SFRXETH_ADDRESS).balanceOf(user), _userSFRXETHBalanceBefore + _amount, 1, "testClaimWithdrawal: E2"); + assertLt(yneigen.totalAssets(), _totalAssetsBefore, "testClaimWithdrawal: E3"); + assertApproxEqRel(redemptionAssetsVault.redemptionRate(), _redemptionRateBefore, 1, "testClaimWithdrawal: E4"); + } + + // + // internal helpers + // + + function _setupTokenStakingNode(uint256 _amount) private { + vm.assume(_amount > 10_000 && _amount < 100 ether); + + vm.prank(actors.ops.STAKING_NODE_CREATOR); + tokenStakingNode = tokenStakingNodesManager.createTokenStakingNode(); + + uint256 _len = 3; + IERC20[] memory _assetsToDeposit = new IERC20[](_len); + _assetsToDeposit[0] = IERC20(chainAddresses.lsd.WSTETH_ADDRESS); + _assetsToDeposit[1] = IERC20(chainAddresses.lsd.WOETH_ADDRESS); + _assetsToDeposit[2] = IERC20(chainAddresses.lsd.SFRXETH_ADDRESS); + + uint256[] memory _amounts = new uint256[](_len); + _amounts[0] = _amount; + _amounts[1] = _amount; + _amounts[2] = _amount; + + vm.startPrank(user); + for (uint256 i = 0; i < _len; i++) { + _assetsToDeposit[i].approve(address(yneigen), _amounts[i]); + yneigen.deposit(_assetsToDeposit[i], _amounts[i], user); + } + vm.stopPrank(); + + vm.startPrank(actors.ops.STRATEGY_CONTROLLER); + eigenStrategyManager.stakeAssetsToNode(tokenStakingNode.nodeId(), _assetsToDeposit, _amounts); + vm.stopPrank(); + } + + function _upgradeTokenStakingNodeImplementation() private { + + ITokenStakingNode[] memory tokenStakingNodesBefore = tokenStakingNodesManager.getAllNodes(); + + uint256 previousTotalAssets = yneigen.totalAssets(); + uint256 previousTotalSupply = IERC20(address(yneigen)).totalSupply(); + + TokenStakingNode testStakingNodeV2 = new TokenStakingNode(); + { + bytes memory _data = abi.encodeWithSignature( + "upgradeTokenStakingNode(address)", + testStakingNodeV2 + ); + vm.startPrank(actors.wallets.YNSecurityCouncil); + timelockController.schedule( + address(tokenStakingNodesManager), // target + 0, // value + _data, + bytes32(0), // predecessor + bytes32(0), // salt + timelockController.getMinDelay() // delay + ); + vm.stopPrank(); + + uint256 minDelay; + if (block.chainid == 1) { // Mainnet + minDelay = 3 days; + } else if (block.chainid == 17000) { // Holesky + minDelay = 15 minutes; + } else { + revert("Unsupported chain ID"); + } + skip(minDelay); + + vm.startPrank(actors.wallets.YNSecurityCouncil); + timelockController.execute( + address(tokenStakingNodesManager), // target + 0, // value + _data, + bytes32(0), // predecessor + bytes32(0) // salt + ); + vm.stopPrank(); + } + + + UpgradeableBeacon beacon = tokenStakingNodesManager.upgradeableBeacon(); + address upgradedImplementationAddress = beacon.implementation(); + assertEq(upgradedImplementationAddress, address(testStakingNodeV2)); + + // check tokenStakingNodesManager.getAllNodes is the same as before + ITokenStakingNode[] memory tokenStakingNodesAfter = tokenStakingNodesManager.getAllNodes(); + assertEq(tokenStakingNodesAfter.length, tokenStakingNodesBefore.length, "TokenStakingNodes length mismatch after upgrade"); + for (uint i = 0; i < tokenStakingNodesAfter.length; i++) { + assertEq(address(tokenStakingNodesAfter[i]), address(tokenStakingNodesBefore[i]), "TokenStakingNode address mismatch after upgrade"); + } + + assertApproxEqRel(yneigen.totalAssets(), previousTotalAssets, 1e17, "Total assets mismatch after upgrade"); + assertEq(yneigen.totalSupply(), previousTotalSupply, "Total supply mismatch after upgrade"); + } + + function _upgradeContract(address _proxyAddress, address _newImplementation, bytes memory data_) internal { + bytes memory _data = abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + _proxyAddress, // proxy + _newImplementation, // implementation + data_ + ); + vm.startPrank(actors.wallets.YNSecurityCouncil); + timelockController.schedule( + getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target + 0, // value + _data, + bytes32(0), // predecessor + bytes32(0), // salt + timelockController.getMinDelay() // delay + ); + vm.stopPrank(); + + uint256 minDelay; + if (block.chainid == 1) { // Mainnet + minDelay = 3 days; + } else if (block.chainid == 17000) { // Holesky + minDelay = 15 minutes; + } else { + revert("Unsupported chain ID"); + } + skip(minDelay); + + vm.startPrank(actors.wallets.YNSecurityCouncil); + timelockController.execute( + getTransparentUpgradeableProxyAdminAddress(_proxyAddress), // target + 0, // value + _data, + bytes32(0), // predecessor + bytes32(0) // salt + ); + vm.stopPrank(); + } +} \ No newline at end of file