Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/inflation bug ynlsd #43

Merged
merged 2 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion scripts/forge/BaseScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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")
});
}

Expand Down
6 changes: 0 additions & 6 deletions scripts/forge/DeployYieldNest.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 0 additions & 3 deletions scripts/forge/DeployYnLSD.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/IStakingNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 4 additions & 29 deletions src/ynETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;

//--------------------------------------------------------------------------------------
Expand All @@ -53,7 +49,6 @@ contract ynETH is IynETH, ynBase, IStakingEvents {
address pauser;
IStakingNodesManager stakingNodesManager;
IRewardsDistributor rewardsDistributor;
uint256 exchangeAdjustmentRate;
address[] pauseWhitelist;
}

Expand All @@ -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);
}
Expand Down Expand Up @@ -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
);
}
Expand Down Expand Up @@ -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 ---------------------------------------
//--------------------------------------------------------------------------------------
Expand Down
40 changes: 22 additions & 18 deletions src/ynLSD.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -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 ---------------------------------------
//--------------------------------------------------------------------------------------
Expand All @@ -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.
Expand All @@ -88,14 +86,14 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents {
IStrategyManager strategyManager;
IDelegationManager delegationManager;
YieldNestOracle oracle;
uint256 exchangeAdjustmentRate;
uint256 maxNodeCount;
address admin;
address pauser;
address stakingAdmin;
address lsdRestakingManager;
address lsdStakingNodeCreatorRole;
address[] pauseWhitelist;
address depositBootstrapper;
}

function initialize(Init memory init)
Expand Down Expand Up @@ -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);
}

//--------------------------------------------------------------------------------------
Expand All @@ -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)){
Expand All @@ -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.
Expand All @@ -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
);
}
Expand Down Expand Up @@ -452,3 +455,4 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents {
_;
}
}

7 changes: 5 additions & 2 deletions test/foundry/ActorAddresses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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({
Expand All @@ -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
});
}

Expand Down
40 changes: 22 additions & 18 deletions test/foundry/integration/IntegrationBaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
});

Expand Down Expand Up @@ -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,
Expand All @@ -250,25 +248,31 @@ contract IntegrationBaseTest is Test, Utils {
});
yieldNestOracle.initialize(oracleInit);

uint startingExchangeAdjustmentRateForYnLSD = 0;

LSDStakingNode lsdStakingNodeImplementation = new LSDStakingNode();
ynLSD.Init memory init = ynLSD.Init({
assets: assets,
strategies: strategies,
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);
Expand Down
Loading
Loading