├── .gitignore ├── .python-version ├── README.md ├── brownie-config.yaml ├── bugbounty.md ├── contracts ├── MerklePatriciaProofVerifier.sol ├── SafeMath.sol ├── StableSwapPriceHelper.vy ├── StableSwapStateOracle.sol └── StateProofVerifier.sol ├── interfaces └── StableSwapStateOracle.json ├── offchain ├── generate_steth_price_proof.py ├── state_proof.py └── utils.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | .DS_Store 7 | .env 8 | /venv 9 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.12 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trustless price oracle for ETH/stETH Curve pool 2 | 3 | A trustless oracle for the ETH/stETH Curve pool using Merkle Patricia proofs of Ethereum state. 4 | 5 | The oracle assumes that the pool's `fee` and `A` (amplification coefficient) values don't 6 | change between the time of proof generation and submission. 7 | 8 | 9 | ## Audits 10 | 11 | Commits [`1033b3e`] and [`ae093b3`] (the currently deployed version) were audited by MixBytes. 12 | Contracts in both commits were assumed as secure to use according to the auditors' security 13 | criteria. See [the full report] for details. 14 | 15 | [`1033b3e`]: https://github.com/lidofinance/curve-merkle-oracle/tree/1033b3e84142317ffd8f366b52e489d5eb49c73f 16 | [`ae093b3`]: https://github.com/lidofinance/curve-merkle-oracle/tree/ae093b308999a564ed3f23d52c6c5dce946dbfa7 17 | [the full report]: https://github.com/lidofinance/audits/blob/main/MixBytes%20stETH%20price%20oracle%20Security%20Audit%20Report%2005-2021.pdf 18 | 19 | 20 | ## Mechanics 21 | 22 | The oracle works by generating and verifying Merkle Patricia proofs of the following Ethereum state: 23 | 24 | * Curve stETH/ETH pool contract account and the following slots from its storage trie: 25 | * `admin_balances[0]` 26 | * `admin_balances[1]` 27 | 28 | * stETH contract account and the following slots from its storage trie: 29 | * `shares[0xDC24316b9AE028F1497c275EB9192a3Ea0f67022]` 30 | * `keccak256("lido.StETH.totalShares")` 31 | * `keccak256("lido.Lido.beaconBalance")` 32 | * `keccak256("lido.Lido.bufferedEther")` 33 | * `keccak256("lido.Lido.depositedValidators")` 34 | * `keccak256("lido.Lido.beaconValidators")` 35 | 36 | 37 | ## Contracts 38 | 39 | The repo contains two main contracts: 40 | 41 | * [`StableSwapStateOracle.sol`] is the main oracle contract. It receives and verifies the report 42 | from the offchain code, and persists the verified state along with its timestamp. 43 | 44 | * [`StableSwapPriceHelper.vy`] is a helper contract used by `StableSwapStateOracle.sol` and written 45 | in Vyper. It contains the code for calculating exchange price based on the values of pool's storage 46 | slots. The code is copied from the [actual pool contract] with minimal modifications. 47 | 48 | [`StableSwapStateOracle.sol`]: ./contracts/StableSwapStateOracle.sol 49 | [`StableSwapPriceHelper.vy`]: ./contracts/StableSwapPriceHelper.vy 50 | [actual pool contract]: https://github.com/curvefi/curve-contract/blob/3fa3b6c/contracts/pools/steth/StableSwapSTETH.vy 51 | 52 | 53 | ## Deploying and using the contracts 54 | 55 | First, deploy `StableSwapPriceHelper`. Then, deploy `StableSwapStateOracle`, pointing it 56 | to `StableSwapPriceHelper` using the constructor param: 57 | 58 | ```python 59 | # assuming eth-brownie console 60 | 61 | helper = StableSwapPriceHelper.deploy({ 'from': deployer }) 62 | 63 | price_update_threshold = 300 # 3% 64 | price_update_threshold_admin = deployer 65 | 66 | oracle = StableSwapStateOracle.deploy( 67 | helper, 68 | price_update_threshold_admin, 69 | price_update_threshold, 70 | { 'from': deployer } 71 | ) 72 | ``` 73 | 74 | To send proofs to the state oracle, call the `submitState` function: 75 | 76 | ```python 77 | header_rlp_bytes = '0x...' 78 | proofs_rlp_bytes = '0x...' 79 | 80 | tx = oracle.submitState(header_rlp_bytes, proofs_rlp_bytes, { 'from': reporter }) 81 | ``` 82 | 83 | The function is permissionless and, upon successful verification, will generate two events, 84 | `SlotValuesUpdated` and `PriceUpdated`, and update the oracle with the verified pool balances 85 | and stETH price. You can access them by calling `getState` and `getPrice`: 86 | 87 | ```python 88 | (timestamp, etherBalance, stethBalance, stethPrice) = oracle.getState() 89 | stethPrice = oracle.getPrice() 90 | print("stETH/ETH price:", stethPrice / 10**18) 91 | ``` 92 | 93 | 94 | ## Sending oracle transaction 95 | 96 | Use the following script to generate and submit a proof to the oracle contract: 97 | 98 | ``` 99 | python offchain/generate_steth_price_proof.py \ 100 | --rpc \ 101 | --keyfile \ 102 | --gas-price \ 103 | --contract \ 104 | --block 105 | ``` 106 | 107 | Some flags are optional: 108 | 109 | * Skip the `--keyfile` flag to print the proof without sending a tx. 110 | * Skip the `--gas-price` flag to use gas price determined by the node. 111 | * Skip the `--block` flag to generate a proof correnspoding to the block `latest - 15`. 112 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - hamdiallam/Solidity-RLP@2.0.5 3 | 4 | compiler: 5 | solc: 6 | version: 0.6.12 7 | optimizer: 8 | enabled: true 9 | runs: 200 10 | 11 | networks: 12 | default: development 13 | development: 14 | cmd: ganache-cli 15 | host: http://127.0.0.1 16 | timeout: 120 17 | cmd_settings: 18 | port: 8545 19 | gas_limit: 12000000 20 | accounts: 10 21 | evm_version: istanbul 22 | mnemonic: brownie 23 | fork: https://archive-node.address 24 | -------------------------------------------------------------------------------- /bugbounty.md: -------------------------------------------------------------------------------- 1 | # Bug Bounties with Immunefi 2 | 3 | ## Overview 4 | 5 | This bug bounty document verifies that Lido hosts a bug bounty on Immunefi at the address [https://immunefi.com/bounty/lido/](https://immunefi.com/bounty/lido/). 6 | 7 | If you have found a vulnerability in our project, it must be submitted through [Immunefi's platform](https://immunefi.com/). Immunefi will handle bug bounty communications. 8 | 9 | See the bounty page at Immunefi for more details on accepted vulnerabilities, payout amounts, and rules of participation. 10 | 11 | Users who violate the rules of participation will not receive bug bounty payouts and may be temporarily suspended or banned from the bug bounty program. 12 | -------------------------------------------------------------------------------- /contracts/MerklePatriciaProofVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /** 4 | * Copied from https://github.com/lorenzb/proveth/blob/c74b20e/onchain/ProvethVerifier.sol 5 | * with minor performance and code style-related modifications. 6 | */ 7 | pragma solidity 0.6.12; 8 | 9 | import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.5/contracts/RLPReader.sol"; 10 | 11 | 12 | library MerklePatriciaProofVerifier { 13 | using RLPReader for RLPReader.RLPItem; 14 | using RLPReader for bytes; 15 | 16 | /// @dev Validates a Merkle-Patricia-Trie proof. 17 | /// If the proof proves the inclusion of some key-value pair in the 18 | /// trie, the value is returned. Otherwise, i.e. if the proof proves 19 | /// the exclusion of a key from the trie, an empty byte array is 20 | /// returned. 21 | /// @param rootHash is the Keccak-256 hash of the root node of the MPT. 22 | /// @param path is the key of the node whose inclusion/exclusion we are 23 | /// proving. 24 | /// @param stack is the stack of MPT nodes (starting with the root) that 25 | /// need to be traversed during verification. 26 | /// @return value whose inclusion is proved or an empty byte array for 27 | /// a proof of exclusion 28 | function extractProofValue( 29 | bytes32 rootHash, 30 | bytes memory path, 31 | RLPReader.RLPItem[] memory stack 32 | ) internal pure returns (bytes memory value) { 33 | bytes memory mptKey = _decodeNibbles(path, 0); 34 | uint256 mptKeyOffset = 0; 35 | 36 | bytes32 nodeHashHash; 37 | RLPReader.RLPItem[] memory node; 38 | 39 | RLPReader.RLPItem memory rlpValue; 40 | 41 | if (stack.length == 0) { 42 | // Root hash of empty Merkle-Patricia-Trie 43 | require(rootHash == 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421); 44 | return new bytes(0); 45 | } 46 | 47 | // Traverse stack of nodes starting at root. 48 | for (uint256 i = 0; i < stack.length; i++) { 49 | 50 | // We use the fact that an rlp encoded list consists of some 51 | // encoding of its length plus the concatenation of its 52 | // *rlp-encoded* items. 53 | 54 | // The root node is hashed with Keccak-256 ... 55 | if (i == 0 && rootHash != stack[i].rlpBytesKeccak256()) { 56 | revert(); 57 | } 58 | // ... whereas all other nodes are hashed with the MPT 59 | // hash function. 60 | if (i != 0 && nodeHashHash != _mptHashHash(stack[i])) { 61 | revert(); 62 | } 63 | // We verified that stack[i] has the correct hash, so we 64 | // may safely decode it. 65 | node = stack[i].toList(); 66 | 67 | if (node.length == 2) { 68 | // Extension or Leaf node 69 | 70 | bool isLeaf; 71 | bytes memory nodeKey; 72 | (isLeaf, nodeKey) = _merklePatriciaCompactDecode(node[0].toBytes()); 73 | 74 | uint256 prefixLength = _sharedPrefixLength(mptKeyOffset, mptKey, nodeKey); 75 | mptKeyOffset += prefixLength; 76 | 77 | if (prefixLength < nodeKey.length) { 78 | // Proof claims divergent extension or leaf. (Only 79 | // relevant for proofs of exclusion.) 80 | // An Extension/Leaf node is divergent iff it "skips" over 81 | // the point at which a Branch node should have been had the 82 | // excluded key been included in the trie. 83 | // Example: Imagine a proof of exclusion for path [1, 4], 84 | // where the current node is a Leaf node with 85 | // path [1, 3, 3, 7]. For [1, 4] to be included, there 86 | // should have been a Branch node at [1] with a child 87 | // at 3 and a child at 4. 88 | 89 | // Sanity check 90 | if (i < stack.length - 1) { 91 | // divergent node must come last in proof 92 | revert(); 93 | } 94 | 95 | return new bytes(0); 96 | } 97 | 98 | if (isLeaf) { 99 | // Sanity check 100 | if (i < stack.length - 1) { 101 | // leaf node must come last in proof 102 | revert(); 103 | } 104 | 105 | if (mptKeyOffset < mptKey.length) { 106 | return new bytes(0); 107 | } 108 | 109 | rlpValue = node[1]; 110 | return rlpValue.toBytes(); 111 | } else { // extension 112 | // Sanity check 113 | if (i == stack.length - 1) { 114 | // shouldn't be at last level 115 | revert(); 116 | } 117 | 118 | if (!node[1].isList()) { 119 | // rlp(child) was at least 32 bytes. node[1] contains 120 | // Keccak256(rlp(child)). 121 | nodeHashHash = node[1].payloadKeccak256(); 122 | } else { 123 | // rlp(child) was less than 32 bytes. node[1] contains 124 | // rlp(child). 125 | nodeHashHash = node[1].rlpBytesKeccak256(); 126 | } 127 | } 128 | } else if (node.length == 17) { 129 | // Branch node 130 | 131 | if (mptKeyOffset != mptKey.length) { 132 | // we haven't consumed the entire path, so we need to look at a child 133 | uint8 nibble = uint8(mptKey[mptKeyOffset]); 134 | mptKeyOffset += 1; 135 | if (nibble >= 16) { 136 | // each element of the path has to be a nibble 137 | revert(); 138 | } 139 | 140 | if (_isEmptyBytesequence(node[nibble])) { 141 | // Sanity 142 | if (i != stack.length - 1) { 143 | // leaf node should be at last level 144 | revert(); 145 | } 146 | 147 | return new bytes(0); 148 | } else if (!node[nibble].isList()) { 149 | nodeHashHash = node[nibble].payloadKeccak256(); 150 | } else { 151 | nodeHashHash = node[nibble].rlpBytesKeccak256(); 152 | } 153 | } else { 154 | // we have consumed the entire mptKey, so we need to look at what's contained in this node. 155 | 156 | // Sanity 157 | if (i != stack.length - 1) { 158 | // should be at last level 159 | revert(); 160 | } 161 | 162 | return node[16].toBytes(); 163 | } 164 | } 165 | } 166 | } 167 | 168 | 169 | /// @dev Computes the hash of the Merkle-Patricia-Trie hash of the RLP item. 170 | /// Merkle-Patricia-Tries use a weird "hash function" that outputs 171 | /// *variable-length* hashes: If the item is shorter than 32 bytes, 172 | /// the MPT hash is the item. Otherwise, the MPT hash is the 173 | /// Keccak-256 hash of the item. 174 | /// The easiest way to compare variable-length byte sequences is 175 | /// to compare their Keccak-256 hashes. 176 | /// @param item The RLP item to be hashed. 177 | /// @return Keccak-256(MPT-hash(item)) 178 | function _mptHashHash(RLPReader.RLPItem memory item) private pure returns (bytes32) { 179 | if (item.len < 32) { 180 | return item.rlpBytesKeccak256(); 181 | } else { 182 | return keccak256(abi.encodePacked(item.rlpBytesKeccak256())); 183 | } 184 | } 185 | 186 | function _isEmptyBytesequence(RLPReader.RLPItem memory item) private pure returns (bool) { 187 | if (item.len != 1) { 188 | return false; 189 | } 190 | uint8 b; 191 | uint256 memPtr = item.memPtr; 192 | assembly { 193 | b := byte(0, mload(memPtr)) 194 | } 195 | return b == 0x80 /* empty byte string */; 196 | } 197 | 198 | 199 | function _merklePatriciaCompactDecode(bytes memory compact) private pure returns (bool isLeaf, bytes memory nibbles) { 200 | require(compact.length > 0); 201 | uint256 first_nibble = uint8(compact[0]) >> 4 & 0xF; 202 | uint256 skipNibbles; 203 | if (first_nibble == 0) { 204 | skipNibbles = 2; 205 | isLeaf = false; 206 | } else if (first_nibble == 1) { 207 | skipNibbles = 1; 208 | isLeaf = false; 209 | } else if (first_nibble == 2) { 210 | skipNibbles = 2; 211 | isLeaf = true; 212 | } else if (first_nibble == 3) { 213 | skipNibbles = 1; 214 | isLeaf = true; 215 | } else { 216 | // Not supposed to happen! 217 | revert(); 218 | } 219 | return (isLeaf, _decodeNibbles(compact, skipNibbles)); 220 | } 221 | 222 | 223 | function _decodeNibbles(bytes memory compact, uint256 skipNibbles) private pure returns (bytes memory nibbles) { 224 | require(compact.length > 0); 225 | 226 | uint256 length = compact.length * 2; 227 | require(skipNibbles <= length); 228 | length -= skipNibbles; 229 | 230 | nibbles = new bytes(length); 231 | uint256 nibblesLength = 0; 232 | 233 | for (uint256 i = skipNibbles; i < skipNibbles + length; i += 1) { 234 | if (i % 2 == 0) { 235 | nibbles[nibblesLength] = bytes1((uint8(compact[i/2]) >> 4) & 0xF); 236 | } else { 237 | nibbles[nibblesLength] = bytes1((uint8(compact[i/2]) >> 0) & 0xF); 238 | } 239 | nibblesLength += 1; 240 | } 241 | 242 | assert(nibblesLength == nibbles.length); 243 | } 244 | 245 | 246 | function _sharedPrefixLength(uint256 xsOffset, bytes memory xs, bytes memory ys) private pure returns (uint256) { 247 | uint256 i; 248 | for (i = 0; i + xsOffset < xs.length && i < ys.length; i++) { 249 | if (xs[i + xsOffset] != ys[i]) { 250 | return i; 251 | } 252 | } 253 | return i; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /contracts/SafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.6.0 <0.8.0; 4 | 5 | 6 | /** 7 | * @dev Wrappers over Solidity's arithmetic operations with added overflow 8 | * checks. 9 | * 10 | * Copied from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/math/SafeMath.sol 11 | */ 12 | library SafeMath { 13 | /** 14 | * @dev Returns the addition of two unsigned integers, reverting on 15 | * overflow. 16 | * 17 | * Counterpart to Solidity's `+` operator. 18 | * 19 | * Requirements: 20 | * 21 | * - Addition cannot overflow. 22 | */ 23 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 24 | uint256 c = a + b; 25 | require(c >= a, "SafeMath: addition overflow"); 26 | return c; 27 | } 28 | 29 | /** 30 | * @dev Returns the subtraction of two unsigned integers, reverting on 31 | * overflow (when the result is negative). 32 | * 33 | * Counterpart to Solidity's `-` operator. 34 | * 35 | * Requirements: 36 | * 37 | * - Subtraction cannot overflow. 38 | */ 39 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 40 | require(b <= a, "SafeMath: subtraction overflow"); 41 | return a - b; 42 | } 43 | 44 | /** 45 | * @dev Returns the multiplication of two unsigned integers, reverting on 46 | * overflow. 47 | * 48 | * Counterpart to Solidity's `*` operator. 49 | * 50 | * Requirements: 51 | * 52 | * - Multiplication cannot overflow. 53 | */ 54 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 55 | if (a == 0) return 0; 56 | uint256 c = a * b; 57 | require(c / a == b, "SafeMath: multiplication overflow"); 58 | return c; 59 | } 60 | 61 | /** 62 | * @dev Returns the integer division of two unsigned integers, reverting on 63 | * division by zero. The result is rounded towards zero. 64 | * 65 | * Counterpart to Solidity's `/` operator. Note: this function uses a 66 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 67 | * uses an invalid opcode to revert (consuming all remaining gas). 68 | * 69 | * Requirements: 70 | * 71 | * - The divisor cannot be zero. 72 | */ 73 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 74 | require(b > 0, "SafeMath: division by zero"); 75 | return a / b; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/StableSwapPriceHelper.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.8 2 | """ 3 | @author Curve.Fi 4 | @license Copyright (c) Curve.Fi, 2020 - all rights reserved 5 | """ 6 | 7 | # The following code has been copied with minimal modifications from 8 | # https://github.com/curvefi/curve-contract/blob/3fa3b6c/contracts/pools/steth/StableSwapSTETH.vy 9 | 10 | 11 | N_COINS: constant(int128) = 2 12 | FEE_DENOMINATOR: constant(uint256) = 10 ** 10 13 | A_PRECISION: constant(uint256) = 100 14 | 15 | 16 | @pure 17 | @internal 18 | def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: 19 | S: uint256 = 0 20 | Dprev: uint256 = 0 21 | 22 | for _x in xp: 23 | S += _x 24 | if S == 0: 25 | return 0 26 | 27 | D: uint256 = S 28 | Ann: uint256 = amp * N_COINS 29 | for _i in range(255): 30 | D_P: uint256 = D 31 | for _x in xp: 32 | D_P = D_P * D / (_x * N_COINS + 1) # +1 is to prevent /0 33 | Dprev = D 34 | D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) 35 | # Equality with the precision of 1 36 | if D > Dprev: 37 | if D - Dprev <= 1: 38 | return D 39 | else: 40 | if Dprev - D <= 1: 41 | return D 42 | # convergence typically occurs in 4 rounds or less, this should be unreachable! 43 | # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` 44 | raise 45 | 46 | 47 | @view 48 | @internal 49 | def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS], amp: uint256) -> uint256: 50 | # x in the input is converted to the same price/precision 51 | 52 | assert i != j # dev: same coin 53 | assert j >= 0 # dev: j below zero 54 | assert j < N_COINS # dev: j above N_COINS 55 | 56 | # should be unreachable, but good for safety 57 | assert i >= 0 58 | assert i < N_COINS 59 | 60 | D: uint256 = self.get_D(xp, amp) 61 | Ann: uint256 = amp * N_COINS 62 | c: uint256 = D 63 | S_: uint256 = 0 64 | _x: uint256 = 0 65 | y_prev: uint256 = 0 66 | 67 | for _i in range(N_COINS): 68 | if _i == i: 69 | _x = x 70 | elif _i != j: 71 | _x = xp[_i] 72 | else: 73 | continue 74 | S_ += _x 75 | c = c * D / (_x * N_COINS) 76 | c = c * D * A_PRECISION / (Ann * N_COINS) 77 | b: uint256 = S_ + D * A_PRECISION / Ann # - D 78 | y: uint256 = D 79 | for _i in range(255): 80 | y_prev = y 81 | y = (y*y + c) / (2 * y + b - D) 82 | # Equality with the precision of 1 83 | if y > y_prev: 84 | if y - y_prev <= 1: 85 | return y 86 | else: 87 | if y_prev - y <= 1: 88 | return y 89 | raise 90 | 91 | 92 | @view 93 | @external 94 | def get_dy(i: int128, j: int128, dx: uint256, xp: uint256[N_COINS], A: uint256, fee: uint256) -> uint256: 95 | x: uint256 = xp[i] + dx 96 | y: uint256 = self.get_y(i, j, x, xp, A) 97 | dy: uint256 = xp[j] - y - 1 98 | return dy - fee * dy / FEE_DENOMINATOR 99 | -------------------------------------------------------------------------------- /contracts/StableSwapStateOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.12; 4 | 5 | import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.5/contracts/RLPReader.sol"; 6 | import {StateProofVerifier as Verifier} from "./StateProofVerifier.sol"; 7 | import {SafeMath} from "./SafeMath.sol"; 8 | 9 | 10 | interface IPriceHelper { 11 | function get_dy( 12 | int128 i, 13 | int128 j, 14 | uint256 dx, 15 | uint256[2] memory xp, 16 | uint256 A, 17 | uint256 fee 18 | ) external pure returns (uint256); 19 | } 20 | 21 | 22 | interface IStableSwap { 23 | function fee() external view returns (uint256); 24 | function A_precise() external view returns (uint256); 25 | } 26 | 27 | 28 | /** 29 | * @title 30 | * A trustless oracle for the stETH/ETH Curve pool using Merkle Patricia 31 | * proofs of Ethereum state. 32 | * 33 | * @notice 34 | * The oracle currently assumes that the pool's fee and A (amplification 35 | * coefficient) values don't change between the time of proof generation 36 | * and submission. 37 | */ 38 | contract StableSwapStateOracle { 39 | using RLPReader for bytes; 40 | using RLPReader for RLPReader.RLPItem; 41 | using SafeMath for uint256; 42 | 43 | /** 44 | * @notice Logs the updated slot values of Curve pool and stETH contracts. 45 | */ 46 | event SlotValuesUpdated( 47 | uint256 timestamp, 48 | uint256 poolEthBalance, 49 | uint256 poolAdminEthBalance, 50 | uint256 poolAdminStethBalance, 51 | uint256 stethPoolShares, 52 | uint256 stethTotalShares, 53 | uint256 stethBeaconBalance, 54 | uint256 stethBufferedEther, 55 | uint256 stethDepositedValidators, 56 | uint256 stethBeaconValidators 57 | ); 58 | 59 | /** 60 | * @notice Logs the updated stETH and ETH pool balances and the calculated stETH/ETH price. 61 | */ 62 | event PriceUpdated( 63 | uint256 timestamp, 64 | uint256 etherBalance, 65 | uint256 stethBalance, 66 | uint256 stethPrice 67 | ); 68 | 69 | /** 70 | * @notice Logs the updated price update threshold percentage advised to offchain clients. 71 | */ 72 | event PriceUpdateThresholdChanged(uint256 threshold); 73 | 74 | /** 75 | * @notice 76 | * Logs the updated address having the right to change the advised price update threshold. 77 | */ 78 | event AdminChanged(address admin); 79 | 80 | 81 | /// @dev Reporting data that is more fresh than this number of blocks ago is prohibited 82 | uint256 constant public MIN_BLOCK_DELAY = 15; 83 | 84 | // Constants for offchain proof generation 85 | 86 | address constant public POOL_ADDRESS = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; 87 | address constant public STETH_ADDRESS = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; 88 | 89 | /// @dev keccak256(abi.encodePacked(uint256(1))) 90 | bytes32 constant public POOL_ADMIN_BALANCES_0_POS = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6; 91 | 92 | /// @dev bytes32(uint256(POOL_ADMIN_BALANCES_0_POS) + 1) 93 | bytes32 constant public POOL_ADMIN_BALANCES_1_POS = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7; 94 | 95 | /// @dev keccak256(abi.encodePacked(uint256(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022), uint256(0))) 96 | bytes32 constant public STETH_POOL_SHARES_POS = 0xae68078d7ee25b2b7bcb7d4b9fe9acf61f251fe08ff637df07889375d8385158; 97 | 98 | /// @dev keccak256("lido.StETH.totalShares") 99 | bytes32 constant public STETH_TOTAL_SHARES_POS = 0xe3b4b636e601189b5f4c6742edf2538ac12bb61ed03e6da26949d69838fa447e; 100 | 101 | /// @dev keccak256("lido.Lido.beaconBalance") 102 | bytes32 constant public STETH_BEACON_BALANCE_POS = 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483; 103 | 104 | /// @dev keccak256("lido.Lido.bufferedEther") 105 | bytes32 constant public STETH_BUFFERED_ETHER_POS = 0xed310af23f61f96daefbcd140b306c0bdbf8c178398299741687b90e794772b0; 106 | 107 | /// @dev keccak256("lido.Lido.depositedValidators") 108 | bytes32 constant public STETH_DEPOSITED_VALIDATORS_POS = 0xe6e35175eb53fc006520a2a9c3e9711a7c00de6ff2c32dd31df8c5a24cac1b5c; 109 | 110 | /// @dev keccak256("lido.Lido.beaconValidators") 111 | bytes32 constant public STETH_BEACON_VALIDATORS_POS = 0x9f70001d82b6ef54e9d3725b46581c3eb9ee3aa02b941b6aa54d678a9ca35b10; 112 | 113 | // Constants for onchain proof verification 114 | 115 | /// @dev keccak256(abi.encodePacked(POOL_ADDRESS)) 116 | bytes32 constant POOL_ADDRESS_HASH = 0xc70f76036d72b7bb865881e931082ea61bb4f13ec9faeb17f0591b18b6fafbd7; 117 | 118 | /// @dev keccak256(abi.encodePacked(STETH_ADDRESS)) 119 | bytes32 constant STETH_ADDRESS_HASH = 0x6c958a912fe86c83262fbd4973f6bd042cef76551aaf679968f98665979c35e7; 120 | 121 | /// @dev keccak256(abi.encodePacked(POOL_ADMIN_BALANCES_0_POS)) 122 | bytes32 constant POOL_ADMIN_BALANCES_0_HASH = 0xb5d9d894133a730aa651ef62d26b0ffa846233c74177a591a4a896adfda97d22; 123 | 124 | /// @dev keccak256(abi.encodePacked(POOL_ADMIN_BALANCES_1_POS) 125 | bytes32 constant POOL_ADMIN_BALANCES_1_HASH = 0xea7809e925a8989e20c901c4c1da82f0ba29b26797760d445a0ce4cf3c6fbd31; 126 | 127 | /// @dev keccak256(abi.encodePacked(STETH_POOL_SHARES_POS) 128 | bytes32 constant STETH_POOL_SHARES_HASH = 0xe841c8fb2710e169d6b63e1130fb8013d57558ced93619655add7aef8c60d4dc; 129 | 130 | /// @dev keccak256(abi.encodePacked(STETH_TOTAL_SHARES_POS) 131 | bytes32 constant STETH_TOTAL_SHARES_HASH = 0x4068b5716d4c00685289292c9cdc7e059e67159cd101476377efe51ba7ab8e9f; 132 | 133 | /// @dev keccak256(abi.encodePacked(STETH_BEACON_BALANCE_POS) 134 | bytes32 constant STETH_BEACON_BALANCE_HASH = 0xa6965d4729b36ed8b238f6ba55294196843f8be2850c5f63b6fb6d29181b50f8; 135 | 136 | /// @dev keccak256(abi.encodePacked(STETH_BUFFERED_ETHER_POS) 137 | bytes32 constant STETH_BUFFERED_ETHER_HASH = 0xa39079072910ef75f32ddc4f40104882abfc19580cc249c694e12b6de868ee1d; 138 | 139 | /// @dev keccak256(abi.encodePacked(STETH_DEPOSITED_VALIDATORS_POS) 140 | bytes32 constant STETH_DEPOSITED_VALIDATORS_HASH = 0x17216d3ffd8719eeee6d8052f7c1e6269bd92d2390d3e3fc4cde1f026e427fb3; 141 | 142 | /// @dev keccak256(abi.encodePacked(STETH_BEACON_VALIDATORS_POS) 143 | bytes32 constant STETH_BEACON_VALIDATORS_HASH = 0x6fd60d3960d8a32cbc1a708d6bf41bbce8152e61e72b2236d5e1ecede9c4cc72; 144 | 145 | uint256 constant internal STETH_DEPOSIT_SIZE = 32 ether; 146 | 147 | /** 148 | * @dev A helper contract for calculating stETH/ETH price from its stETH and ETH balances. 149 | */ 150 | IPriceHelper internal helper; 151 | 152 | /** 153 | * @notice The admin has the right to set the suggested price update threshold (see below). 154 | */ 155 | address public admin; 156 | 157 | /** 158 | * @notice 159 | * The price update threshold percentage advised to oracle clients. 160 | * Expressed in basis points: 10000 BP equal to 100%, 100 BP to 1%. 161 | * 162 | * @dev 163 | * If the current price in the pool differs less than this, the clients are advised to 164 | * skip updating the oracle. However, this threshold is not enforced, so clients are 165 | * free to update the oracle with any valid price. 166 | */ 167 | uint256 public priceUpdateThreshold; 168 | 169 | /** 170 | * @notice The timestamp of the proven pool state/price. 171 | */ 172 | uint256 public timestamp; 173 | 174 | /** 175 | * @notice The proven ETH balance of the pool. 176 | */ 177 | uint256 public etherBalance; 178 | 179 | /** 180 | * @notice The proven stETH balance of the pool. 181 | */ 182 | uint256 public stethBalance; 183 | 184 | /** 185 | * @notice The proven stETH/ETH price in the pool. 186 | */ 187 | uint256 public stethPrice; 188 | 189 | 190 | /** 191 | * @param _helper Address of the deployed instance of the StableSwapPriceHelper.vy contract. 192 | * @param _admin The address that has the right to set the suggested price update threshold. 193 | * @param _priceUpdateThreshold The initial value of the suggested price update threshold. 194 | * Expressed in basis points, 10000 BP corresponding to 100%. 195 | */ 196 | constructor(IPriceHelper _helper, address _admin, uint256 _priceUpdateThreshold) public { 197 | helper = _helper; 198 | _setAdmin(_admin); 199 | _setPriceUpdateThreshold(_priceUpdateThreshold); 200 | } 201 | 202 | 203 | /** 204 | * @notice Passes the right to set the suggested price update threshold to a new address. 205 | */ 206 | function setAdmin(address _admin) external { 207 | require(msg.sender == admin); 208 | _setAdmin(_admin); 209 | } 210 | 211 | 212 | /** 213 | * @notice Sets the suggested price update threshold. 214 | * 215 | * @param _priceUpdateThreshold The suggested price update threshold. 216 | * Expressed in basis points, 10000 BP corresponding to 100%. 217 | */ 218 | function setPriceUpdateThreshold(uint256 _priceUpdateThreshold) external { 219 | require(msg.sender == admin); 220 | _setPriceUpdateThreshold(_priceUpdateThreshold); 221 | } 222 | 223 | 224 | /** 225 | * @notice Returns a set of values used by the clients for proof generation. 226 | */ 227 | function getProofParams() external view returns ( 228 | address poolAddress, 229 | address stethAddress, 230 | bytes32 poolAdminEtherBalancePos, 231 | bytes32 poolAdminCoinBalancePos, 232 | bytes32 stethPoolSharesPos, 233 | bytes32 stethTotalSharesPos, 234 | bytes32 stethBeaconBalancePos, 235 | bytes32 stethBufferedEtherPos, 236 | bytes32 stethDepositedValidatorsPos, 237 | bytes32 stethBeaconValidatorsPos, 238 | uint256 advisedPriceUpdateThreshold 239 | ) { 240 | return ( 241 | POOL_ADDRESS, 242 | STETH_ADDRESS, 243 | POOL_ADMIN_BALANCES_0_POS, 244 | POOL_ADMIN_BALANCES_1_POS, 245 | STETH_POOL_SHARES_POS, 246 | STETH_TOTAL_SHARES_POS, 247 | STETH_BEACON_BALANCE_POS, 248 | STETH_BUFFERED_ETHER_POS, 249 | STETH_DEPOSITED_VALIDATORS_POS, 250 | STETH_BEACON_VALIDATORS_POS, 251 | priceUpdateThreshold 252 | ); 253 | } 254 | 255 | 256 | /** 257 | * @return _timestamp The timestamp of the proven pool state/price. 258 | * Will be zero in the case no state has been reported yet. 259 | * @return _etherBalance The proven ETH balance of the pool. 260 | * @return _stethBalance The proven stETH balance of the pool. 261 | * @return _stethPrice The proven stETH/ETH price in the pool. 262 | */ 263 | function getState() external view returns ( 264 | uint256 _timestamp, 265 | uint256 _etherBalance, 266 | uint256 _stethBalance, 267 | uint256 _stethPrice 268 | ) { 269 | return (timestamp, etherBalance, stethBalance, stethPrice); 270 | } 271 | 272 | 273 | /** 274 | * @notice Used by the offchain clients to submit the proof. 275 | * 276 | * @dev Reverts unless: 277 | * - the block the submitted data corresponds to is in the chain; 278 | * - the block is at least `MIN_BLOCK_DELAY` blocks old; 279 | * - all submitted proofs are valid. 280 | * 281 | * @param _blockHeaderRlpBytes RLP-encoded block header. 282 | * 283 | * @param _proofRlpBytes RLP-encoded list of Merkle Patricia proofs: 284 | * 1. proof of the Curve pool contract account; 285 | * 2. proof of the stETH contract account; 286 | * 3. proof of the `admin_balances[0]` slot of the Curve pool contract; 287 | * 4. proof of the `admin_balances[1]` slot of the Curve pool contract; 288 | * 5. proof of the `shares[0xDC24316b9AE028F1497c275EB9192a3Ea0f67022]` slot of stETH contract; 289 | * 6. proof of the `keccak256("lido.StETH.totalShares")` slot of stETH contract; 290 | * 7. proof of the `keccak256("lido.Lido.beaconBalance")` slot of stETH contract; 291 | * 8. proof of the `keccak256("lido.Lido.bufferedEther")` slot of stETH contract; 292 | * 9. proof of the `keccak256("lido.Lido.depositedValidators")` slot of stETH contract; 293 | * 10. proof of the `keccak256("lido.Lido.beaconValidators")` slot of stETH contract. 294 | */ 295 | function submitState(bytes memory _blockHeaderRlpBytes, bytes memory _proofRlpBytes) 296 | external 297 | { 298 | Verifier.BlockHeader memory blockHeader = Verifier.verifyBlockHeader(_blockHeaderRlpBytes); 299 | 300 | { 301 | uint256 currentBlock = block.number; 302 | // ensure block finality 303 | require( 304 | currentBlock > blockHeader.number && 305 | currentBlock - blockHeader.number >= MIN_BLOCK_DELAY, 306 | "block too fresh" 307 | ); 308 | } 309 | 310 | require(blockHeader.timestamp > timestamp, "stale data"); 311 | 312 | RLPReader.RLPItem[] memory proofs = _proofRlpBytes.toRlpItem().toList(); 313 | require(proofs.length == 10, "total proofs"); 314 | 315 | Verifier.Account memory accountPool = Verifier.extractAccountFromProof( 316 | POOL_ADDRESS_HASH, 317 | blockHeader.stateRootHash, 318 | proofs[0].toList() 319 | ); 320 | 321 | require(accountPool.exists, "accountPool"); 322 | 323 | Verifier.Account memory accountSteth = Verifier.extractAccountFromProof( 324 | STETH_ADDRESS_HASH, 325 | blockHeader.stateRootHash, 326 | proofs[1].toList() 327 | ); 328 | 329 | require(accountSteth.exists, "accountSteth"); 330 | 331 | Verifier.SlotValue memory slotPoolAdminBalances0 = Verifier.extractSlotValueFromProof( 332 | POOL_ADMIN_BALANCES_0_HASH, 333 | accountPool.storageRoot, 334 | proofs[2].toList() 335 | ); 336 | 337 | require(slotPoolAdminBalances0.exists, "adminBalances0"); 338 | 339 | Verifier.SlotValue memory slotPoolAdminBalances1 = Verifier.extractSlotValueFromProof( 340 | POOL_ADMIN_BALANCES_1_HASH, 341 | accountPool.storageRoot, 342 | proofs[3].toList() 343 | ); 344 | 345 | require(slotPoolAdminBalances1.exists, "adminBalances1"); 346 | 347 | Verifier.SlotValue memory slotStethPoolShares = Verifier.extractSlotValueFromProof( 348 | STETH_POOL_SHARES_HASH, 349 | accountSteth.storageRoot, 350 | proofs[4].toList() 351 | ); 352 | 353 | require(slotStethPoolShares.exists, "poolShares"); 354 | 355 | Verifier.SlotValue memory slotStethTotalShares = Verifier.extractSlotValueFromProof( 356 | STETH_TOTAL_SHARES_HASH, 357 | accountSteth.storageRoot, 358 | proofs[5].toList() 359 | ); 360 | 361 | require(slotStethTotalShares.exists, "totalShares"); 362 | 363 | Verifier.SlotValue memory slotStethBeaconBalance = Verifier.extractSlotValueFromProof( 364 | STETH_BEACON_BALANCE_HASH, 365 | accountSteth.storageRoot, 366 | proofs[6].toList() 367 | ); 368 | 369 | require(slotStethBeaconBalance.exists, "beaconBalance"); 370 | 371 | Verifier.SlotValue memory slotStethBufferedEther = Verifier.extractSlotValueFromProof( 372 | STETH_BUFFERED_ETHER_HASH, 373 | accountSteth.storageRoot, 374 | proofs[7].toList() 375 | ); 376 | 377 | require(slotStethBufferedEther.exists, "bufferedEther"); 378 | 379 | Verifier.SlotValue memory slotStethDepositedValidators = Verifier.extractSlotValueFromProof( 380 | STETH_DEPOSITED_VALIDATORS_HASH, 381 | accountSteth.storageRoot, 382 | proofs[8].toList() 383 | ); 384 | 385 | require(slotStethDepositedValidators.exists, "depositedValidators"); 386 | 387 | Verifier.SlotValue memory slotStethBeaconValidators = Verifier.extractSlotValueFromProof( 388 | STETH_BEACON_VALIDATORS_HASH, 389 | accountSteth.storageRoot, 390 | proofs[9].toList() 391 | ); 392 | 393 | require(slotStethBeaconValidators.exists, "beaconValidators"); 394 | 395 | emit SlotValuesUpdated( 396 | blockHeader.timestamp, 397 | accountPool.balance, 398 | slotPoolAdminBalances0.value, 399 | slotPoolAdminBalances1.value, 400 | slotStethPoolShares.value, 401 | slotStethTotalShares.value, 402 | slotStethBeaconBalance.value, 403 | slotStethBufferedEther.value, 404 | slotStethDepositedValidators.value, 405 | slotStethBeaconValidators.value 406 | ); 407 | 408 | uint256 newEtherBalance = accountPool.balance.sub(slotPoolAdminBalances0.value); 409 | uint256 newStethBalance = _getStethBalanceByShares( 410 | slotStethPoolShares.value, 411 | slotStethTotalShares.value, 412 | slotStethBeaconBalance.value, 413 | slotStethBufferedEther.value, 414 | slotStethDepositedValidators.value, 415 | slotStethBeaconValidators.value 416 | ).sub(slotPoolAdminBalances1.value); 417 | 418 | uint256 newStethPrice = _calcPrice(newEtherBalance, newStethBalance); 419 | 420 | timestamp = blockHeader.timestamp; 421 | etherBalance = newEtherBalance; 422 | stethBalance = newStethBalance; 423 | stethPrice = newStethPrice; 424 | 425 | emit PriceUpdated(blockHeader.timestamp, newEtherBalance, newStethBalance, newStethPrice); 426 | } 427 | 428 | 429 | /** 430 | * @dev Given the values of stETH smart contract slots, calculates the amount of stETH owned 431 | * by the Curve pool by reproducing calculations performed in the stETH contract. 432 | */ 433 | function _getStethBalanceByShares( 434 | uint256 _shares, 435 | uint256 _totalShares, 436 | uint256 _beaconBalance, 437 | uint256 _bufferedEther, 438 | uint256 _depositedValidators, 439 | uint256 _beaconValidators 440 | ) 441 | internal pure returns (uint256) 442 | { 443 | // https://github.com/lidofinance/lido-dao/blob/v1.0.0/contracts/0.4.24/StETH.sol#L283 444 | // https://github.com/lidofinance/lido-dao/blob/v1.0.0/contracts/0.4.24/Lido.sol#L719 445 | // https://github.com/lidofinance/lido-dao/blob/v1.0.0/contracts/0.4.24/Lido.sol#L706 446 | if (_totalShares == 0) { 447 | return 0; 448 | } 449 | uint256 transientBalance = _depositedValidators.sub(_beaconValidators).mul(STETH_DEPOSIT_SIZE); 450 | uint256 totalPooledEther = _bufferedEther.add(_beaconBalance).add(transientBalance); 451 | return _shares.mul(totalPooledEther).div(_totalShares); 452 | } 453 | 454 | 455 | /** 456 | * @dev Given the ETH and stETH balances of the Curve pool, calculates the corresponding 457 | * stETH/ETH price by reproducing calculations performed in the pool contract. 458 | */ 459 | function _calcPrice(uint256 _etherBalance, uint256 _stethBalance) internal view returns (uint256) { 460 | uint256 A = IStableSwap(POOL_ADDRESS).A_precise(); 461 | uint256 fee = IStableSwap(POOL_ADDRESS).fee(); 462 | return helper.get_dy(1, 0, 10**18, [_etherBalance, _stethBalance], A, fee); 463 | } 464 | 465 | 466 | function _setPriceUpdateThreshold(uint256 _priceUpdateThreshold) internal { 467 | require(_priceUpdateThreshold <= 10000); 468 | priceUpdateThreshold = _priceUpdateThreshold; 469 | emit PriceUpdateThresholdChanged(_priceUpdateThreshold); 470 | } 471 | 472 | 473 | function _setAdmin(address _admin) internal { 474 | require(_admin != address(0)); 475 | require(_admin != admin); 476 | admin = _admin; 477 | emit AdminChanged(_admin); 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /contracts/StateProofVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.12; 4 | 5 | import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.5/contracts/RLPReader.sol"; 6 | import {MerklePatriciaProofVerifier} from "./MerklePatriciaProofVerifier.sol"; 7 | 8 | 9 | /** 10 | * @title A helper library for verification of Merkle Patricia account and state proofs. 11 | */ 12 | library StateProofVerifier { 13 | using RLPReader for RLPReader.RLPItem; 14 | using RLPReader for bytes; 15 | 16 | uint256 constant HEADER_STATE_ROOT_INDEX = 3; 17 | uint256 constant HEADER_NUMBER_INDEX = 8; 18 | uint256 constant HEADER_TIMESTAMP_INDEX = 11; 19 | 20 | struct BlockHeader { 21 | bytes32 hash; 22 | bytes32 stateRootHash; 23 | uint256 number; 24 | uint256 timestamp; 25 | } 26 | 27 | struct Account { 28 | bool exists; 29 | uint256 nonce; 30 | uint256 balance; 31 | bytes32 storageRoot; 32 | bytes32 codeHash; 33 | } 34 | 35 | struct SlotValue { 36 | bool exists; 37 | uint256 value; 38 | } 39 | 40 | 41 | /** 42 | * @notice Parses block header and verifies its presence onchain within the latest 256 blocks. 43 | * @param _headerRlpBytes RLP-encoded block header. 44 | */ 45 | function verifyBlockHeader(bytes memory _headerRlpBytes) 46 | internal view returns (BlockHeader memory) 47 | { 48 | BlockHeader memory header = parseBlockHeader(_headerRlpBytes); 49 | // ensure that the block is actually in the blockchain 50 | require(header.hash == blockhash(header.number), "blockhash mismatch"); 51 | return header; 52 | } 53 | 54 | 55 | /** 56 | * @notice Parses RLP-encoded block header. 57 | * @param _headerRlpBytes RLP-encoded block header. 58 | */ 59 | function parseBlockHeader(bytes memory _headerRlpBytes) 60 | internal pure returns (BlockHeader memory) 61 | { 62 | BlockHeader memory result; 63 | RLPReader.RLPItem[] memory headerFields = _headerRlpBytes.toRlpItem().toList(); 64 | 65 | require(headerFields.length > HEADER_TIMESTAMP_INDEX); 66 | 67 | result.stateRootHash = bytes32(headerFields[HEADER_STATE_ROOT_INDEX].toUint()); 68 | result.number = headerFields[HEADER_NUMBER_INDEX].toUint(); 69 | result.timestamp = headerFields[HEADER_TIMESTAMP_INDEX].toUint(); 70 | result.hash = keccak256(_headerRlpBytes); 71 | 72 | return result; 73 | } 74 | 75 | 76 | /** 77 | * @notice Verifies Merkle Patricia proof of an account and extracts the account fields. 78 | * 79 | * @param _addressHash Keccak256 hash of the address corresponding to the account. 80 | * @param _stateRootHash MPT root hash of the Ethereum state trie. 81 | */ 82 | function extractAccountFromProof( 83 | bytes32 _addressHash, // keccak256(abi.encodePacked(address)) 84 | bytes32 _stateRootHash, 85 | RLPReader.RLPItem[] memory _proof 86 | ) 87 | internal pure returns (Account memory) 88 | { 89 | bytes memory acctRlpBytes = MerklePatriciaProofVerifier.extractProofValue( 90 | _stateRootHash, 91 | abi.encodePacked(_addressHash), 92 | _proof 93 | ); 94 | 95 | Account memory account; 96 | 97 | if (acctRlpBytes.length == 0) { 98 | return account; 99 | } 100 | 101 | RLPReader.RLPItem[] memory acctFields = acctRlpBytes.toRlpItem().toList(); 102 | require(acctFields.length == 4); 103 | 104 | account.exists = true; 105 | account.nonce = acctFields[0].toUint(); 106 | account.balance = acctFields[1].toUint(); 107 | account.storageRoot = bytes32(acctFields[2].toUint()); 108 | account.codeHash = bytes32(acctFields[3].toUint()); 109 | 110 | return account; 111 | } 112 | 113 | 114 | /** 115 | * @notice Verifies Merkle Patricia proof of a slot and extracts the slot's value. 116 | * 117 | * @param _slotHash Keccak256 hash of the slot position. 118 | * @param _storageRootHash MPT root hash of the account's storage trie. 119 | */ 120 | function extractSlotValueFromProof( 121 | bytes32 _slotHash, 122 | bytes32 _storageRootHash, 123 | RLPReader.RLPItem[] memory _proof 124 | ) 125 | internal pure returns (SlotValue memory) 126 | { 127 | bytes memory valueRlpBytes = MerklePatriciaProofVerifier.extractProofValue( 128 | _storageRootHash, 129 | abi.encodePacked(_slotHash), 130 | _proof 131 | ); 132 | 133 | SlotValue memory value; 134 | 135 | if (valueRlpBytes.length != 0) { 136 | value.exists = true; 137 | value.value = valueRlpBytes.toRlpItem().toUint(); 138 | } 139 | 140 | return value; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /interfaces/StableSwapStateOracle.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "name": "_helper", 6 | "type": "address" 7 | }, 8 | { 9 | "name": "_admin", 10 | "type": "address" 11 | }, 12 | { 13 | "name": "_priceUpdateThreshold", 14 | "type": "uint256" 15 | } 16 | ], 17 | "stateMutability": "nonpayable", 18 | "type": "constructor", 19 | "name": "constructor" 20 | }, 21 | { 22 | "anonymous": false, 23 | "inputs": [ 24 | { 25 | "indexed": false, 26 | "name": "admin", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "AdminChanged", 31 | "type": "event" 32 | }, 33 | { 34 | "anonymous": false, 35 | "inputs": [ 36 | { 37 | "indexed": false, 38 | "name": "threshold", 39 | "type": "uint256" 40 | } 41 | ], 42 | "name": "PriceUpdateThresholdChanged", 43 | "type": "event" 44 | }, 45 | { 46 | "anonymous": false, 47 | "inputs": [ 48 | { 49 | "indexed": false, 50 | "name": "timestamp", 51 | "type": "uint256" 52 | }, 53 | { 54 | "indexed": false, 55 | "name": "etherBalance", 56 | "type": "uint256" 57 | }, 58 | { 59 | "indexed": false, 60 | "name": "stethBalance", 61 | "type": "uint256" 62 | }, 63 | { 64 | "indexed": false, 65 | "name": "stethPrice", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "PriceUpdated", 70 | "type": "event" 71 | }, 72 | { 73 | "anonymous": false, 74 | "inputs": [ 75 | { 76 | "indexed": false, 77 | "name": "timestamp", 78 | "type": "uint256" 79 | }, 80 | { 81 | "indexed": false, 82 | "name": "poolEthBalance", 83 | "type": "uint256" 84 | }, 85 | { 86 | "indexed": false, 87 | "name": "poolAdminEthBalance", 88 | "type": "uint256" 89 | }, 90 | { 91 | "indexed": false, 92 | "name": "poolAdminStethBalance", 93 | "type": "uint256" 94 | }, 95 | { 96 | "indexed": false, 97 | "name": "stethPoolShares", 98 | "type": "uint256" 99 | }, 100 | { 101 | "indexed": false, 102 | "name": "stethTotalShares", 103 | "type": "uint256" 104 | }, 105 | { 106 | "indexed": false, 107 | "name": "stethBeaconBalance", 108 | "type": "uint256" 109 | }, 110 | { 111 | "indexed": false, 112 | "name": "stethBufferedEther", 113 | "type": "uint256" 114 | }, 115 | { 116 | "indexed": false, 117 | "name": "stethDepositedValidators", 118 | "type": "uint256" 119 | }, 120 | { 121 | "indexed": false, 122 | "name": "stethBeaconValidators", 123 | "type": "uint256" 124 | } 125 | ], 126 | "name": "SlotValuesUpdated", 127 | "type": "event" 128 | }, 129 | { 130 | "inputs": [], 131 | "name": "MIN_BLOCK_DELAY", 132 | "outputs": [ 133 | { 134 | "name": "", 135 | "type": "uint256" 136 | } 137 | ], 138 | "stateMutability": "view", 139 | "type": "function" 140 | }, 141 | { 142 | "inputs": [], 143 | "name": "POOL_ADDRESS", 144 | "outputs": [ 145 | { 146 | "name": "", 147 | "type": "address" 148 | } 149 | ], 150 | "stateMutability": "view", 151 | "type": "function" 152 | }, 153 | { 154 | "inputs": [], 155 | "name": "POOL_ADMIN_BALANCES_0_POS", 156 | "outputs": [ 157 | { 158 | "name": "", 159 | "type": "bytes32" 160 | } 161 | ], 162 | "stateMutability": "view", 163 | "type": "function" 164 | }, 165 | { 166 | "inputs": [], 167 | "name": "POOL_ADMIN_BALANCES_1_POS", 168 | "outputs": [ 169 | { 170 | "name": "", 171 | "type": "bytes32" 172 | } 173 | ], 174 | "stateMutability": "view", 175 | "type": "function" 176 | }, 177 | { 178 | "inputs": [], 179 | "name": "STETH_ADDRESS", 180 | "outputs": [ 181 | { 182 | "name": "", 183 | "type": "address" 184 | } 185 | ], 186 | "stateMutability": "view", 187 | "type": "function" 188 | }, 189 | { 190 | "inputs": [], 191 | "name": "STETH_BEACON_BALANCE_POS", 192 | "outputs": [ 193 | { 194 | "name": "", 195 | "type": "bytes32" 196 | } 197 | ], 198 | "stateMutability": "view", 199 | "type": "function" 200 | }, 201 | { 202 | "inputs": [], 203 | "name": "STETH_BEACON_VALIDATORS_POS", 204 | "outputs": [ 205 | { 206 | "name": "", 207 | "type": "bytes32" 208 | } 209 | ], 210 | "stateMutability": "view", 211 | "type": "function" 212 | }, 213 | { 214 | "inputs": [], 215 | "name": "STETH_BUFFERED_ETHER_POS", 216 | "outputs": [ 217 | { 218 | "name": "", 219 | "type": "bytes32" 220 | } 221 | ], 222 | "stateMutability": "view", 223 | "type": "function" 224 | }, 225 | { 226 | "inputs": [], 227 | "name": "STETH_DEPOSITED_VALIDATORS_POS", 228 | "outputs": [ 229 | { 230 | "name": "", 231 | "type": "bytes32" 232 | } 233 | ], 234 | "stateMutability": "view", 235 | "type": "function" 236 | }, 237 | { 238 | "inputs": [], 239 | "name": "STETH_POOL_SHARES_POS", 240 | "outputs": [ 241 | { 242 | "name": "", 243 | "type": "bytes32" 244 | } 245 | ], 246 | "stateMutability": "view", 247 | "type": "function" 248 | }, 249 | { 250 | "inputs": [], 251 | "name": "STETH_TOTAL_SHARES_POS", 252 | "outputs": [ 253 | { 254 | "name": "", 255 | "type": "bytes32" 256 | } 257 | ], 258 | "stateMutability": "view", 259 | "type": "function" 260 | }, 261 | { 262 | "inputs": [], 263 | "name": "admin", 264 | "outputs": [ 265 | { 266 | "name": "", 267 | "type": "address" 268 | } 269 | ], 270 | "stateMutability": "view", 271 | "type": "function" 272 | }, 273 | { 274 | "inputs": [], 275 | "name": "etherBalance", 276 | "outputs": [ 277 | { 278 | "name": "", 279 | "type": "uint256" 280 | } 281 | ], 282 | "stateMutability": "view", 283 | "type": "function" 284 | }, 285 | { 286 | "inputs": [], 287 | "name": "getProofParams", 288 | "outputs": [ 289 | { 290 | "name": "poolAddress", 291 | "type": "address" 292 | }, 293 | { 294 | "name": "stethAddress", 295 | "type": "address" 296 | }, 297 | { 298 | "name": "poolAdminEtherBalancePos", 299 | "type": "bytes32" 300 | }, 301 | { 302 | "name": "poolAdminCoinBalancePos", 303 | "type": "bytes32" 304 | }, 305 | { 306 | "name": "stethPoolSharesPos", 307 | "type": "bytes32" 308 | }, 309 | { 310 | "name": "stethTotalSharesPos", 311 | "type": "bytes32" 312 | }, 313 | { 314 | "name": "stethBeaconBalancePos", 315 | "type": "bytes32" 316 | }, 317 | { 318 | "name": "stethBufferedEtherPos", 319 | "type": "bytes32" 320 | }, 321 | { 322 | "name": "stethDepositedValidatorsPos", 323 | "type": "bytes32" 324 | }, 325 | { 326 | "name": "stethBeaconValidatorsPos", 327 | "type": "bytes32" 328 | }, 329 | { 330 | "name": "advisedPriceUpdateThreshold", 331 | "type": "uint256" 332 | } 333 | ], 334 | "stateMutability": "view", 335 | "type": "function" 336 | }, 337 | { 338 | "inputs": [], 339 | "name": "getState", 340 | "outputs": [ 341 | { 342 | "name": "_timestamp", 343 | "type": "uint256" 344 | }, 345 | { 346 | "name": "_etherBalance", 347 | "type": "uint256" 348 | }, 349 | { 350 | "name": "_stethBalance", 351 | "type": "uint256" 352 | }, 353 | { 354 | "name": "_stethPrice", 355 | "type": "uint256" 356 | } 357 | ], 358 | "stateMutability": "view", 359 | "type": "function" 360 | }, 361 | { 362 | "inputs": [], 363 | "name": "priceUpdateThreshold", 364 | "outputs": [ 365 | { 366 | "name": "", 367 | "type": "uint256" 368 | } 369 | ], 370 | "stateMutability": "view", 371 | "type": "function" 372 | }, 373 | { 374 | "inputs": [ 375 | { 376 | "name": "_admin", 377 | "type": "address" 378 | } 379 | ], 380 | "name": "setAdmin", 381 | "outputs": [], 382 | "stateMutability": "nonpayable", 383 | "type": "function" 384 | }, 385 | { 386 | "inputs": [ 387 | { 388 | "name": "_priceUpdateThreshold", 389 | "type": "uint256" 390 | } 391 | ], 392 | "name": "setPriceUpdateThreshold", 393 | "outputs": [], 394 | "stateMutability": "nonpayable", 395 | "type": "function" 396 | }, 397 | { 398 | "inputs": [], 399 | "name": "stethBalance", 400 | "outputs": [ 401 | { 402 | "name": "", 403 | "type": "uint256" 404 | } 405 | ], 406 | "stateMutability": "view", 407 | "type": "function" 408 | }, 409 | { 410 | "inputs": [], 411 | "name": "stethPrice", 412 | "outputs": [ 413 | { 414 | "name": "", 415 | "type": "uint256" 416 | } 417 | ], 418 | "stateMutability": "view", 419 | "type": "function" 420 | }, 421 | { 422 | "inputs": [ 423 | { 424 | "name": "_blockHeaderRlpBytes", 425 | "type": "bytes" 426 | }, 427 | { 428 | "name": "_proofRlpBytes", 429 | "type": "bytes" 430 | } 431 | ], 432 | "name": "submitState", 433 | "outputs": [], 434 | "stateMutability": "nonpayable", 435 | "type": "function" 436 | }, 437 | { 438 | "inputs": [], 439 | "name": "timestamp", 440 | "outputs": [ 441 | { 442 | "name": "", 443 | "type": "uint256" 444 | } 445 | ], 446 | "stateMutability": "view", 447 | "type": "function" 448 | } 449 | ] 450 | -------------------------------------------------------------------------------- /offchain/generate_steth_price_proof.py: -------------------------------------------------------------------------------- 1 | import math 2 | import argparse 3 | import json 4 | import sys 5 | import os 6 | from pprint import pprint 7 | from getpass import getpass 8 | 9 | from web3 import Web3 10 | from web3.logs import DISCARD 11 | import requests 12 | import rlp 13 | 14 | from state_proof import request_block_header, request_account_proof 15 | 16 | 17 | ORACLE_CONTRACT_ADDRESS = '0x602C71e4DAC47a042Ee7f46E0aee17F94A3bA0B6' 18 | 19 | 20 | def main(): 21 | parser = argparse.ArgumentParser( 22 | description="Patricia Merkle Trie Proof Generating Tool", 23 | formatter_class=argparse.RawTextHelpFormatter) 24 | 25 | parser.add_argument("-b", "--block-number", 26 | help="Block number, defaults to `latest - 15`") 27 | 28 | parser.add_argument("-r", "--rpc", 29 | default="http://localhost:8545", 30 | help="URL of a full node RPC endpoint, e.g. http://localhost:8545") 31 | 32 | parser.add_argument("-k", "--keyfile", 33 | help="Send transaction and sign it using the keyfile at the provided path") 34 | 35 | parser.add_argument("-g", "--gas-price", 36 | help="Use the specified gas price") 37 | 38 | parser.add_argument("--contract", 39 | default=ORACLE_CONTRACT_ADDRESS, 40 | help="Oracle contract address") 41 | 42 | args = parser.parse_args() 43 | w3 = Web3(Web3.HTTPProvider(args.rpc)) 44 | 45 | block_number = args.block_number if args.block_number is not None else w3.eth.block_number - 15 46 | oracle_contract = get_oracle_contract(args.contract, w3) 47 | params = oracle_contract.functions.getProofParams().call() 48 | 49 | (block_number, block_header, pool_acct_proof, steth_acct_proof, 50 | pool_storage_proofs, steth_storage_proofs) = generate_proof_data( 51 | rpc_endpoint=args.rpc, 52 | block_number=block_number, 53 | pool_address=params[0], 54 | steth_address=params[1], 55 | pool_slots=params[2:4], 56 | steth_slots=params[4:10], 57 | ) 58 | 59 | header_blob = rlp.encode(block_header) 60 | 61 | proofs_blob = rlp.encode( 62 | [pool_acct_proof, steth_acct_proof] + 63 | pool_storage_proofs + 64 | steth_storage_proofs 65 | ) 66 | 67 | print(f"\nBlock number: {block_number}\n") 68 | print("Header RLP bytes:\n") 69 | print(f"0x{header_blob.hex()}\n") 70 | print("Proofs list RLP bytes:\n") 71 | print(f"0x{proofs_blob.hex()}\n") 72 | 73 | if args.keyfile is None: 74 | return 75 | 76 | print(f"Will send transaction calling `submitState` on {oracle_contract.address}") 77 | 78 | private_key = load_private_key(args.keyfile, w3) 79 | account = w3.eth.account.privateKeyToAccount(private_key) 80 | nonce = w3.eth.get_transaction_count(account.address) 81 | gas_price = int(args.gas_price) if args.gas_price is not None else w3.eth.gas_price 82 | 83 | tx = oracle_contract.functions.submitState(header_blob, proofs_blob).buildTransaction({ 84 | 'gasPrice': gas_price, 85 | 'gas': 3000000, 86 | 'nonce': nonce, 87 | }) 88 | 89 | signed = w3.eth.account.sign_transaction(tx, private_key) 90 | 91 | print(f"Sending transaction from {account.address}, gas price {gas_price}...") 92 | 93 | tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction) 94 | 95 | print(f"Transaction sent: {tx_hash.hex()}\nWaiting for inclusion...\n") 96 | 97 | receipt = w3.eth.waitForTransactionReceipt(tx_hash) 98 | pprint(dict(receipt)) 99 | 100 | if int(receipt['status']) != 1: 101 | print("\nTransaction failed") 102 | else: 103 | print_event("SlotValuesUpdated", receipt, oracle_contract) 104 | print_event("PriceUpdated", receipt, oracle_contract) 105 | 106 | 107 | def get_oracle_contract(address, w3): 108 | dir = os.path.dirname(__file__) 109 | interface_path = os.path.join(dir, '../interfaces/StableSwapStateOracle.json') 110 | with open(interface_path) as abi_file: 111 | abi = json.load(abi_file) 112 | return w3.eth.contract(address=address, abi=abi) 113 | 114 | 115 | def load_private_key(path, w3): 116 | with open(path) as keyfile: 117 | encrypted_key = keyfile.read() 118 | password = getpass() 119 | return w3.eth.account.decrypt(encrypted_key, password) 120 | 121 | 122 | def print_event(name, receipt, contract): 123 | # https://github.com/ethereum/web3.py/issues/1738 124 | logs = contract.events[name]().processReceipt(receipt, DISCARD) 125 | if len(logs) != 0: 126 | print(f"\n{name} event:") 127 | for key, value in logs[0]['args'].items(): 128 | print(f" {key}: {value}") 129 | else: 130 | print(f"\nNo {name} event generated") 131 | 132 | 133 | def generate_proof_data( 134 | rpc_endpoint, 135 | block_number, 136 | pool_address, 137 | steth_address, 138 | pool_slots, 139 | steth_slots, 140 | ): 141 | block_number = \ 142 | block_number if block_number == "latest" or block_number == "earliest" \ 143 | else hex(int(block_number)) 144 | 145 | (block_number, block_header) = request_block_header( 146 | rpc_endpoint=rpc_endpoint, 147 | block_number=block_number, 148 | ) 149 | 150 | (pool_acct_proof, pool_storage_proofs) = request_account_proof( 151 | rpc_endpoint=rpc_endpoint, 152 | block_number=block_number, 153 | address=pool_address, 154 | slots=pool_slots, 155 | ) 156 | 157 | (steth_acct_proof, steth_storage_proofs) = request_account_proof( 158 | rpc_endpoint=rpc_endpoint, 159 | block_number=block_number, 160 | address=steth_address, 161 | slots=steth_slots, 162 | ) 163 | 164 | return ( 165 | block_number, 166 | block_header, 167 | pool_acct_proof, 168 | steth_acct_proof, 169 | pool_storage_proofs, 170 | steth_storage_proofs, 171 | ) 172 | 173 | 174 | if __name__ == "__main__": 175 | main() 176 | exit(0) 177 | -------------------------------------------------------------------------------- /offchain/state_proof.py: -------------------------------------------------------------------------------- 1 | import math 2 | import json 3 | 4 | import requests 5 | import rlp 6 | 7 | from utils import normalize_bytes, normalize_address, normalize_int, decode_hex, to_0x_string 8 | 9 | 10 | BLOCK_HEADER_FIELDS = [ 11 | "parentHash", "sha3Uncles", "miner", "stateRoot", "transactionsRoot", 12 | "receiptsRoot", "logsBloom", "difficulty", "number", "gasLimit", 13 | "gasUsed", "timestamp", "extraData", "mixHash", "nonce" 14 | ] 15 | 16 | 17 | def request_block_header(rpc_endpoint, block_number): 18 | r = requests.post(rpc_endpoint, json={ 19 | "jsonrpc": "2.0", 20 | "method": "eth_getBlockByNumber", 21 | "params": [block_number, True], 22 | "id": 1, 23 | }) 24 | 25 | block_dict = get_json_rpc_result(r) 26 | block_number = normalize_int(block_dict["number"]) 27 | block_header_fields = [normalize_bytes(block_dict[f]) for f in BLOCK_HEADER_FIELDS] 28 | 29 | return (block_number, block_header_fields) 30 | 31 | 32 | def request_account_proof(rpc_endpoint, block_number, address, slots): 33 | hex_slots = [to_0x_string(s) for s in slots] 34 | 35 | r = requests.post(rpc_endpoint, json={ 36 | "jsonrpc": "2.0", 37 | "method": "eth_getProof", 38 | "params": [address.lower(), hex_slots, to_0x_string(block_number)], 39 | "id": 1, 40 | }) 41 | 42 | result = get_json_rpc_result(r) 43 | 44 | account_proof = decode_rpc_proof(result["accountProof"]) 45 | storage_proofs = [ 46 | decode_rpc_proof(slot_data["proof"]) for slot_data in result["storageProof"] 47 | ] 48 | 49 | return (account_proof, storage_proofs) 50 | 51 | 52 | def decode_rpc_proof(proof_data): 53 | return [rlp.decode(decode_hex(node)) for node in proof_data] 54 | 55 | 56 | def get_json_rpc_result(response): 57 | response.raise_for_status() 58 | json_dict = response.json() 59 | if "error" in json_dict: 60 | raise requests.RequestException( 61 | f"RPC error { json_dict['error']['code'] }: { json_dict['error']['message'] }", 62 | response=response 63 | ) 64 | return json_dict["result"] 65 | -------------------------------------------------------------------------------- /offchain/utils.py: -------------------------------------------------------------------------------- 1 | # Partially taken from: https://github.com/ethereum/pyethereum/blob/b704a5c/ethereum/utils.py 2 | 3 | import math 4 | 5 | from eth_utils import decode_hex, to_canonical_address, to_bytes, to_int, to_hex 6 | 7 | 8 | def normalize_bytes(x): 9 | return to_bytes(hexstr=x) if isinstance(x, str) else to_bytes(x) 10 | 11 | 12 | def normalize_address(x): 13 | return to_canonical_address(x) 14 | 15 | 16 | def normalize_int(x): 17 | if isinstance(x, str) and not x.startswith("0x"): 18 | x = int(x) 19 | return to_int(hexstr=x) if isinstance(x, str) else to_int(x) 20 | 21 | 22 | def to_0x_string(x): 23 | if isinstance(x, str) and not x.startswith("0x"): 24 | x = int(x) 25 | return to_hex(x) 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eth-utils==1.10.0 2 | requests==2.21.0 3 | rlp==2.0.1 4 | web3==5.17.0 5 | --------------------------------------------------------------------------------