Giter Club home page Giter Club logo

lido-dao's Introduction

Lido on Ethereum Logo

GitHub license NodeJS Solidity Hardhat Aragon OS GitHub tests GitHub code analysis GitHub Bytecode

The Lido on Ethereum Liquid Staking Protocol allows their users to earn staking rewards on the Beacon chain without locking ether or maintaining staking infrastructure.

Users can deposit ether to the Lido smart contract and receive stETH tokens in return. The smart contract then stakes tokens with the DAO-picked node operators. Users' deposited funds are pooled by the DAO, and node operators never have direct access to the users' assets.

Unlike staked ether, the stETH token is free from the limitations associated with a lack of liquidity, and can be transferred at any time. The stETH token balance corresponds to the amount of ether that the holder could request to withdraw.

NB: It's advised to read Documentation before getting started with this repo.

Lido DAO

The Lido DAO is a Decentralized Autonomous Organization that manages the liquid staking protocol by deciding on key parameters (e.g., setting fees, assigning node operators and oracles, performing upgrades, etc.) through the voting power of governance token (LDO) holders.

The Lido DAO charges service fees that support infrastructure maintenance, research, development, protocol upgrades, and potential loss coverage.

The Lido DAO was built using the Aragon DAO framework.

Protocol levers

A full list of protocol levers that are controllable by the Aragon DAO can be found here.

Contracts

For the contracts description see https://docs.lido.fi/ contracts section.

Deployments

For the protocol contracts addresses see https://docs.lido.fi/deployed-contracts/

Development

Requirements

  • shell - bash or zsh
  • find
  • sed
  • jq
  • curl
  • cut
  • node.js v18
  • (optional) Lerna
  • (optional) Foundry

Installing Aragon & other deps

Installation is local and doesn't require root privileges.

If you have yarn installed globally:

yarn

otherwise:

npx yarn

Build & test

Run unit tests:

yarn test

Run unit tests and report gas used by each Solidity function:

yarn test:gas

Generate unit test coverage report:

yarn test:coverage

Test coverage is reported to coverage.json and coverage/index.html files located inside each app's folder.

Keep in mind that the code uses asserts to check invariants that should always be kept unless the code is buggy (in contrast to require statements which check pre-conditions), so full branch coverage will never be reported until solidity-coverage#219 is implemented.

Run fuzzing tests with foundry:

curl -L https://foundry.paradigm.xyz | bash
foundryup
forge test

Deploying

To deploy the smart contracts and run the protocol instance either locally or on a new testnet, please proceed to the following scratch deploy documentation

License

2023 Lido [email protected]

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License, or any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

lido-dao's People

Contributors

arwer13 avatar avsetsin avatar bulbozaur avatar claudijd avatar dechjo avatar dependabot[bot] avatar dgusakov avatar diraiks avatar eenae avatar eridianalpha avatar folkyatina avatar grstepanov avatar jeday avatar kadmil avatar klyaus avatar kolyasapphire avatar krogla avatar loga4 avatar madlabman avatar manneredboor avatar mymphe avatar ongrid avatar onionglass avatar psirex avatar rkolpakov avatar sanbir avatar skozin avatar thedzhon avatar ujenjt avatar vshvsh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lido-dao's Issues

Docstrings for DePool, Oracle and stETH contracts

All the libraries, contracts and functions should have relevant DoxygenStrings. Docs should follow the same style, describing the interface and purpose. Non-trivial math should be explained in detail. If the code was inherited or inspired, the link to the source should be provided.

Styling references:

@aragon/os/contracts/apps/AragonApp.sol
@aragon/os/contracts/lib/math/SafeMath.sol
@openzeppelin-solidity/contracts/token/ERC20/ERC20.sol

Insurance spec

We need to come to an agreement for an insurance spec with cover providers. From what I can gather, no additional smart contract coding is required to buy cover and redeem it, it's just a simple tx by a DAO, but there might be options.

Fix muted tests in steth_depool_linked

Base branch: steth_depool_linked

Fix muted assertions and setups in integration tests:

  • depool_happy_path.js:301-308
  • depool.test.js - lines highlighted with //FixMe

Reward distribution spec

This spec describes logic of accounting and distributing rewards on the Ethereum 1 side according to Ethereum 2 side balance changes.

The idea is to distribute rewards by diluting the supply of StETH and giving newly generated tokens to proper parties. Moreover, slashing is taken into account so as not to take fee on Ether2 rewards which just replenish slashed balances.

Initially set:

fee_basis_points = <number in [0..10000]> # protocol fee rate

treasury_fee_basis_points = <number in [0..10000]> # share of protocol fees going into the treasury
insurance_fee_basis_points = <number in [0..10000]> # share of protocol fees going into the insurance fund
staking_providers_fee_basis_points = <number in [0..10000]> # share of protocol fees distributed among staking providers
require(treasury_fee_basis_points + insurance_fee_basis_points + staking_providers_fee_basis_points == 10000)

reward_base = 0

On each deposit to the Ethereum 2 side:

on_deposit(deposit):
    reward_base += deposit  # increase is coming our way, but thats not rewards

On each update of the Ether on the Ethereum 2 side by the oracle:

on_oracle(ether2):
    if ether2 > reward_base:
        rewards = ether2 - reward_base
        distribute(rewards, ether2)
        reward_base = ether2

