[stETH] ERC20 Token Smart Contract Technical Assessment

[stETH] ERC20 Token Smart Contract Technical Assessment

General Information

Risk Summary

  • Does the contract implement the ERC20 token standards?
    Partially. The standard specifies that a ‘Transfer’ event must be emitted when tokens are transferred. stETH has periodic rebases where all balances change without such event emissions. For wstETH this discrepancy does not exist and it is ERC20 compliant.
  • Risk analysis : HIGH.

Technical Information

  • Compiler version : v0.4.24+commit.e67f0147
  • Decimals : 18
  • Overflow checks : Yes, the contract uses a SafeMath library.
  • Mitigation against allowance race-condition : Yes, the contract implements increaseAllowance and decreaseAllowance to get around this issue.
  • Upgradeable contract patterns :
    Yes. A proxy contract is used. An upgrade will be needed to support withdrawals (when supported on ETH2).
  • Access control or restriction lists :
    Yes. There is access control for pausing the token transfers, burning tokens, changing the token supply oracle and managing deposit fees. These capabilities are currently in the hands of the Lido DAO.
  • Non-standard features or behaviors :
    • The contract supply is periodically rebased through an oracle (currently with 3/5 feed quorum). There are sanity thresholds to the periodic supply change (currently -5% daily or +10% annualy).
    • The contract is pausable by a privileged actor (currently the Lido DAO).
    • Tokens can be burned by a privileged actor (currently the Lido DAO).
    • Ether submitted in exchange for minting stETH can not be withdrawn in the current contract version. The funds are protected by an m-of-n threshold scheme (currently 6-of-11).
    • The token implementation is not in a standalone contract and instead inherited by the Lido staking and fee distribution contract.

Formal Verification Considerations:

  • Does transfer have simple semantics? No. As this is a rebase token, storage changes are according to calculated shares amount and not the input token amount.
  • Does transferFrom have simple semantics? No. As this is a rebase token, storage changes are according to calculated shares amount and not the input token amount.
  • Can balances be arbitrarily modified by some actor? Yes, an oracle can change the total supply, which will change balances. Tokens of a can also be burned by a privileged actor.
  • Are there any external calls? Yes. The contract stakes Ether on the ETH2 deposit contract. There is also support for recovering stuck Ether/tokens by sending them to a recovery address.

Testnet Information

From https://blog.lido.fi/stake-lido-using-metamask-testnet/:

Contract Logic Summary

The Lido contract acts as a liquid ETH2 staking pool. The contract is responsible for Ether deposits, minting and burning liquid tokens, delegating funds to node operators and applying fees.

Lido inherits an ERC20 token contract which represents staked ether, stETH.
Tokens are minted upon deposit and can also be burned by a privileged actor.
stETH token’s balances are periodically updated when an oracle reports change in total stake (currently every day).

Upgrading the Contract

The contract uses an Aragon based proxy for upgradeability, utilizing the unstructured storage pattern.

Administrative Addresses

The Lido DAO contract has these privileged abilities:

  • Stop/resume deposits and future withdrawal requests.
  • Manage future withdrawal credentials.
  • Burn tokens.
  • Set addresses (oracle, treasury, insurance fund contract addresses).
  • Set fees.

Contract Risk Summary

Overall we believe this is a high risk integration.

On a technical level the contract risk can be considered medium. It is implemented to the industry standards (SafeMath is used, ERC20 allowance race condition is mitigated), but has uncommonn logic:

  • The vast staking and Aragon inherited logic, which might contain an undiscovered bug, and includes a non trivial upgrade process.
  • The reliance on an off chain oracle for rebalancing.
  • The need for a new Makerdao oracle implementation based on Curve as a sole liquidity source.

There are additional custodial risks which we believe that combined with the above elevate the overall risk to high:

  • The token is fully controlled by a privileged actor (currently the Lido DAO) which has the power to upgrade implementation and change storage at will. In the current version that privileged actor can also change the withdrawal address, pause transfers or burn tokens.
  • There is an additional custodial risk concerning the withdrawal multi-key scheme, which can lead to loss of funds.

Market liquidity risk is not evaluated in this scope.

Supporting Materials

Inheritance Diagram

drawing
drawing

Sūrya’s Description Report

Files Description Table

File Name SHA-1 Hash
contracts/0.4.24/StETH.sol a4466bf44002eede1fc1289d48ca58fc2c81ae90

Contracts Description Table

