// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 /* See contracts/COMPILERS.md */ pragma solidity 0.4.24; import {IERC20} from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; import {Pausable} from "./utils/Pausable.sol"; import {UnstructuredStorageExt} from "./utils/UnstructuredStorageExt.sol"; import {Math256} from "../common/lib/Math256.sol"; /** * @title Interest-bearing ERC20-like token for Lido Liquid Stacking protocol. * * This contract is abstract. To make the contract deployable override the * `_getTotalPooledEther` function. `Lido.sol` contract inherits StETH and defines * the `_getTotalPooledEther` function. * * StETH balances are dynamic and represent the holder's share in the total amount * of ether controlled by the protocol. Account shares aren't normalized, so the * contract also stores the sum of all shares to calculate each account's token balance * which equals to: * * shares[account] * _getTotalPooledEther() / _getTotalShares() * * For example, assume that we have: * * _getTotalPooledEther() -> 10 ETH * sharesOf(user1) -> 100 * sharesOf(user2) -> 400 * * Therefore: * * balanceOf(user1) -> 2 tokens which corresponds 2 ETH * balanceOf(user2) -> 8 tokens which corresponds 8 ETH * * Since balances of all token holders change when the amount of total pooled Ether * changes, this token cannot fully implement ERC20 standard: it only emits `Transfer` * events upon explicit transfer between holders. In contrast, when total amount of * pooled ether increases, no `Transfer` events are generated: doing so would require * emitting an event for each token holder and thus running an unbounded loop. * * The token inherits from `Pausable` and uses `whenNotStopped` modifier for methods * which change `shares` or `allowances`. `_stop` and `_resume` functions are overridden * in `Lido.sol` and might be called by an account with the `PAUSE_ROLE` assigned by the * DAO. This is useful for emergency scenarios, e.g. a protocol bug, where one might want * to freeze all token transfers and approvals until the emergency is resolved. */ contract StETH is IERC20, Pausable { using SafeMath for uint256; using UnstructuredStorage for bytes32; using UnstructuredStorageExt for bytes32; address constant internal INITIAL_TOKEN_HOLDER = 0xdead; uint256 constant internal INFINITE_ALLOWANCE = ~uint256(0); uint256 constant internal UINT128_MAX = ~uint128(0); /** * @dev StETH balances are dynamic and are calculated based on the accounts' shares * and the total amount of ether controlled by the protocol. Account shares aren't * normalized, so the contract also stores the sum of all shares to calculate * each account's token balance which equals to: * * shares[account] * _getTotalPooledEther() / _getTotalShares() */ mapping (address => uint256) private shares; /** * @dev Allowances are nominated in tokens, not token shares. */ mapping (address => mapping (address => uint256)) private allowances; /** * @dev Storage position used for holding the total amount of shares in existence. * * The Lido protocol is built on top of Aragon and uses the Unstructured Storage pattern * for value types: * * https://blog.openzeppelin.com/upgradeability-using-unstructured-storage * https://blog.8bitzen.com/posts/20-02-2020-understanding-how-solidity-upgradeable-unstructured-proxies-work * * For reference types, conventional storage variables are used since it's non-trivial * and error-prone to implement reference-type unstructured storage using Solidity v0.4; * see https://github.com/lidofinance/lido-dao/issues/181#issuecomment-736098834 * * keccak256("lido.StETH.totalAndExternalShares") * * @dev Since version 3, high 128 bits can be used to store the external shares from Lido contract */ bytes32 internal constant TOTAL_SHARES_POSITION_LOW128 = 0x6038150aecaa250d524370a0fdcdec13f2690e0723eaf277f41d7cae26b359e6; /** * @dev Bitmask for high 128 bits of 256-bit slot */ uint256 constant internal UINT128_HIGH_MASK = ~uint256(0) << 128; /** * @notice An executed shares transfer from `sender` to `recipient`. * * @dev emitted in pair with an ERC20-defined `Transfer` event. */ event TransferShares( address indexed from, address indexed to, uint256 sharesValue ); /** * @notice An executed `burnShares` request * * @dev Reports simultaneously burnt shares amount * and corresponding stETH amount. * The stETH amount is calculated twice: before and after the burning incurred rebase. * * @param account holder of the burnt shares * @param preRebaseTokenAmount amount of stETH the burnt shares corresponded to before the burn * @param postRebaseTokenAmount amount of stETH the burnt shares corresponded to after the burn * @param sharesAmount amount of burnt shares */ event SharesBurnt( address indexed account, uint256 preRebaseTokenAmount, uint256 postRebaseTokenAmount, uint256 sharesAmount ); /** * @return the name of the token. */ function name() external pure returns (string) { return "Liquid staked Ether 2.0"; } /** * @return the symbol of the token, usually a shorter version of the * name. */ function symbol() external pure returns (string) { return "stETH"; } /** * @return the number of decimals for getting user representation of a token amount. */ function decimals() external pure returns (uint8) { return 18; } /** * @return the amount of tokens in existence. * * @dev Always equals to `_getTotalPooledEther()` since token amount * is pegged to the total amount of ether controlled by the protocol. */ function totalSupply() external view returns (uint256) { return _getTotalPooledEther(); } /** * @return the entire amount of ether controlled by the protocol. * * @dev The sum of all ETH balances in the protocol, equals to the total supply of stETH. */ function getTotalPooledEther() external view returns (uint256) { return _getTotalPooledEther(); } /** * @return the amount of tokens owned by the `_account`. * * @dev Balances are dynamic and equal the `_account`'s share in the amount of the * total ether controlled by the protocol. See `sharesOf`. */ function balanceOf(address _account) external view returns (uint256) { return getPooledEthByShares(_sharesOf(_account)); } /** * @notice Moves `_amount` tokens from the caller's account to the `_recipient` account. * * @return a boolean value indicating whether the operation succeeded. * Emits a `Transfer` event. * Emits a `TransferShares` event. * * Requirements: * * - `_recipient` cannot be the zero address or the stETH contract itself. * - the caller must have a balance of at least `_amount`. * - the contract must not be paused. * * @dev The `_amount` argument is the amount of tokens, not shares. */ function transfer(address _recipient, uint256 _amount) external returns (bool) { _transfer(msg.sender, _recipient, _amount); return true; } /** * @return the remaining number of tokens that `_spender` is allowed to spend * on behalf of `_owner` through `transferFrom`. This is zero by default. * * @dev This value changes when `approve` or `transferFrom` is called. */ function allowance(address _owner, address _spender) public view returns (uint256) { return allowances[_owner][_spender]; } /** * @notice Sets `_amount` as the allowance of `_spender` over the caller's tokens. * * @dev allowance can be set to "infinity" (INFINITE_ALLOWANCE). * In this case allowance is not to be spent on transfer, that can save some gas. * * @return a boolean value indicating whether the operation succeeded. * Emits an `Approval` event. * * Requirements: * * - `_spender` cannot be the zero address. * * @dev The `_amount` argument is the amount of tokens, not shares. */ function approve(address _spender, uint256 _amount) external returns (bool) { _approve(msg.sender, _spender, _amount); return true; } /** * @notice Moves `_amount` tokens from `_sender` to `_recipient` using the * allowance mechanism. `_amount` is then deducted from the caller's * allowance if it's not infinite. * * @return a boolean value indicating whether the operation succeeded. * * Emits a `Transfer` event. * Emits a `TransferShares` event. * Emits an `Approval` event if the allowance is updated. * * Requirements: * * - `_sender` cannot be the zero address. * - `_recipient` cannot be the zero address or the stETH contract itself. * - `_sender` must have a balance of at least `_amount`. * - the caller must have allowance for `_sender`'s tokens of at least `_amount`. * - the contract must not be paused. * * @dev The `_amount` argument is the amount of tokens, not shares. */ function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) { _spendAllowance(_sender, msg.sender, _amount); _transfer(_sender, _recipient, _amount); return true; } /** * @notice Atomically increases the allowance granted to `_spender` by the caller by `_addedValue`. * * This is an alternative to `approve` that can be used as a mitigation for * problems described in: * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/b709eae01d1da91902d06ace340df6b324e6f049/contracts/token/ERC20/IERC20.sol#L57 * Emits an `Approval` event indicating the updated allowance. * * Requirements: * * - `_spender` cannot be the zero address. */ function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool) { _approve(msg.sender, _spender, allowances[msg.sender][_spender].add(_addedValue)); return true; } /** * @notice Atomically decreases the allowance granted to `_spender` by the caller by `_subtractedValue`. * * This is an alternative to `approve` that can be used as a mitigation for * problems described in: * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/b709eae01d1da91902d06ace340df6b324e6f049/contracts/token/ERC20/IERC20.sol#L57 * Emits an `Approval` event indicating the updated allowance. * * Requirements: * * - `_spender` cannot be the zero address. * - `_spender` must have allowance for the caller of at least `_subtractedValue`. */ function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) { uint256 currentAllowance = allowances[msg.sender][_spender]; require(currentAllowance >= _subtractedValue, "ALLOWANCE_BELOW_ZERO"); _approve(msg.sender, _spender, currentAllowance.sub(_subtractedValue)); return true; } /** * @return the total amount of shares in existence. * * @dev The sum of all accounts' shares can be an arbitrary number, therefore * it is necessary to store it in order to calculate each account's relative share. */ function getTotalShares() external view returns (uint256) { return _getTotalShares(); } /** * @return the amount of shares owned by `_account`. */ function sharesOf(address _account) external view returns (uint256) { return _sharesOf(_account); } /** * @param _ethAmount the amount of ether to convert to shares. Must be less than UINT128_MAX. * @return the amount of shares that corresponds to `_ethAmount` protocol-controlled Ether. * @dev the result is rounded down. */ function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) { require(_ethAmount < UINT128_MAX, "ETH_TOO_LARGE"); return (_ethAmount * _getShareRateDenominator()) // denominator in shares / _getShareRateNumerator(); // numerator in ether } /** * @param _sharesAmount the amount of shares to convert to ether. Must be less than UINT128_MAX. * @return the amount of ether that corresponds to `_sharesAmount` token shares. * @dev the result is rounded down. */ function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) { require(_sharesAmount < UINT128_MAX, "SHARES_TOO_LARGE"); return (_sharesAmount * _getShareRateNumerator()) // numerator in ether / _getShareRateDenominator(); // denominator in shares } /** * @param _sharesAmount the amount of shares to convert to ether. Must be less than UINT128_MAX. * @return the amount of ether that corresponds to `_sharesAmount` token shares. * @dev The result is rounded up. So, * for `shareRate >= 0.5`, `getSharesByPooledEth(getPooledEthBySharesRoundUp(1))` will be 1. */ function getPooledEthBySharesRoundUp(uint256 _sharesAmount) public view returns (uint256) { require(_sharesAmount < UINT128_MAX, "SHARES_TOO_LARGE"); uint256 numeratorInEther = _getShareRateNumerator(); uint256 denominatorInShares = _getShareRateDenominator(); return Math256.ceilDiv(_sharesAmount * numeratorInEther, denominatorInShares); } /** * @notice Moves `_sharesAmount` token shares from the caller's account to the `_recipient` account. * * @return amount of transferred tokens. * Emits a `TransferShares` event. * Emits a `Transfer` event. * * Requirements: * * - `_recipient` cannot be the zero address or the stETH contract itself. * - the caller must have at least `_sharesAmount` shares. * - the contract must not be paused. * * @dev The `_sharesAmount` argument is the amount of shares, not tokens. */ function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256) { _transferShares(msg.sender, _recipient, _sharesAmount); uint256 tokensAmount = getPooledEthByShares(_sharesAmount); _emitTransferEvents(msg.sender, _recipient, tokensAmount, _sharesAmount); return tokensAmount; } /** * @notice Moves `_sharesAmount` token shares from the `_sender` account to the `_recipient` account. * * @return amount of transferred tokens. * Emits a `TransferShares` event. * Emits a `Transfer` event. * * Requirements: * * - `_sender` and `_recipient` cannot be the zero addresses. * - `_sender` must have at least `_sharesAmount` shares. * - the caller must have allowance for `_sender`'s tokens of at least `getPooledEthByShares(_sharesAmount)`. * - the contract must not be paused. * * @dev The `_sharesAmount` argument is the amount of shares, not tokens. */ function transferSharesFrom( address _sender, address _recipient, uint256 _sharesAmount ) external returns (uint256) { uint256 tokensAmount = getPooledEthByShares(_sharesAmount); _spendAllowance(_sender, msg.sender, tokensAmount); _transferShares(_sender, _recipient, _sharesAmount); _emitTransferEvents(_sender, _recipient, tokensAmount, _sharesAmount); return tokensAmount; } /** * @return the total amount (in wei) of ether controlled by the protocol. * @dev This is used for calculating tokens from shares and vice versa. * @dev This function is required to be implemented in a derived contract. */ function _getTotalPooledEther() internal view returns (uint256); /** * @return the numerator of the protocol's share rate (in ether). * @dev used to convert shares to tokens and vice versa. * @dev can be overridden in a derived contract. */ function _getShareRateNumerator() internal view returns (uint256) { return _getTotalPooledEther(); } /** * @return the denominator of the protocol's share rate (in shares). * @dev used to convert shares to tokens and vice versa. * @dev can be overridden in a derived contract. */ function _getShareRateDenominator() internal view returns (uint256) { return _getTotalShares(); } /** * @notice Moves `_amount` tokens from `_sender` to `_recipient`. * Emits a `Transfer` event. * Emits a `TransferShares` event. */ function _transfer(address _sender, address _recipient, uint256 _amount) internal { uint256 _sharesToTransfer = getSharesByPooledEth(_amount); _transferShares(_sender, _recipient, _sharesToTransfer); _emitTransferEvents(_sender, _recipient, _amount, _sharesToTransfer); } /** * @notice Sets `_amount` as the allowance of `_spender` over the `_owner` s tokens. * * Emits an `Approval` event. * * NB: the method can be invoked even if the protocol paused. * * Requirements: * * - `_owner` cannot be the zero address. * - `_spender` cannot be the zero address. */ function _approve(address _owner, address _spender, uint256 _amount) internal { require(_owner != address(0), "APPROVE_FROM_ZERO_ADDR"); require(_spender != address(0), "APPROVE_TO_ZERO_ADDR"); allowances[_owner][_spender] = _amount; emit Approval(_owner, _spender, _amount); } /** * @dev Updates `owner` s allowance for `spender` based on spent `amount`. * * Does not update the allowance amount in case of infinite allowance. * Revert if not enough allowance is available. * * Might emit an {Approval} event. */ function _spendAllowance(address _owner, address _spender, uint256 _amount) internal { uint256 currentAllowance = allowances[_owner][_spender]; if (currentAllowance != INFINITE_ALLOWANCE) { require(currentAllowance >= _amount, "ALLOWANCE_EXCEEDED"); _approve(_owner, _spender, currentAllowance - _amount); } } /** * @return the total amount of shares in existence. */ function _getTotalShares() internal view returns (uint256) { return TOTAL_SHARES_POSITION_LOW128.getLowUint128(); } /** * @return the amount of shares owned by `_account`. */ function _sharesOf(address _account) internal view returns (uint256) { return shares[_account]; } /** * @notice Moves `_sharesAmount` shares from `_sender` to `_recipient`. * * Requirements: * * - `_sender` cannot be the zero address. * - `_recipient` cannot be the zero address or the `stETH` token contract itself * - `_sender` must hold at least `_sharesAmount` shares. * - the contract must not be paused. */ function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal { require(_sender != address(0), "TRANSFER_FROM_ZERO_ADDR"); require(_recipient != address(0), "TRANSFER_TO_ZERO_ADDR"); require(_recipient != address(this), "TRANSFER_TO_STETH_CONTRACT"); _whenNotStopped(); uint256 currentSenderShares = shares[_sender]; require(_sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED"); shares[_sender] = currentSenderShares.sub(_sharesAmount); shares[_recipient] = shares[_recipient].add(_sharesAmount); } /** * @notice Creates `_sharesAmount` shares and assigns them to `_recipient`, increasing the total amount of shares. * @dev This doesn't increase the token total supply. * * NB: The method doesn't check protocol pause relying on the external enforcement. * * Requirements: * * - `_recipient` cannot be the zero address or StETH token contract itself * - the contract must not be paused. */ function _mintShares(address _recipient, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { require(_recipient != address(0), "MINT_TO_ZERO_ADDR"); require(_recipient != address(this), "MINT_TO_STETH_CONTRACT"); newTotalShares = _getTotalShares().add(_sharesAmount); require(newTotalShares & UINT128_HIGH_MASK == 0, "SHARES_OVERFLOW"); TOTAL_SHARES_POSITION_LOW128.setLowUint128(newTotalShares); shares[_recipient] = shares[_recipient].add(_sharesAmount); // Notice: we're not emitting a Transfer event from the zero address here since shares mint // works by taking the amount of tokens corresponding to the minted shares from all other // token holders, proportionally to their share. The total supply of the token doesn't change // as the result. This is equivalent to performing a send from each other token holder's // address to `address`, but we cannot reflect this as it would require sending an unbounded // number of events. } /** * @notice Destroys `_sharesAmount` shares from `_account`'s holdings, decreasing the total amount of shares. * @dev This doesn't decrease the token total supply. * * Requirements: * * - `_account` cannot be the zero address. * - `_account` must hold at least `_sharesAmount` shares. * - the contract must not be paused. */ function _burnShares(address _account, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { require(_account != address(0), "BURN_FROM_ZERO_ADDR"); uint256 accountShares = shares[_account]; require(_sharesAmount <= accountShares, "BALANCE_EXCEEDED"); newTotalShares = _getTotalShares().sub(_sharesAmount); TOTAL_SHARES_POSITION_LOW128.setLowUint128(newTotalShares); shares[_account] = accountShares.sub(_sharesAmount); } /** * @dev Emits {Transfer} and {TransferShares} events */ function _emitTransferEvents(address _from, address _to, uint256 _tokenAmount, uint256 _sharesAmount) internal { emit Transfer(_from, _to, _tokenAmount); emit TransferShares(_from, _to, _sharesAmount); } /** * @dev Emits {Transfer} and {TransferShares} events where `from` is 0 address. Indicates mint events. */ function _emitTransferAfterMintingShares(address _to, uint256 _sharesAmount) internal { _emitTransferEvents(address(0), _to, getPooledEthByShares(_sharesAmount), _sharesAmount); } /** * @dev Emits {SharesBurnt} event */ function _emitSharesBurnt( address _account, uint256 _preRebaseTokenAmount, uint256 _postRebaseTokenAmount, uint256 _sharesAmount ) internal { emit SharesBurnt(_account, _preRebaseTokenAmount, _postRebaseTokenAmount, _sharesAmount); } /** * @dev Mints shares to INITIAL_TOKEN_HOLDER */ function _mintInitialShares(uint256 _sharesAmount) internal { _mintShares(INITIAL_TOKEN_HOLDER, _sharesAmount); _emitTransferAfterMintingShares(INITIAL_TOKEN_HOLDER, _sharesAmount); } }