distribute(rewards, ether2):
    protocol_rewards = rewards * fee_basis_points / 10000
    
    steth_to_mint = protocol_rewards * steth.total_supply() / (getTotalControlledEther() - protocol_rewards)
    // Where getTotalControlledEther = buffered + eth on eth2 side - withdrawals

    steth.mint(vault, steth_to_mint * treasury_fee_basis_points / 10000)
    steth.mint(insurance_fund_vault, steth_to_mint * insurance_fee_basis_points / 10000)
    steth.mint(staking_providers_fund, steth_to_mint * staking_providers_fee_basis_points / 10000)

Distribution inside staking_providers_fund is covered by a staking providers spec.

Backlog for RC-2 milestone

The following table summarizes the list of DePool contracts, docs and test improvements for RC-2 release candidate. RC2 plans will be confirmed on the zoom call 28 Oct 2020.

Timing:
PRs are ready: 12 Nov 2020
RC-2 release: 19 Nov 2020

For RC-1 backlog see #77

Table maintained by @ongrid. Please feel free to drop your ideas in comments (for discussion) or create the issue with assigned RC-2 milestone if you are able to finally formalize the scope and provide the link to it.

Short Description and GH issue Where Type Milestone, status, assignee Details
reportEther2 RBAC #85 DePool, Oracle, stETH Unify RC-1 @ongrid @Klyaus DePool.sol:reportEther2 - use Aragon's approach with role modifier instead of require check
Insurance Vault #46 #33 DePool Impl RC-2 Implement insurance fund as a separate vault under DAO management.
Modular tests. Not relevant anymore. Decided to implement the tests in a linear fashion. tests: DePool, Oracle Tests RC-2 All tests should be combined in modular hierarchical setup structures (with it, contexts and fixtures established within beforeEach block). Printable context names shold clearly explain what happens from user or businness-logic perspective. Printable it-block names should explain what is expected in assert. Avoid mutate/assert/mutate/assert anti-pattern since they are too fragile and painful to maintain.
stake vs deposit. Not relevant anymore after initial grooming and updated oracle spec. DePool Unify RC-2 Eliminate intersections of stake and deposit words. deposit word is reserved for official contract and its function. In-DePool 32-Ether portions and its accounted states are stakes. Rename depositedEther to stakedEther
totalControlledEther to pooledEther. In the LIP-3 it's converted to getter and named getTotalPooledEth, to be implemented in v0.2.0 DePool Unify RC-2 Rename totalControlledEther to pooledEther - shorter and pronounced better
remoteEther2 to beaconEther. It's proposed in LIP-3, to be implemented in v0.2.0 DePool Unify RC-2 Rename remoteEther2 to beaconEther because remote term depends on the point of view, should be avoided in documentation. Also agreed before to use beacon instead of Ether2 as more specific term
reportBeaconBalance. It's proposed in LIP-3, to be implemented in v0.2.0 DePool, Oracle Impl RC-2 Replace existing functions reportEther2 and pushData to the unified form reportBeaconBalance(reportEpoch, beaconEther). Rename data to dataPoint (already used throughout the code and looks good). Add require checks: isEpochReportable (is the epoch number allowed to report, or in other words, does it divide by EPOCHS_PER_REPORT without remainder) and isEpochEligible (timing checks, isn't it in the future or in the too distant past)
member getters #119 Oracle Cosmetic RC-2 Change member-related getters to more ideomatic: findMember to _getMemberID, getOracleMembers to getMembers
ppm instead of base points #120 Oracle, DePool Impl RC-2 Relative numbers are accounted in Parts-per_notation. Since the most popular per cent unit doesn't give enough granularity, the next widely adopted ppm or parts-per-million unit can be used.
maxBalanceChange per epoch. Should be discussed in a separate lip-4 Oracle Impl RC-2 Protect the system from synchronous oracle misbehavior by limiting the maximum allowed balance change per epoch. Should have corresponding setters and management role for fine-tuning.
Epoch-related math and timing. The epoch-bount math proposed in LIP-3, to be implemented in v0.2.0. Ideas on validation and protection should be discussed in a separate LIP#4 Oracle Impl RC-2 Oracle implementation and lifecycle management can be significantly simplified if we use a single chackpoint identifier throughout the entire system - reportEpoch and make Oracle contract the single point of daemon's configuration and source of the timing. The oracle daemon can run in endless loop and poll the Oracle's getOpenReportEpoch on each iteration. It returns the last epoch id, that is open to receive reports. Then oracle daemon should check if it's already transacted reportBeaconBalance with given reportEpoch or not. If not, the oracle retrieves the balances for the given reportEpoch, summarizes them, then initiates and expedites transaction. Rename _getCurrentReportInterval->getCurrentReportableEpoch, getReportIntervalDurationSeconds->getEpochsPerReport to unify. Remove _getReportIntervalForTimestamp - it's not used. Affects E2E tests.
Bad naming: final, tryFinalize. Proposed in LIP-3 (called lido_contract.ReportBeacon), to be implemented in v0.2.0 Oracle Cosmetic RC-2 Reason: to avoid confusions with in-oracle ETH2 finality terms like finalizedEpoch. Without separation we are in confusing situation when finalized epoch in Oracle daemon is not finalized in the contract. So rename _tryFinalize to more specific tryReportToDePool.
Bad naming: "Aggregated" #119 Oracle Cosmetic RC-2 In currentlyAggregatedReportInterval and currentlyAggregatedData "Aggregated" adjective sounds confusing - data doesn't aggregate, staying intact, just temporarily cached and voted. Should change to openForReporting or something shorter like that.
Rename DePool to Lido #52 Oracle, DePool, docs and tests Cosmetic RC-2
Deposit store gas optimizations #53 #109 #82 DePool Impl RC-2
Oracle gas optimizations. Proposed in LIP-3 (tight packing), to be implemented in v0.2.0 Oracle Impl RC-2
E2E tests in RC-2 DONE E2E Tests RC-2, Vlad? Need to specify the scope of test-cases. Note from @ongrid : implementation of new oracles behavior and new Oracle interface will introduce breaking changes between RC's.

