diff --git a/scripts/forge/BaseScript.s.sol b/scripts/forge/BaseScript.s.sol index 4e2f8a192..172b7df4f 100644 --- a/scripts/forge/BaseScript.s.sol +++ b/scripts/forge/BaseScript.s.sol @@ -103,7 +103,8 @@ abstract contract BaseScript is Script, Utils { PAUSE_ADMIN: vm.envAddress("PAUSER_ADDRESS"), LSD_RESTAKING_MANAGER: vm.envAddress("LSD_RESTAKING_MANAGER_ADDRESS"), STAKING_NODE_CREATOR: vm.envAddress("LSD_STAKING_NODE_CREATOR_ADDRESS"), - ORACLE_MANAGER: vm.envAddress("YIELDNEST_ORACLE_MANAGER_ADDRESS") + ORACLE_MANAGER: vm.envAddress("YIELDNEST_ORACLE_MANAGER_ADDRESS"), + DEPOSIT_BOOTSTRAPER: vm.envAddress("DEPOSIT_BOOTSTRAPER_ADDRESS") }); } diff --git a/scripts/forge/DeployYieldNest.s.sol b/scripts/forge/DeployYieldNest.s.sol index eb8f178fe..055ad356a 100644 --- a/scripts/forge/DeployYieldNest.s.sol +++ b/scripts/forge/DeployYieldNest.s.sol @@ -44,8 +44,6 @@ contract DeployYieldNest is BaseScript { IDepositContract public depositContract; IWETH public weth; - uint startingExchangeAdjustmentRate; - bytes ZERO_PUBLIC_KEY = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; bytes ONE_PUBLIC_KEY = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"; bytes TWO_PUBLIC_KEY = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002"; @@ -72,9 +70,6 @@ contract DeployYieldNest is BaseScript { feeReceiver = payable(_broadcaster); // Casting the default signer address to payable - - startingExchangeAdjustmentRate = 4; - ContractAddresses contractAddresses = new ContractAddresses(); ContractAddresses.ChainAddresses memory chainAddresses = contractAddresses.getChainAddresses(block.chainid); eigenPodManager = IEigenPodManager(chainAddresses.eigenlayer.EIGENPOD_MANAGER_ADDRESS); @@ -120,7 +115,6 @@ contract DeployYieldNest is BaseScript { pauser: actors.PAUSE_ADMIN, stakingNodesManager: IStakingNodesManager(address(stakingNodesManager)), rewardsDistributor: IRewardsDistributor(address(rewardsDistributor)), - exchangeAdjustmentRate: startingExchangeAdjustmentRate, pauseWhitelist: pauseWhitelist }); yneth.initialize(ynethInit); diff --git a/scripts/forge/DeployYnLSD.s.sol b/scripts/forge/DeployYnLSD.s.sol index 3da5513ea..aca8f9682 100644 --- a/scripts/forge/DeployYnLSD.s.sol +++ b/scripts/forge/DeployYnLSD.s.sol @@ -38,8 +38,6 @@ contract DeployYnLSD is BaseScript { // solhint-disable-next-line no-console console.log("Current Chain ID:", block.chainid); - uint256 startingExchangeAdjustmentRate = 0; - ContractAddresses contractAddresses = new ContractAddresses(); ContractAddresses.ChainAddresses memory chainAddresses = contractAddresses.getChainAddresses(block.chainid); eigenPodManager = IEigenPodManager(chainAddresses.eigenlayer.EIGENPOD_MANAGER_ADDRESS); @@ -80,7 +78,6 @@ contract DeployYnLSD is BaseScript { strategyManager: strategyManager, delegationManager: delegationManager, oracle: yieldNestOracle, - exchangeAdjustmentRate: startingExchangeAdjustmentRate, maxNodeCount: 10, admin: actors.ADMIN, pauser: actors.PAUSE_ADMIN, diff --git a/src/interfaces/IStakingNode.sol b/src/interfaces/IStakingNode.sol index 556d519a9..604f5301a 100644 --- a/src/interfaces/IStakingNode.sol +++ b/src/interfaces/IStakingNode.sol @@ -23,7 +23,6 @@ interface IStakingEvents { event Staked(address indexed staker, uint256 ethAmount, uint256 ynETHAmount); event DepositETHPausedUpdated(bool isPaused); event Deposit(address indexed sender, address indexed receiver, uint256 assets, uint256 shares); - event ExchangeAdjustmentRateUpdated(uint256 newRate); } interface IStakingNode { diff --git a/src/ynETH.sol b/src/ynETH.sol index 9e867c41f..d41337966 100644 --- a/src/ynETH.sol +++ b/src/ynETH.sol @@ -23,7 +23,6 @@ contract ynETH is IynETH, ynBase, IStakingEvents { error Paused(); error ValueOutOfBounds(uint256 value); error ZeroAddress(); - error ExchangeAdjustmentRateOutOfBounds(uint256 exchangeAdjustmentRate); error ZeroETH(); error NoDirectETHDeposit(); error CallerNotStakingNodeManager(address expected, address provided); @@ -38,9 +37,6 @@ contract ynETH is IynETH, ynBase, IStakingEvents { IRewardsDistributor public rewardsDistributor; bool public depositsPaused; - /// @dev The value is in basis points (1/10000). - uint256 public exchangeAdjustmentRate; - uint256 public totalDepositedInPool; //-------------------------------------------------------------------------------------- @@ -53,7 +49,6 @@ contract ynETH is IynETH, ynBase, IStakingEvents { address pauser; IStakingNodesManager stakingNodesManager; IRewardsDistributor rewardsDistributor; - uint256 exchangeAdjustmentRate; address[] pauseWhitelist; } @@ -80,11 +75,6 @@ contract ynETH is IynETH, ynBase, IStakingEvents { stakingNodesManager = init.stakingNodesManager; rewardsDistributor = init.rewardsDistributor; - if (init.exchangeAdjustmentRate > BASIS_POINTS_DENOMINATOR) { - revert ExchangeAdjustmentRateOutOfBounds(init.exchangeAdjustmentRate); - } - exchangeAdjustmentRate = init.exchangeAdjustmentRate; - _setTransfersPaused(true); // transfers are initially paused _updatePauseWhitelist(init.pauseWhitelist, true); } @@ -130,16 +120,13 @@ contract ynETH is IynETH, ynBase, IStakingEvents { if (totalSupply() == 0) { return ethAmount; } - - // deltaynETH = (1 - exchangeAdjustmentRate) * (ynETHSupply / totalControlled) * ethAmount - // If `(1 - exchangeAdjustmentRate) * ethAmount * ynETHSupply < totalControlled` this will be 0. - // Can only happen in bootstrap phase if `totalControlled` and `ynETHSupply` could be manipulated - // independently. That should not be possible. + + // deltaynETH = (ynETHSupply / totalControlled) * ethAmount return Math.mulDiv( ethAmount, - totalSupply() * uint256(BASIS_POINTS_DENOMINATOR - exchangeAdjustmentRate), - totalAssets() * uint256(BASIS_POINTS_DENOMINATOR), + totalSupply(), + totalAssets(), rounding ); } @@ -220,18 +207,6 @@ contract ynETH is IynETH, ynBase, IStakingEvents { emit DepositETHPausedUpdated(depositsPaused); } - /// @notice Sets the exchange adjustment rate. - /// @dev Can only be called by the admin.. - /// Reverts if the new rate exceeds the basis points denominator. - /// @param newRate The new exchange adjustment rate to be set. - function setExchangeAdjustmentRate(uint256 newRate) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (newRate > BASIS_POINTS_DENOMINATOR) { - revert ValueOutOfBounds(newRate); - } - exchangeAdjustmentRate = newRate; - emit ExchangeAdjustmentRateUpdated(newRate); - } - //-------------------------------------------------------------------------------------- //---------------------------------- MODIFIERS --------------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynLSD.sol b/src/ynLSD.sol index a9234414b..136f9a927 100644 --- a/src/ynLSD.sol +++ b/src/ynLSD.sol @@ -7,7 +7,6 @@ import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IStrategy} from "./external/eigenlayer/v0.1.0/interfaces/IStrategy.sol"; import {IStrategyManager} from "./external/eigenlayer/v0.1.0/interfaces/IStrategyManager.sol"; import {IDelegationManager} from "./external/eigenlayer/v0.1.0/interfaces/IDelegationManager.sol"; @@ -33,7 +32,6 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { error UnsupportedAsset(IERC20 asset); error ZeroAmount(); - error ExchangeAdjustmentRateOutOfBounds(uint256 exchangeAdjustmentRate); error ZeroAddress(); error BeaconImplementationAlreadyExists(); error NoBeaconImplementationExists(); @@ -48,6 +46,8 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { bytes32 public constant LSD_RESTAKING_MANAGER_ROLE = keccak256("LSD_RESTAKING_MANAGER_ROLE"); bytes32 public constant LSD_STAKING_NODE_CREATOR_ROLE = keccak256("LSD_STAKING_NODE_CREATOR_ROLE"); + uint256 public constant BOOTSTRAP_AMOUNT_UNITS = 10; + //-------------------------------------------------------------------------------------- //---------------------------------- VARIABLES --------------------------------------- //-------------------------------------------------------------------------------------- @@ -63,8 +63,6 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { /// @notice List of supported ERC20 asset contracts. IERC20[] public assets; - - uint256 public exchangeAdjustmentRate; /** * @notice Array of LSD Staking Node contracts. @@ -88,7 +86,6 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { IStrategyManager strategyManager; IDelegationManager delegationManager; YieldNestOracle oracle; - uint256 exchangeAdjustmentRate; uint256 maxNodeCount; address admin; address pauser; @@ -96,6 +93,7 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { address lsdRestakingManager; address lsdStakingNodeCreatorRole; address[] pauseWhitelist; + address depositBootstrapper; } function initialize(Init memory init) @@ -128,15 +126,12 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { strategyManager = init.strategyManager; delegationManager = init.delegationManager; oracle = init.oracle; - - if (init.exchangeAdjustmentRate > BASIS_POINTS_DENOMINATOR) { - revert ExchangeAdjustmentRateOutOfBounds(init.exchangeAdjustmentRate); - } - exchangeAdjustmentRate = init.exchangeAdjustmentRate; maxNodeCount = init.maxNodeCount; _setTransfersPaused(true); // transfers are initially paused _updatePauseWhitelist(init.pauseWhitelist, true); + + _deposit(assets[0], BOOTSTRAP_AMOUNT_UNITS * (10 ** (IERC20Metadata(address(assets[0])).decimals())), init.depositBootstrapper, init.depositBootstrapper); } //-------------------------------------------------------------------------------------- @@ -157,7 +152,16 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { IERC20 asset, uint256 amount, address receiver - ) external nonReentrant returns (uint256 shares) { + ) public nonReentrant returns (uint256 shares) { + return _deposit(asset, amount, receiver, msg.sender); + } + + function _deposit( + IERC20 asset, + uint256 amount, + address receiver, + address sender + ) internal returns (uint256 shares) { IStrategy strategy = strategies[asset]; if(address(strategy) == address(0x0)){ @@ -178,15 +182,15 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { // Transfer assets in after shares are computed since _convertToShares relies on totalAssets // which inspects asset.balanceOf(address(this)) - asset.safeTransferFrom(msg.sender, address(this), amount); + asset.safeTransferFrom(sender, address(this), amount); - emit Deposit(msg.sender, receiver, amount, shares); + emit Deposit(sender, receiver, amount, shares); } /** * @dev Converts an ETH amount to shares based on the current exchange rate and specified rounding method. * If it's the first stake (bootstrap phase), uses a 1:1 exchange rate. Otherwise, calculates shares based on - * the formula: deltaynETH = (1 - exchangeAdjustmentRate) * (ynETHSupply / totalControlled) * ethAmount. + * the formula: deltaynETH = (ynETHSupply / totalControlled) * ethAmount. * This calculation can result in 0 during the bootstrap phase if `totalControlled` and `ynETHSupply` could be * manipulated independently, which should not be possible. * @param ethAmount The amount of ETH to convert to shares. @@ -200,15 +204,14 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { return ethAmount; } - // deltaynETH = (1 - exchangeAdjustmentRate) * (ynETHSupply / totalControlled) * ethAmount - // If `(1 - exchangeAdjustmentRate) * ethAmount * ynETHSupply < totalControlled` this will be 0. + // deltaynETH = (ynETHSupply / totalControlled) * ethAmount // Can only happen in bootstrap phase if `totalControlled` and `ynETHSupply` could be manipulated // independently. That should not be possible. return Math.mulDiv( ethAmount, - totalSupply() * uint256(BASIS_POINTS_DENOMINATOR - exchangeAdjustmentRate), - totalAssets() * uint256(BASIS_POINTS_DENOMINATOR), + totalSupply(), + totalAssets(), rounding ); } @@ -452,3 +455,4 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { _; } } + diff --git a/test/foundry/ActorAddresses.sol b/test/foundry/ActorAddresses.sol index c32c1aa5d..6f4e9c4f4 100644 --- a/test/foundry/ActorAddresses.sol +++ b/test/foundry/ActorAddresses.sol @@ -16,6 +16,7 @@ contract ActorAddresses { address LSD_RESTAKING_MANAGER; address STAKING_NODE_CREATOR; address ORACLE_MANAGER; + address DEPOSIT_BOOTSTRAPER; } mapping(uint256 => Actors) public actors; @@ -33,7 +34,8 @@ contract ActorAddresses { PAUSE_ADMIN: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f, LSD_RESTAKING_MANAGER: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720, STAKING_NODE_CREATOR: 0xBcd4042DE499D14e55001CcbB24a551F3b954096, - ORACLE_MANAGER: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 + ORACLE_MANAGER: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788, + DEPOSIT_BOOTSTRAPER: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a }); actors[5] = Actors({ @@ -48,7 +50,8 @@ contract ActorAddresses { PAUSE_ADMIN: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f, LSD_RESTAKING_MANAGER: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720, STAKING_NODE_CREATOR: 0xBcd4042DE499D14e55001CcbB24a551F3b954096, - ORACLE_MANAGER: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 + ORACLE_MANAGER: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788, + DEPOSIT_BOOTSTRAPER: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a }); } diff --git a/test/foundry/integration/IntegrationBaseTest.sol b/test/foundry/integration/IntegrationBaseTest.sol index 15d528dc9..a4171a0cd 100644 --- a/test/foundry/integration/IntegrationBaseTest.sol +++ b/test/foundry/integration/IntegrationBaseTest.sol @@ -41,8 +41,6 @@ contract IntegrationBaseTest is Test, Utils { bytes constant ZERO_SIGNATURE = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; bytes32 constant ZERO_DEPOSIT_ROOT = bytes32(0); - uint256 startingExchangeAdjustmentRate = 4; - // Utils ContractAddresses public contractAddresses; ContractAddresses.ChainAddresses public chainAddresses; @@ -165,7 +163,6 @@ contract IntegrationBaseTest is Test, Utils { pauser: actors.PAUSE_ADMIN, stakingNodesManager: IStakingNodesManager(address(stakingNodesManager)), rewardsDistributor: IRewardsDistributor(address(rewardsDistributor)), - exchangeAdjustmentRate: startingExchangeAdjustmentRate, pauseWhitelist: pauseWhitelist }); @@ -227,19 +224,20 @@ contract IntegrationBaseTest is Test, Utils { address[] memory pauseWhitelist = new address[](1); pauseWhitelist[0] = actors.TRANSFER_ENABLED_EOA; - // rETH - assets[0] = IERC20(chainAddresses.lsd.RETH_ADDRESS); - assetsAddresses[0] = chainAddresses.lsd.RETH_ADDRESS; - strategies[0] = IStrategy(chainAddresses.lsd.RETH_STRATEGY_ADDRESS); - priceFeeds[0] = chainAddresses.lsd.RETH_FEED_ADDRESS; - maxAges[0] = uint256(86400); // stETH - assets[1] = IERC20(chainAddresses.lsd.STETH_ADDRESS); - assetsAddresses[1] = chainAddresses.lsd.STETH_ADDRESS; - strategies[1] = IStrategy(chainAddresses.lsd.STETH_STRATEGY_ADDRESS); - priceFeeds[1] = chainAddresses.lsd.STETH_FEED_ADDRESS; - maxAges[1] = uint256(86400); //one hour + assets[0] = IERC20(chainAddresses.lsd.STETH_ADDRESS); + assetsAddresses[0] = chainAddresses.lsd.STETH_ADDRESS; + strategies[0] = IStrategy(chainAddresses.lsd.STETH_STRATEGY_ADDRESS); + priceFeeds[0] = chainAddresses.lsd.STETH_FEED_ADDRESS; + maxAges[0] = uint256(86400); //one hour + + // rETH + assets[1] = IERC20(chainAddresses.lsd.RETH_ADDRESS); + assetsAddresses[1] = chainAddresses.lsd.RETH_ADDRESS; + strategies[1] = IStrategy(chainAddresses.lsd.RETH_STRATEGY_ADDRESS); + priceFeeds[1] = chainAddresses.lsd.RETH_FEED_ADDRESS; + maxAges[1] = uint256(86400); YieldNestOracle.Init memory oracleInit = YieldNestOracle.Init({ assets: assetsAddresses, @@ -250,8 +248,6 @@ contract IntegrationBaseTest is Test, Utils { }); yieldNestOracle.initialize(oracleInit); - uint startingExchangeAdjustmentRateForYnLSD = 0; - LSDStakingNode lsdStakingNodeImplementation = new LSDStakingNode(); ynLSD.Init memory init = ynLSD.Init({ assets: assets, @@ -259,16 +255,24 @@ contract IntegrationBaseTest is Test, Utils { strategyManager: strategyManager, delegationManager: delegationManager, oracle: yieldNestOracle, - exchangeAdjustmentRate: startingExchangeAdjustmentRateForYnLSD, maxNodeCount: 10, admin: actors.ADMIN, stakingAdmin: actors.STAKING_ADMIN, lsdRestakingManager: actors.LSD_RESTAKING_MANAGER, lsdStakingNodeCreatorRole: actors.STAKING_NODE_CREATOR, pauseWhitelist: pauseWhitelist, - pauser: actors.PAUSE_ADMIN + pauser: actors.PAUSE_ADMIN, + depositBootstrapper: actors.DEPOSIT_BOOTSTRAPER }); + vm.deal(actors.DEPOSIT_BOOTSTRAPER, 10000 ether); + + vm.prank(actors.DEPOSIT_BOOTSTRAPER); + (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: 1000 ether}(""); + require(success, "ETH transfer failed"); + + vm.prank(actors.DEPOSIT_BOOTSTRAPER); + IERC20(chainAddresses.lsd.STETH_ADDRESS).approve(address(ynlsd), type(uint256).max); ynlsd.initialize(init); vm.prank(actors.STAKING_ADMIN); diff --git a/test/foundry/integration/Upgrades.t.sol b/test/foundry/integration/Upgrades.t.sol index 453f4f26e..d92db95db 100644 --- a/test/foundry/integration/Upgrades.t.sol +++ b/test/foundry/integration/Upgrades.t.sol @@ -99,7 +99,6 @@ contract UpgradesTest is IntegrationBaseTest { IRewardsDistributor originalRewardsDistributor = yneth.rewardsDistributor(); uint originalAllocatedETHForDeposits = yneth.totalDepositedInPool(); bool originalIsDepositETHPaused = yneth.depositsPaused(); - uint originalExchangeAdjustmentRate = yneth.exchangeAdjustmentRate(); uint originalTotalDepositedInPool = yneth.totalDepositedInPool(); MockERC20 nETH = new MockERC20("Nest ETH", "nETH"); @@ -122,7 +121,6 @@ contract UpgradesTest is IntegrationBaseTest { assertEq(address(upgradedYnETH.rewardsDistributor()), address(originalRewardsDistributor), "RewardsDistributor mismatch"); assertEq(upgradedYnETH.totalDepositedInPool(), originalAllocatedETHForDeposits, "AllocatedETHForDeposits mismatch"); assertEq(upgradedYnETH.depositsPaused(), originalIsDepositETHPaused, "IsDepositETHPaused mismatch"); - assertEq(upgradedYnETH.exchangeAdjustmentRate(), originalExchangeAdjustmentRate, "ExchangeAdjustmentRate mismatch"); assertEq(upgradedYnETH.totalDepositedInPool(), originalTotalDepositedInPool, "TotalDepositedInPool mismatch"); assertEq(finalTotalAssets, yneth.totalAssets(), "Total assets mismatch after upgrade"); @@ -134,38 +132,7 @@ contract UpgradesTest is IntegrationBaseTest { upgradedYnETH.deposit(nETHDepositAmount, address(this)); } - function testUpgradeYnETHRevertswithInvalidAdjustmentRate() public { - - TransparentUpgradeableProxy ynethProxy; - yneth = ynETH(payable(ynethProxy)); - - // Re-deploying ynETH and creating its proxy again - yneth = new ynETH(); - ynethProxy = new TransparentUpgradeableProxy(address(yneth), actors.PROXY_ADMIN_OWNER, ""); - yneth = ynETH(payable(ynethProxy)); - - - address[] memory pauseWhitelist = new address[](1); - pauseWhitelist[0] = actors.TRANSFER_ENABLED_EOA; - - uint256 invalidRate = 100000000000000000000; - - ynETH.Init memory ynethInit = ynETH.Init({ - admin: actors.ADMIN, - pauser: actors.PAUSE_ADMIN, - stakingNodesManager: IStakingNodesManager(address(stakingNodesManager)), - rewardsDistributor: IRewardsDistributor(address(rewardsDistributor)), - exchangeAdjustmentRate: invalidRate, - pauseWhitelist: pauseWhitelist - }); - - bytes memory encodedError = abi.encodeWithSelector(ynETH.ExchangeAdjustmentRateOutOfBounds.selector, invalidRate); - - vm.expectRevert(encodedError); - yneth.initialize(ynethInit); - } - - function setupInitializeYnLSD(uint256 adjustmentRate, address assetAddress) internal returns (ynLSD.Init memory, ynLSD ynlsd) { + function setupInitializeYnLSD(address assetAddress) internal returns (ynLSD.Init memory, ynLSD ynlsd) { TransparentUpgradeableProxy ynlsdProxy; ynlsd = ynLSD(payable(ynlsdProxy)); @@ -203,30 +170,23 @@ contract UpgradesTest is IntegrationBaseTest { strategyManager: strategyManager, delegationManager: delegationManager, oracle: yieldNestOracle, - exchangeAdjustmentRate: adjustmentRate, maxNodeCount: 10, admin: actors.ADMIN, stakingAdmin: actors.STAKING_ADMIN, lsdRestakingManager: actors.LSD_RESTAKING_MANAGER, lsdStakingNodeCreatorRole: actors.STAKING_NODE_CREATOR, pauseWhitelist: pauseWhitelist, - pauser: actors.PAUSE_ADMIN + pauser: actors.PAUSE_ADMIN, + depositBootstrapper: actors.DEPOSIT_BOOTSTRAPER }); return (init, ynlsd); } function testYnLSDInitializeRevertsAssetAddressZero() public { - (ynLSD.Init memory init, ynLSD ynlsd) = setupInitializeYnLSD(1000, address(0)); + (ynLSD.Init memory init, ynLSD ynlsd) = setupInitializeYnLSD(address(0)); bytes memory encodedError = abi.encodeWithSelector(ynLSD.ZeroAddress.selector); vm.expectRevert(encodedError); ynlsd.initialize(init); } - - function testYnLSDInitializeRevertsInvalidAdjustmentRate() public { - (ynLSD.Init memory init, ynLSD ynlsd) = setupInitializeYnLSD(1 ether, chainAddresses.lsd.STETH_ADDRESS); - bytes memory encodedError = abi.encodeWithSelector(ynLSD.ExchangeAdjustmentRateOutOfBounds.selector, 1 ether); - vm.expectRevert(encodedError); - ynlsd.initialize(init); - } } diff --git a/test/foundry/integration/ynETH.t.sol b/test/foundry/integration/ynETH.t.sol index ece4fdcec..837b54b3f 100644 --- a/test/foundry/integration/ynETH.t.sol +++ b/test/foundry/integration/ynETH.t.sol @@ -108,7 +108,7 @@ contract ynETHIntegrationTest is IntegrationBaseTest { // Act uint256 sharesAfterFirstDeposit = yneth.previewDeposit(secondDepositAmount); - uint256 expectedShares = Math.mulDiv(secondDepositAmount, 10000 - startingExchangeAdjustmentRate, 10000, Math.Rounding.Floor); + uint256 expectedShares = secondDepositAmount; // Assert assertEq(sharesAfterFirstDeposit, expectedShares, "Fuzz: Shares should match expected shares"); @@ -136,7 +136,7 @@ contract ynETHIntegrationTest is IntegrationBaseTest { assertEq(totalAssetsAfterSecondDeposit, expectedTotalAssets, "Total assets should match expected total after second deposit"); // Assuming initial total supply equals shares after first deposit - uint256 expectedTotalSupply = firstDepositAmount + secondDepositAmount - startingExchangeAdjustmentRate * secondDepositAmount / 10000; + uint256 expectedTotalSupply = firstDepositAmount + secondDepositAmount; uint256 totalSupplyAfterSecondDeposit = yneth.totalSupply(); // TODO: figure out this precision issue assertTrue(compareWithThreshold(totalSupplyAfterSecondDeposit, expectedTotalSupply, 1), "Total supply should match expected total supply after second deposit"); @@ -146,11 +146,10 @@ contract ynETHIntegrationTest is IntegrationBaseTest { uint256 sharesAfterSecondDeposit = yneth.previewDeposit(thirdDepositAmount); // Using the formula from ynETH to calculate expectedShares - // Assuming exchangeAdjustmentRate is applied as in the _convertToShares function of ynETH uint256 expectedShares = Math.mulDiv( thirdDepositAmount, - expectedTotalSupply * uint256(10000 - startingExchangeAdjustmentRate), - expectedTotalAssets * uint256(10000), + expectedTotalSupply, + expectedTotalAssets, Math.Rounding.Floor ); @@ -178,11 +177,10 @@ contract ynETHIntegrationTest is IntegrationBaseTest { uint256 expectedTotalAssets = ethAmount + expectedNetRewardAmount; // Assuming initial total assets were equal to ethAmount before rewards uint256 expectedTotalSupply = ethAmount; // Assuming initial total supply equals shares after first deposit // Using the formula from ynETH to calculate expectedShares - // Assuming exchangeAdjustmentRate is applied as in the _convertToShares function of ynETH uint256 expectedShares = Math.mulDiv( ethAmount, - expectedTotalSupply * uint256(10000 - startingExchangeAdjustmentRate), - expectedTotalAssets * uint256(10000), + expectedTotalSupply, + expectedTotalAssets, Math.Rounding.Floor ); @@ -358,21 +356,5 @@ contract ynETHIntegrationTest is IntegrationBaseTest { vm.expectRevert(encodedError); yneth.withdrawETH(1); vm.stopPrank(); - } - - function testSetExchangeAdjustmentRate() public { - uint256 newRate = 1000; - vm.prank(address(actors.ADMIN)); - yneth.setExchangeAdjustmentRate(newRate); - assertEq(yneth.exchangeAdjustmentRate(), newRate); - } - - function testSetExchangeAdjustmentRateWithInvalidRate() public { - uint256 invalidRate = 100000000000000000000; - bytes memory encodedError = abi.encodeWithSelector(ynETH.ValueOutOfBounds.selector, invalidRate); - vm.prank(address(actors.ADMIN)); - vm.expectRevert(encodedError); - yneth.setExchangeAdjustmentRate(invalidRate); - vm.stopPrank(); - } + } } diff --git a/test/foundry/integration/ynLSD.t.sol b/test/foundry/integration/ynLSD.t.sol index fcf8aa355..fb07f7bbc 100644 --- a/test/foundry/integration/ynLSD.t.sol +++ b/test/foundry/integration/ynLSD.t.sol @@ -15,7 +15,6 @@ import {TestLSDStakingNodeV2} from "test/foundry/mocks/TestLSDStakingNodeV2.sol" import {TestYnLSDV2} from "test/foundry/mocks/TestYnLSDV2.sol"; import {ynBase} from "src/ynBase.sol"; - contract ynLSDAssetTest is IntegrationBaseTest { function testDepositSTETHFailingWhenStrategyIsPaused() public { IERC20 asset = IERC20(chainAddresses.lsd.STETH_ADDRESS); @@ -38,7 +37,7 @@ contract ynLSDAssetTest is IntegrationBaseTest { assets[0] = asset; amounts[0] = amount; - vm.expectRevert(bytes("BALANCE_EXCEEDED")); + vm.expectRevert(bytes("Pausable: index is paused")); vm.prank(actors.LSD_RESTAKING_MANAGER); lsdStakingNode.depositAssetsToEigenlayer(assets, amounts); } @@ -47,6 +46,9 @@ contract ynLSDAssetTest is IntegrationBaseTest { IERC20 stETH = IERC20(chainAddresses.lsd.STETH_ADDRESS); uint256 amount = 32 ether; + uint256 initialSupply = ynlsd.totalSupply(); + uint256 initialTotalAssets = ynlsd.totalAssets(); + // Obtain STETH (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: amount + 1}(""); require(success, "ETH transfer failed"); @@ -58,8 +60,8 @@ contract ynLSDAssetTest is IntegrationBaseTest { stETH.approve(address(ynlsd), 32 ether); ynlsd.deposit(stETH, depositAmount, address(this)); - assertEq(ynlsd.balanceOf(address(this)), ynlsd.totalSupply(), "ynlsd balance does not match total supply"); - assertTrue((depositAmount - ynlsd.totalAssets()) < 1e18, "Total assets do not match user deposits"); + assertEq(ynlsd.balanceOf(address(this)), ynlsd.totalSupply() - initialSupply, "ynlsd balance does not match total supply"); + assertTrue((depositAmount - (ynlsd.totalAssets() - initialTotalAssets)) < 1e18, "Total assets do not match user deposits"); assertTrue((depositAmount - ynlsd.balanceOf(address(this))) < 1e18, "Invalid ynLSD Balance"); } @@ -67,6 +69,9 @@ contract ynLSDAssetTest is IntegrationBaseTest { IERC20 stETH = IERC20(chainAddresses.lsd.STETH_ADDRESS); uint256 amount = 32 ether; + uint256 initialSupply = ynlsd.totalSupply(); + uint256 initialTotalAssets = ynlsd.totalAssets(); + // Obtain STETH (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: amount + 1}(""); require(success, "ETH transfer failed"); @@ -84,8 +89,8 @@ contract ynLSDAssetTest is IntegrationBaseTest { uint256 totalDeposit = depositAmountOne + depositAmountTwo + depositAmountThree; - assertEq(ynlsd.balanceOf(address(this)), ynlsd.totalSupply(), "ynlsd balance does not match total supply"); - assertTrue((totalDeposit - ynlsd.totalAssets()) < 1e18, "Total assets do not match user deposits"); + assertEq(ynlsd.balanceOf(address(this)), ynlsd.totalSupply() - initialSupply, "ynlsd balance does not match total supply"); + assertTrue((totalDeposit - (ynlsd.totalAssets() - initialTotalAssets)) < 1e18, "Total assets do not match user deposits"); assertTrue(totalDeposit - ynlsd.balanceOf(address(this)) < 1e18, "Invalid ynLSD Balance"); } @@ -114,17 +119,29 @@ contract ynLSDAssetTest is IntegrationBaseTest { ynlsd.convertToShares(asset, amount); } + function testConvertToSharesBootstrapStrategy() public { + vm.prank(actors.STAKING_NODE_CREATOR); + ynlsd.createLSDStakingNode(); + uint256[] memory totalAssets = ynlsd.getTotalAssets(); + ynlsd.nodes(0); + + uint256 bootstrapAmountUnits = ynlsd.BOOTSTRAP_AMOUNT_UNITS() * 1e18 - 1; + assertTrue(compareWithThreshold(totalAssets[0], bootstrapAmountUnits, 1), "Total assets should be equal to bootstrap amount"); + } + function testConvertToSharesZeroStrategy() public { vm.prank(actors.STAKING_NODE_CREATOR); ynlsd.createLSDStakingNode(); uint256[] memory totalAssets = ynlsd.getTotalAssets(); ynlsd.nodes(0); - assertEq(totalAssets[0], 0, "Total assets should be zero"); + + assertEq(totalAssets[1], 0, "Total assets should be equal to bootstrap 0"); } function testGetTotalAssets() public { + uint256 totalAssetsInETH = ynlsd.convertToETH(ynlsd.assets(0), ynlsd.BOOTSTRAP_AMOUNT_UNITS() * 1e18 - 1); uint256 totalAssets = ynlsd.totalAssets(); - assertEq(totalAssets, 0, "Total assets should be zero"); + assertTrue(compareWithThreshold(totalAssets, totalAssetsInETH, 1), "Total assets should be equal to bootstrap amount converted to its ETH value"); } function testLSDWrongStrategy() public { @@ -142,8 +159,8 @@ contract ynLSDAssetTest is IntegrationBaseTest { uint256 shares = ynlsd.convertToShares(asset, amount); (, int256 price, , uint256 timeStamp, ) = assetPriceFeed.latestRoundData(); - assertEq(ynlsd.totalAssets(), 0); - assertEq(ynlsd.totalSupply(), 0); + // assertEq(ynlsd.totalAssets(), 0); + // assertEq(ynlsd.totalSupply(), 0); assertEq(timeStamp > 0, true, "Zero timestamp"); assertEq(price > 0, true, "Zero price"); @@ -164,6 +181,8 @@ contract ynLSDAssetTest is IntegrationBaseTest { vm.startPrank(unpauser); pausableStrategyManager.unpause(0); vm.stopPrank(); + + uint256 totalAssetsBeforeDeposit = ynlsd.totalAssets(); // Obtain STETH (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: amount + 1}(""); @@ -173,6 +192,7 @@ contract ynLSDAssetTest is IntegrationBaseTest { asset.approve(address(ynlsd), amount); ynlsd.deposit(asset, amount, address(this)); + { IERC20[] memory assets = new IERC20[](1); uint256[] memory amounts = new uint256[](1); @@ -194,7 +214,7 @@ contract ynLSDAssetTest is IntegrationBaseTest { uint256 expectedBalance = balanceInStrategyForNode * oraclePrice / 1e18; // Assert that totalAssets reflects the deposit - assertEq(totalAssetsAfterDeposit, expectedBalance, "Total assets do not reflect the deposit"); + assertEq(totalAssetsAfterDeposit - totalAssetsBeforeDeposit, expectedBalance, "Total assets do not reflect the deposit"); } function testPreviewDeposit() public { @@ -527,4 +547,54 @@ contract ynLSDTransferPauseTest is IntegrationBaseTest { assertFalse(isFirstAddressWhitelisted, "First new whitelist address was not removed"); assertFalse(isSecondAddressWhitelisted, "Second new whitelist address was not removed"); } +} + + +contract ynLSDDonationsTest is IntegrationBaseTest { + + function testYnLSDdonationToZeroShareAttackResistance() public { + + uint INITIAL_AMOUNT = 10_000 ether; + + address _alice = makeAddr("Alice"); + address _bob = makeAddr("Bob"); + + IERC20 assetToken = IERC20(chainAddresses.lsd.STETH_ADDRESS); + + vm.deal(_alice, INITIAL_AMOUNT); + vm.deal(_bob, INITIAL_AMOUNT); + + + IERC20 steth = IERC20(chainAddresses.lsd.STETH_ADDRESS); + + // get stETH + vm.startPrank(_alice); + (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: INITIAL_AMOUNT}(""); + require(success, "ETH transfer failed"); + + steth.approve(address(ynlsd), type(uint256).max); + + vm.startPrank(_bob); + (success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: INITIAL_AMOUNT}(""); + require(success, "ETH transfer failed"); + + steth.approve(address(ynlsd), type(uint256).max); + + // Front-running part + uint256 bobDepositAmount = INITIAL_AMOUNT / 2; + // Alice knows that Bob is about to deposit INITIAL_AMOUNT*0.5 ATK to the Vault by observing the mempool + vm.startPrank(_alice); + uint256 aliceDepositAmount = 1; + uint256 aliceShares = ynlsd.deposit(assetToken, aliceDepositAmount, _alice); + assertEq(aliceShares, 0); // Since there are boostrap funds, this has no effect + // Try to inflate shares value + assetToken.transfer(address(ynlsd), bobDepositAmount); + vm.stopPrank(); + + // Check that Bob did not get 0 share when he deposits + vm.prank(_bob); + uint256 bobShares = ynlsd.deposit(assetToken, bobDepositAmount, _bob); + + assertGt(bobShares, 1 wei, "Bob's shares should be greater than 1 wei"); + } } \ No newline at end of file diff --git a/test/foundry/mocks/MockYnETHERC4626.sol b/test/foundry/mocks/MockYnETHERC4626.sol index adc7aed1c..ffb76d68b 100644 --- a/test/foundry/mocks/MockYnETHERC4626.sol +++ b/test/foundry/mocks/MockYnETHERC4626.sol @@ -22,7 +22,6 @@ contract MockYnETHERC4626 is IynETH, AccessControlUpgradeable, ERC4626Upgradeabl /// @param ynETHAmount The amount of ynETH received. event Staked(address indexed staker, uint256 ethAmount, uint256 ynETHAmount); event DepositETHPausedUpdated(bool isPaused); - event ExchangeAdjustmentRateUpdated(uint256 newRate); uint16 internal constant _BASIS_POINTS_DENOMINATOR = 10_000; bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); @@ -36,10 +35,6 @@ contract MockYnETHERC4626 is IynETH, AccessControlUpgradeable, ERC4626Upgradeabl bool public depositsPaused; // Storage variables - - /// @dev The value is in basis points (1/10000). - uint256 public exchangeAdjustmentRate; - uint256 public totalDepositedInPool; struct ReInit { @@ -84,16 +79,10 @@ contract MockYnETHERC4626 is IynETH, AccessControlUpgradeable, ERC4626Upgradeabl if (totalSupply() == 0) { return ethAmount; } - - // deltaynETH = (1 - exchangeAdjustmentRate) * (ynETHSupply / totalControlled) * ethAmount - // If `(1 - exchangeAdjustmentRate) * ethAmount * ynETHSupply < totalControlled` this will be 0. - - // Can only happen in bootstrap phase if `totalControlled` and `ynETHSupply` could be manipulated - // independently. That should not be possible. return Math.mulDiv( ethAmount, - totalSupply() * uint256(_BASIS_POINTS_DENOMINATOR - exchangeAdjustmentRate), - totalAssets() * uint256(_BASIS_POINTS_DENOMINATOR), + totalSupply(), + totalAssets(), rounding ); } @@ -152,13 +141,6 @@ contract MockYnETHERC4626 is IynETH, AccessControlUpgradeable, ERC4626Upgradeabl emit DepositETHPausedUpdated(depositsPaused); } - function setExchangeAdjustmentRate(uint256 newRate) external onlyStakingNodesManager { - if (newRate > _BASIS_POINTS_DENOMINATOR) { - revert ValueOutOfBounds(newRate); - } - exchangeAdjustmentRate = newRate; - emit ExchangeAdjustmentRateUpdated(newRate); - } //-------------------------------------------------------------------------------------- //---------------------------------- MODIFIERS --------------------------------------- //--------------------------------------------------------------------------------------