Skip to content

Commit

Permalink
Merge pull request #43 from yieldnest/fix/inflation-bug-ynlsd
Browse files Browse the repository at this point in the history
Fix/inflation bug ynlsd
  • Loading branch information
danoctavian authored Mar 23, 2024
2 parents b85a72b + 2c1d767 commit 55a265e
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 178 deletions.
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 @@ -79,11 +74,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 @@ -129,16 +119,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 @@ -219,18 +206,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 @@ -178,7 +176,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 @@ -239,19 +236,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 @@ -262,25 +260,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

0 comments on commit 55a265e

Please sign in to comment.