Integration with official Deposit Contract

Now IValidatorRegistration interface and ValidatorRegistrationMock are synthetic. To prove compatibility with real-world environment, we should switch to official Ethereum 2 DepositContract and use it in all the test scenarios.

Note: Since there is a lot of math and costly operations (including iterative Merkle Tree walks) inside the real deposit call we can get new gas estimates and the numbers may depend on the Deposit contract state. Probably we'll need to test it at scale.

Related issue: #82

Staking providers spec

This spec describes the staking provider entity. The staking provider is an entity validating Ethereum 2.0 on behalf of the stakers and controlled by the dao.
Up until now, there was no staking provider distinction in the signing key management. Now we can slice the key management into sub-pools keeping most of the key management logic intact.

Interface

Each staking provider has the following properties:

  • id (uint, starting from 1) - a unique key
  • active (bool) - a flag indicating if the SP can participate in further staking and reward distribution
  • name (string) - human-readable name
  • rewardAddress (address) - Ethereum 1 address which receives steth rewards for this SP
  • stakingLimit (uint) - the maximum number of validators to stake for this SP
  • keys (array of pairs (signing key, deposit signature)) - signing keys to use for signing blocks on the Ethereum 2 side
  • stoppedValidators (uint) number of signing keys which stopped validation (e.g. were slashed)
  • usedKeys (uint) - internal property, number of signing keys of this SP which were used in deposits to the Ethereum 2

Properties active, name, rewardAddress, stakingLimit, keys, stoppedValidators are modifiable by any actor with the proper access rights.
Keys are modifiable by the corresponding rewardAddress (implicit permission).

The active property can be switched back and forth, i.e. an SP can be reactivated.
The stakingLimit property can be set to any value, even less than the current stake.
No one can remove signing keys that were already used.
The stoppedValidators property is incremental, i.e. we can only add a positive number to the existing value.

Implementation details

Keeping SPs in a storage array indexed by the SP id is trivial, as well as changing their properties. Each property should be managed by a dedicated function with a dedicated access role. totalSigningKeys and usedSigningKeys global vars go to the SP structure.

Key management changes.

addSigningKeys and removeSigningKey functions get an extra argument - SP id. This parameter should be eventually passed to the _signingKeyOffset function which should calculate the offset as keccak256(abi.encodePacked(SIGNING_KEYS_MAPPING_NAME, SP_id, _keyIndex)). This way we isolate the keys of each SP from others.

_ETH2Deposit changes.

Before loading the next signing key we have to find an SP to load the key from.
We filter out deactivated SPs and SPs out of available keys.
Then, for the remaining SPs we filter out any for which usedKeys - stoppedValidators + 1 > stakingLimit holds true.
Then, we find the SP with the smallest usedKeys - stoppedValidators value.
Transparent optimizations can be applied, e.g. caching the array of stakes in memory during the main _ETH2Deposit loop execution.

Reward distribution.

We designate total reward to be distributed among stake providers as totalReward.
Then, we filter out deactivated SPs.
Then, for each i-th SP we calculate effectiveStake[i] as usedKeys[i] - stoppedValidators[i].
Then, we sum all the effectiveStakes to effectiveStakeTotal.
Then, for each i-th SP their reward is calculated as effectiveStake[i] * totalReward / effectiveStakeTotal and transferred to their rewardAddress.

JavaScript code style (tests, frontend)

Currently, multiple different styles are mixed within even a single test file:

  • Semicolons/no semicolons;
  • Different string literal brackets;
  • Indentation (2 or 4 spaces);
  • etc.

This makes it harder to read and write the code. I would suggest to agree upon some selected style and use it throughout our whole codebase.

As for the style itself, I'd suggest JavaScript Standard Style since it's used in most projects, including blockchain/DeFi ones.

I would also use some linter to enforce the selected style.

Implement cstETH wrap/unwrap functions (yUSD way, 1-to-1 ratio)

Minimal wrap/unwrap functions

Methods responsible for swapping between stETH and cstETH tokens.

Wrapping

Transforming dynamic stETH token to classical cstETH is made in two-stage approach:

  1. the holder authorises cstETH contract to transfer given amount of stETH on his behalf.
  2. the holder initiates cstETH.wrap() tx and provides reserved amount of stETH and the address to withdraw stETH from

wrap(addr, stETHamount) called by the holder of stETH.

  1. cstETH contract withdraws approved amount of stETH and puts it into collateral, then mints the same amount of cstETH tokens to the holder

Example: yUSD:_deposit

UnWrapping

The opposite action initiated by someone wishing to return cstETH and get the corresponding amount of stETH token back.

  1. the holder authorises cstETH contract to withdraw given amount of cstETH.
  2. then holder initiates cstETH.withdraw() tx and provides the amount of approved cstETH

unWrap(addr, cstETHamount) called by the holder of cstETH.

  1. cstETH contract withdraws and burns the approved amount of cstETH and then calculates the amount of stETH and sends i back from the collateral.

Example: yUSD:_withdraw

  • The ratio should be implemented as a constant 1:1 (the spec is in progress).
  • Unit tests
  • use abstract ERC-20 mock for stETH

Improve stETH unit and integration tests

Base branch: steth_depool_linked

Unit tests with mocked dePool

  • steth.test.js should be extended with OZ-like modular tests
  • steth.test.js every share-changing operation need to assert the effect on shares
  • user burns his tokens, other users see their balances increase

