Conversation
| function redeem(uint256 _stETHAmount, address _ethRecipient) external onlyRole(REDEEMER_ROLE) whenResumed { | ||
| if (_stETHAmount == 0) revert ZeroAmount(); | ||
| if (_ethRecipient == address(0)) revert ZeroRecipient(); | ||
| if (LIDO.isStopped()) revert LidoStopped(); | ||
| if (WITHDRAWAL_QUEUE.isBunkerModeActive()) revert BunkerMode(); | ||
| if (WITHDRAWAL_QUEUE.isPaused()) revert WQPaused(); | ||
|
|
||
| uint256 sharesAmount = LIDO.getSharesByPooledEth(_stETHAmount); | ||
| uint256 etherAmount = LIDO.getPooledEthByShares(sharesAmount); | ||
|
|
||
| uint256 redeemedBefore = STORE.getValue(REDEEMED_ETHER_SLOT); | ||
| uint256 available = _reserveBalance - redeemedBefore; | ||
| if (etherAmount > available) { | ||
| revert InsufficientReserve(etherAmount, available); | ||
| } | ||
|
|
||
| LIDO.transferSharesFrom(msg.sender, address(this), sharesAmount); | ||
| BURNER.requestBurnShares(address(this), sharesAmount); | ||
|
|
||
| STORE.set(REDEEMED_ETHER_SLOT, uint104(redeemedBefore + etherAmount)); | ||
|
|
||
| (bool success,) = _ethRecipient.call{value: etherAmount}(""); | ||
| if (!success) revert ETHTransferFailed(_ethRecipient, etherAmount); | ||
|
|
||
| emit Redeemed(msg.sender, _ethRecipient, _stETHAmount, sharesAmount, etherAmount); | ||
| } |
Hardhat Unit Tests Coverage SummaryDetailsDiff against masterResults for commit: 8f8b960 Minimum allowed coverage is ♻️ This comment has been updated with latest results |
9e8757c to
eb28e1a
Compare
eb28e1a to
7f95abd
Compare
| function initialize(address _admin) external initializer { | ||
| if (_admin == address(0)) revert AdminCannotBeZero(); | ||
| __AccessControlEnumerable_init(); | ||
| LIDO.approve(address(BURNER), type(uint256).max); | ||
| _grantRole(DEFAULT_ADMIN_ROLE, _admin); | ||
| } |
| function recoverStETHShares(address _recipient) external onlyRole(RECOVER_ROLE) { | ||
| if (_recipient == address(0)) revert ZeroRecipient(); | ||
| uint256 shares = LIDO.sharesOf(address(this)); | ||
| if (shares > 0) { | ||
| emit StETHSharesRecovered(msg.sender, shares, _recipient); | ||
| LIDO.transferShares(_recipient, shares); | ||
| } | ||
| } |
7f95abd to
5151aa6
Compare
| */ | ||
| struct BufferedEtherAllocation { | ||
| uint256 total; | ||
| uint256 redeemsReserve; |
There was a problem hiding this comment.
Consider adding it as last property of the struct
| // --- 1. Buffer round-trip: withdraw unredeemed ETH, reconcile bufferedEther --- | ||
| address buffer = locator.redeemsBuffer(); | ||
| if (buffer != address(0)) { | ||
| IRedeemsBuffer(buffer).withdrawUnredeemed(_redeemedEther); |
There was a problem hiding this comment.
Nitpick: the naming is a bit misleading here, since withdrawUnredeemed() is called with redeemedEther
| contract RedeemsBuffer is PausableUntil, AccessControlEnumerableUpgradeable { | ||
| using SafeERC20 for IERC20; | ||
| using SafeCast for uint256; | ||
| bytes32 public constant PAUSE_ROLE = keccak256("RedeemsBuffer.PauseRole"); |
There was a problem hiding this comment.
I’d recommend keeping the previous role naming convention unless there is a specific need to change it. Having the constant name match the keccak256 input that produces the role hash is a convenient convention and worth preserving if possible
| * @author Lido | ||
| * @notice Generic key-value store that auto-snapshots uint104 values at oracle frame boundaries | ||
| */ | ||
| contract RefSlotStore is IRefSlotStore, AccessControlEnumerable { |
There was a problem hiding this comment.
Consider making this contract expose only the beacon for the current oracle reporting phase. It may be better to move away from the refSlot terminology and use naming aligned with oracle frames instead — something like AccountingFrameClock.getCurrentFrame(). A shared global storage contract that can be used by different callers, where one caller may overwrite values written by another, feels a bit scary
| _pre.totalPooledEther - _pre.externalEther, // we need to change the base as shareRate is now calculated on | ||
| _pre.totalShares - _pre.externalShares, // internal ether and shares, but inside it's still total | ||
| _pre.totalPooledEther - _pre.externalEther - _pre.redeemedEther, // we need to change the base as shareRate is now calculated on | ||
| _pre.totalShares - _pre.externalShares - update.redeemedShares, // internal ether and shares, but inside it's still total |
There was a problem hiding this comment.
Could this line underflow in the case of a negative report and deferred redeemed shares from the previous report?
| function recoverEther(address _recipient) external onlyRole(RECOVER_ROLE) { | ||
| if (_recipient == address(0)) revert ZeroRecipient(); | ||
| uint256 amount = address(this).balance + _redeemedEther.value - _reserveBalance; | ||
| if (amount > 0) { | ||
| emit EtherRecovered(msg.sender, amount, _recipient); | ||
| (bool success,) = _recipient.call{value: amount}(""); | ||
| if (!success) revert ETHTransferFailed(_recipient, amount); | ||
| } | ||
| } |
| function _currentRefSlot() private view returns (uint256) { | ||
| (uint256 refSlot,) = HASH_CONSENSUS.getCurrentFrame(); | ||
| return refSlot; | ||
| } |
45ef24a to
ba3ee47
Compare
ba3ee47 to
8f8b960
Compare
No description provided.