Skip to content

Commit

Permalink
Merge pull request #529 from lidofinance/feature/shapella-upgrade-san…
Browse files Browse the repository at this point in the history
…ity-checks-registry

Oracle report sanity checks
  • Loading branch information
TheDZhon authored Feb 5, 2023
2 parents 6384981 + 63c1172 commit 4089bd1
Show file tree
Hide file tree
Showing 26 changed files with 907 additions and 134 deletions.
69 changes: 3 additions & 66 deletions contracts/0.4.24/Lido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import "@aragon/os/contracts/apps/AragonApp.sol";
import "@aragon/os/contracts/lib/math/SafeMath.sol";

import "./lib/StakeLimitUtils.sol";
import "./lib/PositiveTokenRebaseLimiter.sol";

import "./StETHPermit.sol";

Expand Down Expand Up @@ -64,7 +63,6 @@ contract Lido is StETHPermit, AragonApp {
using UnstructuredStorage for bytes32;
using StakeLimitUnstructuredStorage for bytes32;
using StakeLimitUtils for StakeLimitState.Data;
using PositiveTokenRebaseLimiter for LimiterState.Data;

/// ACL
bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE");
Expand All @@ -73,7 +71,6 @@ contract Lido is StETHPermit, AragonApp {
bytes32 public constant STAKING_CONTROL_ROLE = keccak256("STAKING_CONTROL_ROLE");
bytes32 public constant MANAGE_PROTOCOL_CONTRACTS_ROLE = keccak256("MANAGE_PROTOCOL_CONTRACTS_ROLE");
bytes32 public constant BURN_ROLE = keccak256("BURN_ROLE");
bytes32 public constant MANAGE_MAX_POSITIVE_TOKEN_REBASE_ROLE = keccak256("MANAGE_MAX_POSITIVE_TOKEN_REBASE_ROLE");

uint256 private constant DEPOSIT_SIZE = 32 ether;
uint256 public constant TOTAL_BASIS_POINTS = 10000;
Expand All @@ -97,9 +94,6 @@ contract Lido is StETHPermit, AragonApp {
/// @dev number of Lido's validators available in the Consensus Layer state
// "beacon" in the `keccak256()` parameter is staying here for compatibility reason
bytes32 internal constant CL_VALIDATORS_POSITION = keccak256("lido.Lido.beaconValidators");
/// @dev positive token rebase allowed per single LidoOracle report
/// uses 1e9 precision, e.g.: 1e6 - 0.1%; 1e9 - 100%, see `setMaxPositiveTokenRebase()`
bytes32 internal constant MAX_POSITIVE_TOKEN_REBASE_POSITION = keccak256("lido.Lido.MaxPositiveTokenRebase");
/// @dev Just a counter of total amount of execution layer rewards received by Lido contract. Not used in the logic.
bytes32 internal constant TOTAL_EL_REWARDS_COLLECTED_POSITION = keccak256("lido.Lido.totalELRewardsCollected");
/// @dev version of contract
Expand All @@ -124,9 +118,6 @@ contract Lido is StETHPermit, AragonApp {
// The amount of ETH withdrawn from LidoExecutionLayerRewardsVault contract to Lido contract
event ELRewardsReceived(uint256 amount);

// Max positive token rebase set (see `setMaxPositiveTokenRebase()`)
event MaxPositiveTokenRebaseSet(uint256 maxPositiveTokenRebase);

// Records a deposit made by a user
event Submitted(address indexed sender, uint256 amount, address referral);

Expand Down Expand Up @@ -460,36 +451,6 @@ contract Lido is StETHPermit, AragonApp {
_setProtocolContracts(_oracle, _treasury, _executionLayerRewardsVault);
}

/**
* @dev Set max positive rebase allowed per single oracle report
* token rebase happens on total supply adjustment,
* huge positive rebase can incur oracle report sandwitching.
*
* stETH balance for the `account` defined as:
* balanceOf(account) = shares[account] * totalPooledEther / totalShares = shares[account] * shareRate
*
* Suppose shareRate changes when oracle reports (see `handleOracleReport`)
* which means that token rebase happens:
*
* preShareRate = preTotalPooledEther() / preTotalShares()
* postShareRate = postTotalPooledEther() / postTotalShares()
* R = (postShareRate - preShareRate) / preShareRate
*
* R > 0 corresponds to the relative positive rebase value (i.e., instant APR)
*
* NB: The value is not set by default (explicit initialization required),
* the recommended sane values are from 0.05% to 0.1%.
*
* @param _maxTokenPositiveRebase max positive token rebase value with 1e9 precision:
* e.g.: 1e6 - 0.1%; 1e9 - 100%
* - passing zero value is prohibited
* - to allow unlimited rebases, pass max uint256, i.e.: type(uint256).max
*/
function setMaxPositiveTokenRebase(uint256 _maxTokenPositiveRebase) external {
_auth(MANAGE_MAX_POSITIVE_TOKEN_REBASE_ROLE);
_setMaxPositiveTokenRebase(_maxTokenPositiveRebase);
}

/**
* @notice Updates accounting stats, collects EL rewards and distributes collected rewards if beacon balance increased
* @dev periodically called by the Oracle contract
Expand Down Expand Up @@ -522,12 +483,6 @@ contract Lido is StETHPermit, AragonApp {
require(msg.sender == getOracle(), "APP_AUTH_FAILED");
_whenNotStopped();

LimiterState.Data memory tokenRebaseLimiter = PositiveTokenRebaseLimiter.initLimiterState(
getMaxPositiveTokenRebase(),
_getTotalPooledEther(),
_getTotalShares()
);

uint256 preClBalance = CL_BALANCE_POSITION.getStorageUint256();

// update saved CL stats checking its sanity
Expand All @@ -539,9 +494,9 @@ contract Lido is StETHPermit, AragonApp {
uint256 rewardsBase = appearedValidators.mul(DEPOSIT_SIZE).add(preClBalance);
int256 clBalanceDiff = _signedSub(int256(_clBalance), int256(rewardsBase));

tokenRebaseLimiter.applyCLBalanceUpdate(clBalanceDiff);
withdrawals = tokenRebaseLimiter.appendEther(_withdrawalVaultBalance);
elRewards = tokenRebaseLimiter.appendEther(_elRewardsVaultBalance);
// TODO: temporary disable limit
withdrawals = _withdrawalVaultBalance;
elRewards = _elRewardsVaultBalance;

// collect ETH from EL and Withdrawal vaults and send some to WithdrawalQueue if required
_processETHDistribution(withdrawals, elRewards, _requestIdToFinalizeUpTo, _finalizationShareRate);
Expand Down Expand Up @@ -610,14 +565,6 @@ contract Lido is StETHPermit, AragonApp {
return TOTAL_EL_REWARDS_COLLECTED_POSITION.getStorageUint256();
}

/**
* @notice Get max positive token rebase value
* @return max positive token rebase value, nominated id MAX_POSITIVE_REBASE_PRECISION_POINTS (10**9 == 100% = 10000 BP)
*/
function getMaxPositiveTokenRebase() public view returns (uint256) {
return MAX_POSITIVE_TOKEN_REBASE_POSITION.getStorageUint256();
}

/**
* @notice Gets authorized oracle address
* @return address of oracle contract
Expand Down Expand Up @@ -1016,16 +963,6 @@ contract Lido is StETHPermit, AragonApp {
return _stakeLimitData.calculateCurrentStakeLimit();
}

/**
* @dev Set max positive token rebase value
* @param _maxPositiveTokenRebase max positive token rebase, nominated in MAX_POSITIVE_REBASE_PRECISION_POINTS
*/
function _setMaxPositiveTokenRebase(uint256 _maxPositiveTokenRebase) internal {
MAX_POSITIVE_TOKEN_REBASE_POSITION.setStorageUint256(_maxPositiveTokenRebase);

emit MaxPositiveTokenRebaseSet(_maxPositiveTokenRebase);
}

/**
* @dev Size-efficient analog of the `auth(_role)` modifier
* @param _role Permission name
Expand Down
11 changes: 2 additions & 9 deletions contracts/0.4.24/template/LidoTemplate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,7 @@ contract LidoTemplate is IsContract {

function finalizeDAO(
string _daoName,
uint256 _unvestedTokensAmount,
uint256 _maxPositiveTokenRebase
uint256 _unvestedTokensAmount
)
external onlyOwner
{
Expand All @@ -381,11 +380,6 @@ contract LidoTemplate is IsContract {
require(state.dao != address(0), ERROR_DAO_NOT_DEPLOYED);
require(bytes(_daoName).length > 0, ERROR_INVALID_ID);

bytes32 LIDO_MANAGE_MAX_POSITIVE_TOKEN_REBASE = state.lido.MANAGE_MAX_POSITIVE_TOKEN_REBASE_ROLE();
_createPermissionForTemplate(state.acl, state.lido, LIDO_MANAGE_MAX_POSITIVE_TOKEN_REBASE);
state.lido.setMaxPositiveTokenRebase(_maxPositiveTokenRebase);
_removePermissionFromTemplate(state.acl, state.lido, LIDO_MANAGE_MAX_POSITIVE_TOKEN_REBASE);

if (_unvestedTokensAmount != 0) {
// using issue + assign to avoid setting the additional MINT_ROLE for the template
state.tokenManager.issue(_unvestedTokensAmount);
Expand Down Expand Up @@ -606,8 +600,7 @@ contract LidoTemplate is IsContract {
perms[3] = _state.lido.RESUME_ROLE();
perms[4] = _state.lido.STAKING_PAUSE_ROLE();
perms[5] = _state.lido.STAKING_CONTROL_ROLE();
perms[6] = _state.lido.MANAGE_MAX_POSITIVE_TOKEN_REBASE_ROLE();
for (i = 0; i < 7; ++i) {
for (i = 0; i < 6; ++i) {
_createPermissionForVoting(acl, _state.lido, perms[i], voting);
}
}
Expand Down
1 change: 0 additions & 1 deletion contracts/0.4.24/test_helpers/LidoMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ contract LidoMock is Lido {
function resumeProtocolAndStaking() public {
_resume();
_resumeStaking();
_setMaxPositiveTokenRebase(UNLIMITED_TOKEN_REBASE);
}

/**
Expand Down
1 change: 0 additions & 1 deletion contracts/0.4.24/test_helpers/LidoPushableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ contract LidoPushableMock is Lido {
function initialize(address _oracle) public onlyInit {
_setProtocolContracts(_oracle, _oracle, address(0));
_resume();
_setMaxPositiveTokenRebase(UNLIMITED_TOKEN_REBASE);
initialized();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
// SPDX-License-Identifier: GPL-3.0

/* See contracts/COMPILERS.md */
pragma solidity 0.4.24;

import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol";
pragma solidity 0.8.9;

import {Math256} from "../../common/lib/Math256.sol";

Expand Down Expand Up @@ -35,31 +33,29 @@ library LimiterState {
}

library PositiveTokenRebaseLimiter {
using SafeMath for uint256;

/// @dev Precision base for the limiter (e.g.: 1e6 - 0.1%; 1e9 - 100%)
uint256 private constant LIMITER_PRECISION_BASE = 10**9;
/// @dev Disabled limit
uint256 private constant UNLIMITED_REBASE = uint256(-1);
uint256 private constant UNLIMITED_REBASE = type(uint64).max;

/**
* @dev Initialize the new `LimiterState` structure instance
* @param _rebaseLimit max limiter value (saturation point), see `LIMITER_PRECISION_POINTS`
* @param _totalPooledEther total pooled ether, see `Lido.getTotalPooledEther()`
* @param _totalShares total shares, see `Lido.getTotalShares()`
* @return newly initialized limiter structure
* @return limiterState newly initialized limiter structure
*/
function initLimiterState(
uint256 _rebaseLimit,
uint256 _totalPooledEther,
uint256 _totalShares
) internal pure returns (LimiterState.Data memory _limiterState) {
) internal pure returns (LimiterState.Data memory limiterState) {
require(_rebaseLimit > 0, "TOO_LOW_TOKEN_REBASE_MAX");
require(_rebaseLimit <= UNLIMITED_REBASE, "WRONG_REBASE_LIMIT");

_limiterState.totalPooledEther = _totalPooledEther;
_limiterState.totalShares = _totalShares;
_limiterState.rebaseLimit = _rebaseLimit;
limiterState.totalPooledEther = _totalPooledEther;
limiterState.totalShares = _totalShares;
limiterState.rebaseLimit = _rebaseLimit;
}

/**
Expand All @@ -83,9 +79,7 @@ library PositiveTokenRebaseLimiter {
require(_limiterState.accumulatedRebase == 0, "DIRTY_LIMITER_STATE");

if (_clBalanceDiff < 0 && (_limiterState.rebaseLimit != UNLIMITED_REBASE)) {
_limiterState.rebaseLimit = _limiterState.rebaseLimit.add(
uint256(-_clBalanceDiff).mul(LIMITER_PRECISION_BASE).div(_limiterState.totalPooledEther)
);
_limiterState.rebaseLimit += (uint256(-_clBalanceDiff) * LIMITER_PRECISION_BASE) / _limiterState.totalPooledEther;
} else {
appendEther(_limiterState, uint256(_clBalanceDiff));
}
Expand All @@ -95,7 +89,7 @@ library PositiveTokenRebaseLimiter {
* @dev append ether and return value not exceeding the limit
* @param _limiterState limit repr struct
* @param _etherAmount desired ether addition
* @return allowed to add ether to not exceed the limit
* @return appendableEther allowed to add ether to not exceed the limit
*/
function appendEther(LimiterState.Data memory _limiterState, uint256 _etherAmount)
internal
Expand All @@ -104,25 +98,25 @@ library PositiveTokenRebaseLimiter {
{
if (_limiterState.rebaseLimit == UNLIMITED_REBASE) return _etherAmount;

uint256 remainingRebase = _limiterState.rebaseLimit.sub(_limiterState.accumulatedRebase);
uint256 remainingEther = remainingRebase.mul(_limiterState.totalPooledEther).div(LIMITER_PRECISION_BASE);
uint256 remainingRebase = _limiterState.rebaseLimit - _limiterState.accumulatedRebase;
uint256 remainingEther = (remainingRebase * _limiterState.totalPooledEther) / LIMITER_PRECISION_BASE;

appendableEther = Math256.min(remainingEther, _etherAmount);

if (appendableEther == remainingEther) {
_limiterState.accumulatedRebase = _limiterState.rebaseLimit;
} else {
_limiterState.accumulatedRebase = _limiterState.accumulatedRebase.add(
appendableEther.mul(LIMITER_PRECISION_BASE).div(_limiterState.totalPooledEther)
);
_limiterState.accumulatedRebase += (
appendableEther * LIMITER_PRECISION_BASE
) / _limiterState.totalPooledEther;
}
}

/**
* @dev deduct shares and return value not exceeding the limit
* @param _limiterState limit repr struct
* @param _sharesAmount desired shares deduction
* @return allowed to deduct shares to not exceed the limit
* @return deductableShares allowed to deduct shares to not exceed the limit
*/
function deductShares(LimiterState.Data memory _limiterState, uint256 _sharesAmount)
internal
Expand All @@ -131,19 +125,19 @@ library PositiveTokenRebaseLimiter {
{
if (_limiterState.rebaseLimit == UNLIMITED_REBASE) return _sharesAmount;

uint256 remainingRebase = _limiterState.rebaseLimit.sub(_limiterState.accumulatedRebase);
uint256 remainingShares = _limiterState.totalShares.mul(remainingRebase).div(
LIMITER_PRECISION_BASE.add(remainingRebase)
);
uint256 remainingRebase = _limiterState.rebaseLimit - _limiterState.accumulatedRebase;
uint256 remainingShares = (
_limiterState.totalShares * remainingRebase
) / (LIMITER_PRECISION_BASE + remainingRebase);

deductableShares = Math256.min(_sharesAmount, remainingShares);

if (deductableShares == remainingShares) {
_limiterState.accumulatedRebase = _limiterState.rebaseLimit;
} else {
_limiterState.accumulatedRebase = _limiterState.accumulatedRebase.add(
deductableShares.mul(LIMITER_PRECISION_BASE).div(_limiterState.totalShares.sub(deductableShares))
);
_limiterState.accumulatedRebase += (
deductableShares * LIMITER_PRECISION_BASE
) / (_limiterState.totalShares - deductableShares);
}
}

Expand Down
Loading

0 comments on commit 4089bd1

Please sign in to comment.