Integration tests (depool_w_steth.test.js)

  • user burns his tokens, other users see their balances increase

Oracle reporting correct numbers will result in the erroneous slashing

        // Calculating real amount of rewards
        uint256 rewardBase = REWARD_BASE_VALUE_POSITION.getStorageUint256();
        if (_eth2balance > rewardBase) {
            uint256 rewards = _eth2balance.sub(rewardBase);
            REWARD_BASE_VALUE_POSITION.setStorageUint256(_eth2balance);
            distributeRewards(rewards);
        }

That's the code that is calculating rewards: it thinks that any positive change in eth2 balance (between last and this report) is a reward and any negative one is slashing.

Imagine this scenario:

Block X-100000: deposit 32 eth (total_used_keys = 1)

Block X-1: oracle computes report (epoch,  _eth2balance = 32.01)

Block X: 
1. deposit 32 eth (reward base = 64eth)
2. oracle final report (_eth2balance = 32.01eth, epoch)

Now we have almost 32eth slashing.

depool.withdraw should clearly signal it's not implemented yet

As of now withdraw lets you get a bit of ether from the buffer and burns your tokens + signals that you want to withdraw. At the start there'll be no withdrawal logic implemented yet bc there's no state transitions on eth2 and working temporary solutions around that is too complex for us to possibly fit into the deadline.

Thus, withdraw function should be there with a correct interface (just like now) but it should be always throwing something along the lines of "NOT_IMPLEMENTED"

Backlog for RC-1 milestone

The following table summarizes the list of DePool contracts, docs and test improvements for RC-1 release candidate. RC1 plans are confirmed on the zoom call 20 Oct 2020.

For RC-2 see #94

Table maintained by @ongrid. Please feel free to drop your ideas in comments (for discussion) or create the issue with assigned RC milestone if you are able to finally formalize the scope.

Short Description and GH issue Where Type Milestone, status, assignee Details
SP Registry distributeRewards fails with stETH token #72 done stETH Integration RC-1 @lxzrv @ongrid Integration tests fail on reward distribution
Fix muted tests in steth_depool_linked #73 done DePool, stETH Tests RC-1 @lxzrv @ongrid Fix muted assertions and setups in integration tests: depool_happy_path.js:301-308 depool.test.js - lines highlighted with //FixMe
Withdraw explanation wait DePool Doc RC-1 @vshvsh withdraw function has unclear state - ToDo. Since it occludes the key business logic - it will be in the main people focus, so its docstring should describe in detail about future implementation and migration. When and how it will be implemented, how migration to the contract with implemented withdraw will be carried out. It must be the most documented function in the entire codebase.
Fix solc, solium and solhint warnings #84 done DePool, Oracle, stETH, cStETH + child libs and interfaces Unify RC-1 @ongrid @lxzrv Fix solhint warnings and stilyze the contract codebase in a single manner including existing comments. Fix compiler warnings. Solc stdout should be clean.
Docstrings #87 wip DePool, Oracle, stETH Doc, Unify RC-1 @ongrid All the libraries, contracts and functions should have relevant DoxygenStrings. Docs should follow the same style, describing the interface and purpose. Non-trivial math should be explained in detail. If the code was inherited or inspired, should provide the link to the source.
Solc motivation #86 done DePool, Oracle, stETH, cstETH Doc RC-1 @ongrid Comment above pragma statement should clearly explain why solidity 0.4.24 or 0.6.x selected for specific contract.
ACL roles done DePool, Oracle, stETH Doc RC-1 @skozin Describe each ACL role and where it's used
Positions done DePool, Oracle, stETH Doc RC-1 @skozin describe why POSITIONs used instead of traditional variables
reportEther2 RBAC #85 wip DePool, Oracle, stETH Unify RC-1 @ongrid @Klyaus DePool.sol:reportEther2 - use Aragon's approach with role modifier instead of require check
JavaScript code style #19 done Tests, Frontend Tests RC-1 @krogla or @skozin ? Currently, multiple different styles are mixed within even a single test file. This makes it harder to read and write the code.
IDepositContract #88 done DePool Unify RC-2 @Klyaus use original Ethereum 2 IDepositContract instead of IValidatorRegistration for simplicity and uniform terminology.

Other 16 items were moved to Backlog for RC-2 milestone #94

Add more getters to staking providers smart contracts for testing purposes

Suggestions:

1) Implement getSigningKeys() function which will return an array of signing keys that were added for current sp.
Current solution: right now we can only getSigningKey(sp_id,SigningKey_index)
Reason: for testing purposes where we will test the whole system with a big number of signing keys. We must check the correctness of added signing keys with the source. We can't expect how the system will react to it and also it's not too flexible to call getSigningKey(sp_index,signingKey_index) 200 times for example.
2) Implement getActiveSigningKeys() function which will return an array of active signing keys that became active after validators started.
Current solution: right now we can only check that added signing keys are started validation by check sp1.getUsedSigningKeys().
Reasons: To match used signing keys with active validators pubKeys in the eth2 side. We can clearly check sp1.usedSigningKeys() count,but can`t correctly compare usedSigningKeys active validators pubKeys in eth2.

In conclusion, I might admit that in bundle sp signing keys -> to eth2 we can`t track correctly this action.

Gas optimizations for deposit process

Currently, the submit function, compiled with 200 optimizer iterations, consumes around 577000 gas plus 107000 gas for each registered validator, so the default limit is set by deploy scripts to 16 validators to make the submit transaction occupy no more than 20% of a block. See this file for the related calculations; you can run them with yarn estimate-deposit-loop-gas.