Contract Type Bases
└ Function Name Visibility Mutability Modifiers
StETH Implementation IERC20, Pausable
└ name Public :exclamation: NO❗️
└ symbol Public :exclamation: NO❗️
└ decimals Public :exclamation: NO❗️
└ totalSupply Public :exclamation: NO❗️
└ getTotalPooledEther Public :exclamation: NO❗️
└ balanceOf Public :exclamation: NO❗️
└ transfer Public :exclamation: :stop_sign: NO❗️
└ allowance Public :exclamation: NO❗️
└ approve Public :exclamation: :stop_sign: NO❗️
└ transferFrom Public :exclamation: :stop_sign: NO❗️
└ increaseAllowance Public :exclamation: :stop_sign: NO❗️
└ decreaseAllowance Public :exclamation: :stop_sign: NO❗️
└ getTotalShares Public :exclamation: NO❗️
└ sharesOf Public :exclamation: NO❗️
└ getSharesByPooledEth Public :exclamation: NO❗️
└ getPooledEthByShares Public :exclamation: NO❗️
└ _getTotalPooledEther Internal :lock:
└ _transfer Internal :lock: :stop_sign:
└ _approve Internal :lock: :stop_sign: whenNotStopped
└ _getTotalShares Internal :lock:
└ _sharesOf Internal :lock:
└ _transferShares Internal :lock: :stop_sign: whenNotStopped
└ _mintShares Internal :lock: :stop_sign: whenNotStopped
└ _burnShares Internal :lock: :stop_sign: whenNotStopped

Files Description Table

File Name SHA-1 Hash
contracts/0.4.24/Lido.sol b1f9dde1cb5558e94131e227b2533de11489d630

Contracts Description Table

Contract Type Bases
└ Function Name Visibility Mutability Modifiers
Lido Implementation ILido, IsContract, StETH, AragonApp
└ initialize Public :exclamation: :stop_sign: onlyInit
└ External :exclamation: :dollar: NO❗️
└ submit External :exclamation: :dollar: NO❗️
└ depositBufferedEther External :exclamation: :stop_sign: NO❗️
└ depositBufferedEther External :exclamation: :stop_sign: NO❗️
└ burnShares External :exclamation: :stop_sign: authP
└ stop External :exclamation: :stop_sign: auth
└ resume External :exclamation: :stop_sign: auth
└ setFee External :exclamation: :stop_sign: auth
└ setFeeDistribution External :exclamation: :stop_sign: auth
└ setOracle External :exclamation: :stop_sign: auth
└ setTreasury External :exclamation: :stop_sign: auth
└ setInsuranceFund External :exclamation: :stop_sign: auth
└ setWithdrawalCredentials External :exclamation: :stop_sign: auth
└ withdraw External :exclamation: :stop_sign: whenNotStopped
└ pushBeacon External :exclamation: :stop_sign: whenNotStopped
└ transferToVault External :exclamation: :stop_sign: NO❗️
└ getFee External :exclamation: NO❗️
└ getFeeDistribution External :exclamation: NO❗️
└ getWithdrawalCredentials Public :exclamation: NO❗️
└ getBufferedEther External :exclamation: NO❗️
└ getDepositContract Public :exclamation: NO❗️
└ getOracle Public :exclamation: NO❗️
└ getOperators Public :exclamation: NO❗️
└ getTreasury Public :exclamation: NO❗️
└ getInsuranceFund Public :exclamation: NO❗️
└ getBeaconStat Public :exclamation: NO❗️
└ _setDepositContract Internal :lock: :stop_sign:
└ _setOracle Internal :lock: :stop_sign:
└ _setOperators Internal :lock: :stop_sign:
└ _setTreasury Internal :lock: :stop_sign:
└ _setInsuranceFund Internal :lock: :stop_sign:
└ _submit Internal :lock: :stop_sign: whenNotStopped
└ _emitTransferAfterMintingShares Internal :lock: :stop_sign:
└ _depositBufferedEther Internal :lock: :stop_sign: whenNotStopped
└ _ETH2Deposit Internal :lock: :stop_sign:
└ _stake Internal :lock: :stop_sign:
└ distributeRewards Internal :lock: :stop_sign:
└ _distributeNodeOperatorsReward Internal :lock: :stop_sign:
└ _submitted Internal :lock: :stop_sign:
└ _markAsUnbuffered Internal :lock: :stop_sign:
└ _setBPValue Internal :lock: :stop_sign:
└ _getFee Internal :lock:
└ _getFeeDistribution Internal :lock:
└ _readBPValue Internal :lock:
└ _getBufferedEther Internal :lock:
└ _getUnaccountedEther Internal :lock:
└ _getTransientBalance Internal :lock:
└ _getTotalPooledEther Internal :lock:
└ _pad64 Internal :lock:
└ _toLittleEndian64 Internal :lock:
└ to64 Internal :lock:

Legend

Symbol Meaning
:stop_sign: Function can modify state
:dollar: Function is payable
10 Likes

Thank you for this great assessment.

I also believe this represents a high risk which warrants additional discussions.

3 Likes

As a developer in Lido, can confirm that the assessment is impeccable.

3 Likes

This sounds pretty expensive to maintain. Any idea of how much stETH is currently being used as collateral? I would imagine if onboarded it would call for a double-digit SF north of 10%++ based on the high risk integration, IMO–Not Financial Advice.

1 Like