Skip to content

Commit

Permalink
Merge pull request #389 from lidofinance/audit/LIP-6
Browse files Browse the repository at this point in the history
LIP-6: burning the limited amount per single run [WIP]
  • Loading branch information
TheDZhon authored Jan 24, 2022
2 parents ee1991b + f3d8950 commit 3d6a3f5
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 31 deletions.
97 changes: 80 additions & 17 deletions contracts/0.8.9/SelfOwnedStETHBurner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pragma solidity 0.8.9;

import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts-v4.4/utils/math/Math.sol";
import "./interfaces/IBeaconReportReceiver.sol";

/**
Expand Down Expand Up @@ -47,6 +48,11 @@ interface ILido {
* @param _account provided account address.
*/
function sharesOf(address _account) external view returns (uint256);

/**
* @notice Get total amount of shares in existence
*/
function getTotalShares() external view returns (uint256);
}

/**
Expand All @@ -68,16 +74,27 @@ interface IOracle {
* @dev Burning stETH means 'decrease total underlying shares amount to perform stETH token rebase'
*/
contract SelfOwnedStETHBurner is IBeaconReportReceiver {
uint256 private constant MAX_BASIS_POINTS = 10000;

uint256 private coverSharesBurnRequested;
uint256 private nonCoverSharesBurnRequested;

uint256 private totalCoverSharesBurnt;
uint256 private totalNonCoverSharesBurnt;

uint256 private maxBurnAmountPerRunBasisPoints = 4; // 0.04% by default for the biggest `stETH:ETH` curve pool

address public immutable LIDO;
address public immutable TREASURY;
address public immutable VOTING;

/**
* Emitted when a new single burn quota is set
*/
event BurnAmountPerRunQuotaChanged(
uint256 maxBurnAmountPerRunBasisPoints
);

/**
* Emitted when a new stETH burning request is added by the `requestedBy` address.
*/
Expand Down Expand Up @@ -135,24 +152,48 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver {
* @param _voting the Lido Aragon Voting address
* @param _totalCoverSharesBurnt Shares burnt counter init value (cover case)
* @param _totalNonCoverSharesBurnt Shares burnt counter init value (non-cover case)
* @param _maxBurnAmountPerRunBasisPoints Max burn amount per single run
*/
constructor(
address _treasury,
address _lido,
address _voting,
uint256 _totalCoverSharesBurnt,
uint256 _totalNonCoverSharesBurnt
uint256 _totalNonCoverSharesBurnt,
uint256 _maxBurnAmountPerRunBasisPoints
) {
require(_treasury != address(0), "TREASURY_ZERO_ADDRESS");
require(_lido != address(0), "LIDO_ZERO_ADDRESS");
require(_voting != address(0), "VOTING_ZERO_ADDRESS");
require(_maxBurnAmountPerRunBasisPoints > 0, "ZERO_BURN_AMOUNT_PER_RUN");
require(_maxBurnAmountPerRunBasisPoints <= MAX_BASIS_POINTS, "TOO_LARGE_BURN_AMOUNT_PER_RUN");

TREASURY = _treasury;
LIDO = _lido;
VOTING = _voting;

totalCoverSharesBurnt = _totalCoverSharesBurnt;
totalNonCoverSharesBurnt = _totalNonCoverSharesBurnt;

maxBurnAmountPerRunBasisPoints = _maxBurnAmountPerRunBasisPoints;
}

/**
* Sets the maximum amount of shares allowed to burn per single run (quota).
*
* @dev only `voting` allowed to call this function.
*
* @param _maxBurnAmountPerRunBasisPoints a fraction expressed in basis points (taken from Lido.totalSharesAmount)
*
*/
function setBurnAmountPerRunQuota(uint256 _maxBurnAmountPerRunBasisPoints) external {
require(_maxBurnAmountPerRunBasisPoints > 0, "ZERO_BURN_AMOUNT_PER_RUN");
require(_maxBurnAmountPerRunBasisPoints <= MAX_BASIS_POINTS, "TOO_LARGE_BURN_AMOUNT_PER_RUN");
require(msg.sender == VOTING, "MSG_SENDER_MUST_BE_VOTING");

emit BurnAmountPerRunQuotaChanged(_maxBurnAmountPerRunBasisPoints);

maxBurnAmountPerRunBasisPoints = _maxBurnAmountPerRunBasisPoints;
}

/**
Expand Down Expand Up @@ -200,7 +241,7 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver {

emit ExcessStETHRecovered(msg.sender, excessStETH, excessSharesAmount);

IERC20(LIDO).transfer(TREASURY, excessStETH);
require(IERC20(LIDO).transfer(TREASURY, excessStETH));
}
}

Expand All @@ -220,12 +261,11 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver {
*/
function recoverERC20(address _token, uint256 _amount) external {
require(_amount > 0, "ZERO_RECOVERY_AMOUNT");
require(_token != address(0), "ZERO_ERC20_ADDRESS");
require(_token != LIDO, "STETH_RECOVER_WRONG_FUNC");

emit ERC20Recovered(msg.sender, _token, _amount);

IERC20(_token).transfer(TREASURY, _amount);
require(IERC20(_token).transfer(TREASURY, _amount));
}

/**
Expand All @@ -236,8 +276,6 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver {
* @param _tokenId minted token id
*/
function recoverERC721(address _token, uint256 _tokenId) external {
require(_token != address(0), "ZERO_ERC721_ADDRESS");

emit ERC721Recovered(msg.sender, _token, _tokenId);

IERC721(_token).transferFrom(address(this), TREASURY, _tokenId);
Expand Down Expand Up @@ -271,20 +309,38 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver {
"APP_AUTH_FAILED"
);

uint256 maxSharesToBurnNow = (ILido(LIDO).getTotalShares() * maxBurnAmountPerRunBasisPoints) / MAX_BASIS_POINTS;

if (memCoverSharesBurnRequested > 0) {
totalCoverSharesBurnt += memCoverSharesBurnRequested;
uint256 coverStETHBurnAmountRequested = ILido(LIDO).getPooledEthByShares(memCoverSharesBurnRequested);
emit StETHBurnt(true /* isCover */, coverStETHBurnAmountRequested, memCoverSharesBurnRequested);
coverSharesBurnRequested = 0;
}
if (memNonCoverSharesBurnRequested > 0) {
totalNonCoverSharesBurnt += memNonCoverSharesBurnRequested;
uint256 nonCoverStETHBurnAmountRequested = ILido(LIDO).getPooledEthByShares(memNonCoverSharesBurnRequested);
emit StETHBurnt(false /* isCover */, nonCoverStETHBurnAmountRequested, memNonCoverSharesBurnRequested);
nonCoverSharesBurnRequested = 0;
uint256 sharesToBurnNowForCover = Math.min(maxSharesToBurnNow, memCoverSharesBurnRequested);

totalCoverSharesBurnt += sharesToBurnNowForCover;
uint256 stETHToBurnNowForCover = ILido(LIDO).getPooledEthByShares(sharesToBurnNowForCover);
emit StETHBurnt(true /* isCover */, stETHToBurnNowForCover, sharesToBurnNowForCover);

coverSharesBurnRequested -= sharesToBurnNowForCover;

// early return if at least one of the conditions is TRUE:
// - we have reached a capacity per single run already
// - there are no pending non-cover requests
if ((sharesToBurnNowForCover == maxSharesToBurnNow) || (memNonCoverSharesBurnRequested == 0)) {
ILido(LIDO).burnShares(address(this), sharesToBurnNowForCover);
return;
}
}

ILido(LIDO).burnShares(address(this), burnAmount);
// we're here only if memNonCoverSharesBurnRequested > 0
uint256 sharesToBurnNowForNonCover = Math.min(
maxSharesToBurnNow - memCoverSharesBurnRequested,
memNonCoverSharesBurnRequested
);

totalNonCoverSharesBurnt += sharesToBurnNowForNonCover;
uint256 stETHToBurnNowForNonCover = ILido(LIDO).getPooledEthByShares(sharesToBurnNowForNonCover);
emit StETHBurnt(false /* isCover */, stETHToBurnNowForNonCover, sharesToBurnNowForNonCover);
nonCoverSharesBurnRequested -= sharesToBurnNowForNonCover;

ILido(LIDO).burnShares(address(this), memCoverSharesBurnRequested + sharesToBurnNowForNonCover);
}

/**
Expand All @@ -301,6 +357,13 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver {
return totalNonCoverSharesBurnt;
}

/**
* Returns the max amount of shares allowed to burn per single run
*/
function getBurnAmountPerRunQuota() external view returns (uint256) {
return maxBurnAmountPerRunBasisPoints;
}

/**
* Returns the stETH amount belonging to the burner contract address but not marked for burning.
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/abi/SelfOwnedStETHBurner.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"inputs":[{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_voting","type":"address"},{"internalType":"uint256","name":"_totalCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_totalNonCoverSharesBurnt","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"ExcessStETHRecovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnt","type":"event"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExcessStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"processLidoOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverExcessStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETHForCover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
[{"inputs":[{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_voting","type":"address"},{"internalType":"uint256","name":"_totalCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_totalNonCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_maxBurnAmountPerRunBasePoints","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"ExcessStETHRecovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxBurnAmountPerRunBasePoints","type":"uint256"}],"name":"MaxBurnAmountPerRunChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnt","type":"event"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExcessStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaxBurnAmountPerRunBasePoints","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"processLidoOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverExcessStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETHForCover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxBurnAmountPerRunBasePoints","type":"uint256"}],"name":"setMaxBurnAmountPerRunBasePoints","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
Loading

0 comments on commit 3d6a3f5

Please sign in to comment.