That is on average 130k+ gas per validation key in a batch of 16, about twice as much as a simple deposit. We might find a way to make this number lower.

Note: Switching to the official Deposit contract #88 will impact the numbers mentioned above

Tighter coupling for deposit keys

Deposit keys take 48 bytes which means they take 1.5 slots each that is padded to 2 (and signature takes exactly 3). We can try to pack them more tightly, leading to about 10% reduction in storage costs (5 slots to 4.5). Need to investigate if it's worth increased complexity.

Yarn install broken due to malformed yarn.lock

$ yarn install
yarn install v1.16.0
error An unexpected error occurred: "Unknown token: { line: 3, col: 2, type: 'INVALID', value: undefined } 3:2 in /Users/kirill/Prog/DePool/lido-dao/yarn.lock".
info If you think this is a bug, please open a bug report with the information provided in "/Users/kirill/Prog/DePool/lido-dao/yarn-error.log".
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.

Wrong hash function in DePool contract

According to the official version, the sha256 function is used in the calculation of deposit_data_root (ETH 2.0. spec), whereas in our code keccak256.

As a result, it is impossible to transfer (make submit) more than 32ETH to the DePool contract, due to transaction revert in the Deposit Contract.

#56

Adding signing keys through dao voting is too expensive

addSigningKeys pas a lot of gas for storage (in extremis it's about 1-2k of ether total, but even in the worst case it'll be about 30 eth). Calling functions through dao voting temporarily storages the arguments until the vote is set, leading to double pay for the storage. This is prohibitively expensive for no particular reason, we should do something about it.

Options I see:

  1. Staking providers can set or remove their own keys up to threshold set by the DAO (that looks the best so far)
  2. DAO votes for a hash of keys to add and then anyone can add the keys with the approved hash (variant: merkle root) (too complex)
  3. do a voting modification that allows to vote for an action hash instead of a whole action

referral

There should be an optional parameter in deposit function that takes a referral eth address eg
deposit_with_referral(uint256 referral) payable
That parameter is either unused or emits an event with referral address. I'd prefer a calldata referral bc it's cheaper, but event's okay too

There should be a script to collect referral data in the following format, given block1 and block2.
between block1 and block2 (both included) there have been referalls:

0x123...890 1000.8249240020934eth   10.008249240020934% 
0xabc...def 200eth                  2%

The easiest and most useful way to make that is probably thegraph subgraph, but something simpler might work too

Cosmetic: single-form naming in DePool and StETH

Rename

  • ETHER to ETH in constant names (Ether/Ethers allowed in comments)
  • getBufferedEther to getBufferedEth
  • getTotalControlledEther to getTotalPooledEth
  • _getUnaccountedEther to _getUnaccountedEth

Split signing keys management functions

In the StakingProvidersRegistry contract, the addSigningKeys and removeSigningKeys functions have a custom realization of the permissions check (via 'canPerform') to run the function, but not the recommended Aragon authP modifier.

The buidler-aragon utility, therefore, generates errors when building the application.

I suggest dividing each one of these functions into three separate functions, two of which are simply a wrapper for a 3-d internal function, and have different call permission checks (1-st via 'authP' modifier, 2-nd with custom check).

Sample implementation:

    function removeSigningKey(uint256 _SP_id, uint256 _index) external
        authP(MANAGE_SIGNING_KEYS, arr(_SP_id))
        SPExists(_SP_id)
    {
        _removeSigningKey(_SP_id, _index);
    }

    function removeSigningKeySP(uint256 _SP_id, uint256 _index) external
        SPExists(_SP_id)
    {
        require(msg.sender == sps[_SP_id].rewardAddress);
        _removeSigningKey(_SP_id, _index);
    }

    function _removeSigningKey(uint256 _SP_id, uint256 _index) internal
    {
       // rest of original code
   }

Implement cstETH fungible token functions (OpenZeppelin-way)

Implement minimal ERC-20 token with fungible-asset functions and classical behavior. In contrast to native DePool platform's stETH token, account balances should change only by making a transfer, so it can be used in any DApp, that expects such behavior.

  • Code should be inherited from recent OpenZeppelin v3.2.x (for solc 0.6.x or 0.7.x). via package if possible.
  • Mintable, but external mint function should be removed from base (move to mock).
  • All relevant tests should be adapted from OpenZeppelin (keep builder as the framework)
  • Add different compiler for token (keep paths of Aragon and its apps intact)

Specification for fungible token functions

Base interface: ERC-20 - de-facto standard for fungible assets on Ethereum. Inherited from well-known OpenZeppelin library.

Decimals: 18 - matches ETH1.0 Ether and stETH

Symbol: cstETH - Compound-style staked ETHer

Name: Wrapped Liquid staked DePool Ether

Burnable: yes - The ability to discard holder's own tokens allows further maneuvers with introduction of the new contract if needed (swap to new version, bridge one asset to another and so on).

Approvable: yes - transferFrom, approve, and allowance ERC-20 methods are used in DeFi workflows, allowing contracts to send tokens on your behalf.

Mintable: yes, internal - cstETH gets minted "internally" within the same contract. Mint function not exposed outside. New emission os done exclusively by depositing the same amount of underlying stETH

Minter: self - Minting is done by the calling the mint function by the Wrapping/Unwrapping functions of the contract. No external minters implemented.

Cap: no - The maximum circulating amount of tokens is not limited (no capped). The supply depends on: how much underlying stETH stay in collateral.

TotalSupply: Classical behavior - total supply increases on new deposits and decreases on withdrawals. Every totalSupply change strongly correlates with transactions and events.

BalanceOf: Classical behavior - balances are statically stored in the internal storage. Every balance change strongly correlates with transactions and events.

Event Arguments: Classical behavior - transfer event arguments always have strong arithmetical correlaion with balances.

Libraries used: SafeMath, OpenZeppelin

Related: depool-dao#3

Add function to bulk remove or to change deposit data

If the withdrawal credentials change, staking providers will have to remove not a single erroneous key+signature, but all unused keys at once. Doing it one tx at a time is impractical. There should be a function to either remove all unused keys, or bulk replace the signature part of deposit data for a new withdrawal credential.

double token model

For staked ether we need two types of tokens. We'll call them aTokens and cTokens, in honor of aave and compound protocols.

aTokens are the simplest conceptually: if you own 1 staked ether aToken, it means that there is 1 ether on beacon chain reserved just for you. E.g. if you deposit 100 ethers to depool, you get 100 aTokens. When stake under depool control gets a reward, your balance increases accordingly, when it's slashed it decreases.

After phase 2 is enabled, by burning 1 aToken you'll get 1 ether withdrawn from staking.

aToken balances are updated when oracle reposts change in total stake every day. Updating balances one by one in a balance map is unfeasible, that's why getBalance function of aToken should be implemented as (in pseudocode)
aToken.balanceOf(address) = depool.getCoeff()*depool.shareOfStakedETH(address)
Where coeff represents the growth/shrinking of beacon chain ether due to staking activity, and shareOf represents your part of that big pile of ether on beacon chain. It's not normalized (so, sum of all shareOf values is not 1) but if your shareOf is 1% of total shares it means you own 1% of all controlled ether on beacon chain.

If the balance on beacon chain is changed due to staking (rewards or slashing), your balance in aToken changes, but you still own the same share of the stake, and the only thing that changes is coeff. If you transfer your tokens, total beacon chain stake doesn't change and coeff remains the same, but the shareOf changes.

aToken's weakness is that it doesn't work properly in Uniswap v2, and won't work nicely with crosschain transfers (bridges won't report balance changes daily to other chains, we can't expect that)

aToken is the default token for staked Ether. User get it on deposit.

cToken is compound-style: your balance is fixed, but the amount of ether it's representing is changing. It is a "pro" token for staked ether to be used in complex defi and crosschain transfer, etc. If
aToken.balanceOf(address) = depool.getCoeff()*depool.shareOfStakedETH(address)
then
cToken.balanceOf(address) = depool.shareOfStakedETH(address)

shareOf changes on deposit, transfer, transferFrom, burn, things like that. Coeff changes on oracle reports.

Total sum of shares changes both on deposits and on oracle updates, bc reward fee is minted as new steth tokens (see #2 )

Task: depool should have both types of tokens and aToken should be convertible to cToken and vice versa at will. aToken should have the default name of stEth.

I see two ways to make that:

  1. one is where cToken is the default token, and aToken is minted when you lock cToken and burned when you withdraw aToken (or vice versa)
    aToken.lock(cToken) -> return minted aToken
    aToken.burn_and_unlock(amount) -> return unlocked cToken
  1. other is common interface and two tokens that use it
share_map
|       \
|        \
atoken   ctoken

I think having cToken as base and aToken as minted/burned is the simplest way to do that.

Readme structure

Here is my proposal for README structure for the repo.

I assume that we aim to leave only the contracts code along with unit tests in the repository. If everyone agrees with this statement, I also propose to discuss moving the code for running the local test environment and the web interface for Aragon into a separate repo. If possible.

Feel free to add things that I missed🙌

DePool Ethereum Liquid Staking Protocol

Overview

Brief description of the protocol
DAO description
Before getting started with this repo, please read:

  • Whitepaper
  • Documentation

Other Links

  • Discord
  • Audits
  • Mainnet address

Contracts

The protocol is implemented as a set of smart contracts connected by Aragon.

DePool

Liquid staking pool implementation. The core contract that is responsible for acceptance Ether from users and minting liquid tokens in return, delegating staking to DAO's picked validators and manage/reward them, update StETH token balances according to staking performance reported by DePoolOracle.

DePoolOracle

Oracle tasked to inform other parts of the system about balances controlled by the DAO on the ETH 2.0 side

StETH token

StETH is a tokenized version of staked ether. Tokens are minted upon deposit and burned when redeemed. StETH tokens are pegged 1:1 to the Ethers that are held by DePool. StETH token’s balances are updated when oracle reports change in total stake every day.

CstETH token

CstETH is a compound style version of StETH token. Unlike the default StETH token, the balance of CstETH token remains fixed despite the changes Ether held by DePool DAO. But as DePool’s validators earn interest, CstETH becomes convertible into an increasing quantity of the underlying asset.

CstETH Vault

CstETH Vault is a contract that is responsible for minting CstETH. Minting CstETH is optional, users can stick with their StETH, which is fine for a lot of DeFi apps, but some of them are accepting only compound-style tokens. Users can deposit their StETH tokens to the contract and gets CstETH in return and vice versa (in this case CstETH tokens got burned).

Development

  • Requirements
  • Installation
  • Build & test all our apps
  • Other things

License

DePool is under ??? licence

Original Deposit contracts should be ignored by linters

Since official deposit contract is not our part, solidity linters should ignore it.

$ yarn run lint
yarn run v1.16.0
$ yarn lint:sol && yarn lint:js
$ yarn lint:sol:solhint && yarn lint:sol:solium
$ solhint "contracts/**/*.sol"

contracts/0.6.11/deposit_contract.sol
  114:15  error  Parse error: missing ';' at '('
  114:30  error  Parse error: missing ';' at 'gwei'
  114:39  error  Parse error: mismatched input ',' expecting ';'
  114:94  error  Parse error: extraneous input ')' expecting ';'
  115:44  error  Parse error: extraneous input 'gwei' expecting ';'

✖ 6 problems (5 errors, 1 warning)

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Enhance SP-related stETH tests + cosmetic

Base branch: steth_depool_linked

  • Current tests don't actually cover dual SP configuration (just one SP active).
    The best option is to use modular setup like this
 with single sp configuration
  <adapt existing tests>
  then added second (inactive) SP
  <adapt setup. Asserts should produce the same result as above>
   then activated second sp with equal amount of keys
     <Adapt math>
     then added keys to first sp (now unequal amount of keys)
      <Adapt math>

Cosmetic:

  • Initialize human-readeble sp reward addresses in the contract context instead of ADDRESS_1
  • Fix function order according to the Natspec - external, public, internal, pure; const after non-const.

Parent issues: #70, #3

Decouple tokens from Aragon

Current tight coupling of tokens with the AragonApp makes it inconsistent with the best practices:

  • older solidity 0.4.24 available for Aragon is too far from 0.7 stable
  • newer tools and approaches are unavailable for older solc

The tokens should be decoupled from this unnecessary dependency:

  • Aragon should have just the required interfaces written in solidity 0.4.24
  • The tokens are compiled by the separate solc 0.7 with up-to-date dependencies from OpenZeppelin
  • The unit-tests for tokens are separate (based on open-zeppelin flow)
  • The integration tests are still common

reportEther2 switch to Aragon's access-control role modifier

function reportEther2(uint256 _epoch, uint256 _eth2balance) uses in-block check of the caller:
require(msg.sender == getOracle(), "APP_AUTH_FAILED"). For RBAC it's correct to use Aragon's auth modifier. Examples: setWithdrawalCredentials and setDepositIterationLimit.

Effects stay the same.

Integrate oracle into E2E scenario

E2E scenario should include running Oracle daemon (in single instance mode for now). So startup.sh script should

  • initalize all relevant contracts (shouldn't need to navigate Aragon's UI)
  • generate signatures
  • submit the keys to the staking provider registry
  • expose all relevant data to the oracle (ORACLE_CONTRACT, DEPOOL_CONTRACT)
  • run the oracle instance

Oracle instance should:

  • run with given envs
  • connect to ETH1 and Beacon nodes
  • pull validators' keys from the DEPOOL contract
  • poll the keys for beacon balances and summarize them
  • report the balances to the ORACLE contract

Related depool-dao#4

Submitting Ether to the pool consumes prohibitively high amount of gas

Currently, submitting even a small amount of Ether to the pool consumes a lot of gas, see #82 (comment). This can be fixed by moving staking to a separate function in Lido.sol:

  • When a user submits Ether to the pool, it gets buffered and tokens are minted.
  • Actual staking of the buffered Ether happens in a separate function (which can be called by anyone).

Split DAO deploy into two transactions

Currently, deploying DAO from the template consumes the amount of gas close to 12M limit, see #98 (comment).

The workaround is to extract assigning permissions to a separate function in the template and call it in a different transaction. Maybe smth else can be safely extracted from the main deployDAO function — need to think about it.

Execute pushData error in e2e environment

In the e2e environment an error occurs when trying to execute the pushData function in the oracle contract.
Presumably the error comes from an external call in function _tryFinalize

ValueError: {'message': 'VM Exception while processing transaction: revert', 'code': -32000, 'data': {'stack': 'c: VM Exception while processing transaction: revert\n at Function.c.fromResults (/usr/local/lib/node_modules/ganache-cli/build/ganache-core.node.cli.js:4:183508)\n at e.exports (/usr/local/lib/node_modules/ganache-cli/build/ganache-core.node.cli.js:48:1989251)', 'name': 'c'}}

template deploy issue

Problem 1: we had an error in the DAO template - the stETH contract was initialized incorrectly: it has zero DePool contract address.

As a solution, it was possible, by analogy, to add a setPool function to the stETH contract. However, as the tests revealed, this leads to problem 2: more than 12 million of Gas is needed for the DAO deployment and the transaction fails.

I propose the following solution for both problems:
Remove the setPool functionality from the Oracle and SPRegistry contracts, and set the DePool address via the initialize function.
In the DePool contract, on the contrary, move the set of all associated contracts to a separate setApps function.
And simply change the order in which applications are initialized in the template.

This small optimization will reduce the amount of Gas to ~ 11M.

SP Registry distributeRewards with share-based stETH token

Base branch: steth_depool_linked

Integration tests with stETH fail on the line:

https://github.com/depools/depool-dao/blob/16820516a8e481e660f5dd106d150c7ff7b8866c/contracts/0.4.24/sps/StakingProvidersRegistry.sol#L312

Also noted that some transfers on this line were called with zero amount, that shouldn't be done.

This can affect depool.test.js:381 (reverts, commented-out)

Need to unmute treasury, insurance and SP balance assertions in depool.test.js:117 to 121

Fix solc, solium and solhint warnings

Fix function-order, indentation, blank-lines and line-length in solidity files. Keep comments and docstrings intact (they will be the subject of separate issue).

Styling references:

@aragon/os/contracts/apps/AragonApp.sol
@aragon/os/contracts/lib/math/SafeMath.sol
@openzeppelin-solidity/contracts/token/ERC20/ERC20.sol
  • Add relevant configs for solium and solhint.
  • Add solhint to package.json lint script (should run both)
  • mute compile-time SPDX-License-Identifier warning (retaled to our code)

Implement dynamic cstETH wrap/unwrap functions and tests

Add dynamic formula for wrap/unwrap functions of cstETH based on the following model discussed in modeling repo

    def get_cst_eth_by_st_eth(self, st_eth):
        st_eth_collateralized = self.st_eth.balanceOf(self)
        cst_eth_issued = self.total_supply
        cst_eth_per_st_eth = cst_eth_issued/st_eth_collateralized if st_eth_collateralized != 0 else 1.0
        return(cst_eth_per_st_eth * st_eth)
    
    def get_st_eth_by_cst_eth(self, cst_eth):
        st_eth_collateralized = self.st_eth.balanceOf(self)
        cst_eth_issued = self.total_supply
        st_eth_per_cst_eth = st_eth_collateralized/cst_eth_issued if cst_eth_issued != 0 else 1.0
        return(st_eth_per_cst_eth * cst_eth)

Add test-cases for later deposit with increased and decreased amounts of st_eth.balanceOf(cst_eth). Reward/Loss should be 'fair' - amounts of stETH after wrapping/waiting/unwraping should be strictly the same as if holder did nothing.

Parent Issue: #3

Add solc compiler motivation

All contracts should contain comment above pragma statement that should clearly explain why solidity 0.4.24 or 0.6.x selected for specific contract.

Make staking loop bounded (DePool contract)

Currently, the _ETH2Deposit function (which is used by DePool to stake Ether buffered in the contract) tries to register as many validators as possible, as long as there is Ether buffered and unused validator keys left in the registry.

This makes it possible for this loop to revert due to it consuming more gas than allowed in a single block, which may happen if the amount of Ether buffered in the contract is large enough. This, in turn, may happen either if somebody sends a large amount of Ether in a single transaction, or if staking providers run out of registered validator keys for some reason (e.g. technical issues) and users continue to send Ether in the meantime.

The situation that arises will effectively lock all buffered Ether and stop the contract from registering new validators.

The proposed fix is to bound the loop using some predefined iteration limit that will ensure that the loop won't consume more than a fraction (e.g. 20%) of block gas. Also, we'll need to add an externally-accessible function that would trigger staking of the remaining buffered Ether, so one may resume the staking loop broken by the iteration limit:

/**
  * @notice Signals that some received Ether was left buffered in the contract
  * due to the inability to register enough Ethereum2 validators in a single
  * transaction. In this case, one needs to manually call depositBufferedEther()
  * function to resume the registration loop.
  */
event NeedToResumeETH2DepositLoop();


/// @dev TODO: determine the optimal limit
uint256 constant internal MAX_VALIDATOR_REGISTRATIONS_IN_A_BLOCK = 30;


/**
  * @notice Deposits as much buffered Ether as possible in a single transaction.
  * The NeedToResumeETH2DepositLoop() event will be generated if more buffered Ether
  * can be deposited by calling the function once again.
  */
function depositBufferedEther() external whenNotStopped {
  _depositBufferedEther();
}


function _submit(address _referral) internal whenNotStopped returns (uint256 StETH) {
  // ...
  _depositBufferedEther();
}


function _depositBufferedEther() internal {
  uint256 buffered = _getBufferedEther();
  if (buffered >= DEPOSIT_SIZE) {
    uint256 unaccounted = _getUnaccountedEther();

    uint256 toUnbuffer = buffered.div(DEPOSIT_SIZE).mul(DEPOSIT_SIZE);
    assert(toUnbuffer <= buffered && toUnbuffer != 0);

    (uint256 deposited, bool canDepositMore) = _ETH2Deposit(toUnbuffer);
    _markAsUnbuffered(deposited);

    assert(_getUnaccountedEther() == unaccounted);

    if (canDepositMore) {
      emit NeedToResumeETH2DepositLoop();
    }
  }
}


function _ETH2Deposit(uint256 _amount) internal returns (uint256 deposited, bool canDepositMore) {
  // ...
  uint256 iter = 0;
  while (_amount != 0 && ++iter <= MAX_VALIDATOR_REGISTRATIONS_IN_A_BLOCK) {
    // ...
  }
  canDepositMore = bestSPidx != cache.length && _amount >= DEPOSIT_SIZE;
}

The depositBufferedEther() can be called by anyone. The MAX_VALIDATOR_REGISTRATIONS_IN_A_BLOCK constant may be made configurable via the DAO voting.

oracle

oracle (smart contract)

based on https://github.com/depools/depool-dao/blob/master/apps/depooloracle/contracts/DePoolOracle.sol but

  1. oracles should report uniform data: it's eth2 blockchain, when finalized there should be no two different views on network and all honest oracles should report the exact same result
    thus https://github.com/depools/depool-dao/blob/master/apps/depooloracle/contracts/DePoolOracle.sol#L225
    should count quorum on exact results instead of mean result
  2. Find a different name for Epoch (maybe interval) bc people are confusing it with eth2 Epoch
  3. allow reporting epochs from the past (not current epochs) as long as epoch is > than the last reported one and <= current one (nb can be exploited in edge cases for a short time)

oracle (daemon)

oracle should report balance for every 7200*x slot (0, 7200, 14400, ...) when it's finalized but not earlier. In pushData(uint256 _epoch, ...) _epoch (that needs to be renamed to _data_timestamp or something) should be the blocktime of the slot. Edge cases are when there's no block for the slot - in that case, balance and blocktime should be for the last finalized block before 7200*x, usually 7200*x-1.

Balance reported = sum of all balances on active validators under depool control (up to date list of keys is in the smart contract), + sum of all balances on validators under depool control in the activation queue + sum of all balances under depool control in the exit queue + sum of all balances on inactive (exited) validators under depool control

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.