From 0b11a59e5bc80cb246bcadc779203bded6839840 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Tue, 29 Oct 2024 14:14:43 +0300 Subject: [PATCH 1/7] feat: add scrvusd oracle --- contracts/oracles/OptimismBlockHashOracle.vy | 81 +++++++ contracts/oracles/ScrvusdOracle.vy | 219 +++++++++++++++++++ contracts/provers/ScrvusdProver.sol | 101 +++++++++ scripts/scrvusd_keeper.py | 103 +++++++++ scripts/submit_scrvusd_price.py | 95 ++++++++ 5 files changed, 599 insertions(+) create mode 100644 contracts/oracles/OptimismBlockHashOracle.vy create mode 100644 contracts/oracles/ScrvusdOracle.vy create mode 100644 contracts/provers/ScrvusdProver.sol create mode 100644 scripts/scrvusd_keeper.py create mode 100644 scripts/submit_scrvusd_price.py diff --git a/contracts/oracles/OptimismBlockHashOracle.vy b/contracts/oracles/OptimismBlockHashOracle.vy new file mode 100644 index 0000000..8726257 --- /dev/null +++ b/contracts/oracles/OptimismBlockHashOracle.vy @@ -0,0 +1,81 @@ +# pragma version 0.4.0 +""" +@title Optimism Block Hash oracle +@notice A contract that saves L1 block hashes. +@license MIT +@author curve.fi +@custom:version 0.0.1 +@custom:security security@curve.fi +""" + +version: public(constant(String[8])) = "0.0.1" + +interface IL1Block: + def number() -> uint64: view + def hash() -> bytes32: view + + +event CommitBlockHash: + committer: indexed(address) + number: indexed(uint256) + hash: bytes32 + +event ApplyBlockHash: + number: indexed(uint256) + hash: bytes32 + +L1_BLOCK: constant(IL1Block) = IL1Block(0x4200000000000000000000000000000000000015) + +block_hash: public(HashMap[uint256, bytes32]) +commitments: public(HashMap[address, HashMap[uint256, bytes32]]) + + +@view +@external +def get_block_hash(_number: uint256) -> bytes32: + """ + @notice Query the block hash of a block. + @dev Reverts for block numbers which have yet to be set. + """ + block_hash: bytes32 = self.block_hash[_number] + assert block_hash != empty(bytes32) + + return block_hash + + +@internal +def _update_block_hash() -> (uint256, bytes32): + number: uint256 = convert(staticcall L1_BLOCK.number(), uint256) + hash: bytes32 = staticcall L1_BLOCK.hash() + self.block_hash[number] = hash + + return number, hash + + +@external +def commit() -> uint256: + """ + @notice Commit (and apply) a block hash. + @dev Same as `apply()` but saves committer + """ + number: uint256 = 0 + hash: bytes32 = empty(bytes32) + number, hash = self._update_block_hash() + + self.commitments[msg.sender][number] = hash + log CommitBlockHash(msg.sender, number, hash) + log ApplyBlockHash(number, hash) + return number + + +@external +def apply() -> uint256: + """ + @notice Apply a block hash. + """ + number: uint256 = 0 + hash: bytes32 = empty(bytes32) + number, hash = self._update_block_hash() + + log ApplyBlockHash(number, hash) + return number diff --git a/contracts/oracles/ScrvusdOracle.vy b/contracts/oracles/ScrvusdOracle.vy new file mode 100644 index 0000000..f9fc9c2 --- /dev/null +++ b/contracts/oracles/ScrvusdOracle.vy @@ -0,0 +1,219 @@ +# pragma version 0.4.0 +""" +@title scrvUSD oracle +@notice Oracle of scrvUSD share price for StableSwap pool and other integrations. + Price updates are linearly smoothed with max acceleration to eliminate sharp changes. +@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@author curve.fi +@custom:version 0.0.1 +@custom:security security@curve.fi +""" + +version: public(constant(String[8])) = "0.0.1" + +from snekmate.auth import ownable + +initializes: ownable +exports: ownable.__interface__ + +event PriceUpdate: + new_price: uint256 # price to achieve + at: uint256 # timestamp at which price will be achieved + +event SetProver: + prover: address + +struct Interval: + previous: uint256 + future: uint256 + + +# scrvUSD Vault rate replication +# 0 total_debt +# 1 total_idle +ASSETS_PARAM_CNT: constant(uint256) = 2 +# 0 totalSupply +# 1 full_profit_unlock_date +# 2 profit_unlocking_rate +# 3 last_profit_update +# 4 balance_of_self +# 5 block.timestamp +SUPPLY_PARAM_CNT: constant(uint256) = 6 +MAX_BPS_EXTENDED: constant(uint256) = 1_000_000_000_000 + +prover: public(address) + +price: public(Interval) # price of asset per share +time: public(Interval) + +max_acceleration: public(uint256) # precision 10**18 + + +@deploy +def __init__(_initial_price: uint256, _max_acceleration: uint256): + """ + @param _initial_price Initial price of asset per share (10**18) + @param _max_acceleration Maximum acceleration (10**12) + """ + self.price = Interval(previous=_initial_price, future=_initial_price) + self.time = Interval(previous=block.timestamp, future=block.timestamp) + + self.max_acceleration = _max_acceleration + + ownable.__init__() + + +@view +@internal +def _price_per_share(ts: uint256) -> uint256: + """ + @notice Using linear interpolation assuming updates are often enough + for absolute difference \approx relative difference + """ + price: Interval = self.price + time: Interval = self.time + if ts >= time.future: + return price.future + if ts <= time.previous: + return price.previous + return (price.previous * (time.future - ts) + price.future * (ts - time.previous)) // (time.future - time.previous) + + +@view +@external +def pricePerShare(ts: uint256=block.timestamp) -> uint256: + """ + @notice Get the price per share (pps) of the vault. + @dev NOT precise. Price is smoothed over time to eliminate sharp changes. + @param ts Timestamp to look price at. Only near future is supported. + @return The price per share. + """ + return self._price_per_share(ts) + + +@view +@external +def pricePerAsset(ts: uint256=block.timestamp) -> uint256: + """ + @notice Get the price per asset of the vault. + @dev NOT precise. Price is smoothed over time to eliminate sharp changes. + @param ts Timestamp to look price at. Only near future is supported. + @return The price per share. + """ + return 10 ** 36 // self._price_per_share(ts) + + +@view +@external +def price_oracle(i: uint256=0) -> uint256: + """ + @notice Alias of `pricePerShare` and `pricePerAsset` made for compatability + @param i 0 for scrvusd per crvusd, 1 for crvusd per scrvusd + @return Price with 10^18 precision + """ + return self._price_per_share(block.timestamp) if i == 0 else 10 ** 36 // self._price_per_share(block.timestamp) + + +@view +@internal +def _unlocked_shares( + full_profit_unlock_date: uint256, + profit_unlocking_rate: uint256, + last_profit_update: uint256, + balance_of_self: uint256, + ts: uint256, +) -> uint256: + """ + Returns the amount of shares that have been unlocked. + To avoid sudden price_per_share spikes, profits can be processed + through an unlocking period. The mechanism involves shares to be + minted to the vault which are unlocked gradually over time. Shares + that have been locked are gradually unlocked over profit_max_unlock_time. + """ + unlocked_shares: uint256 = 0 + if full_profit_unlock_date > ts: + # If we have not fully unlocked, we need to calculate how much has been. + unlocked_shares = profit_unlocking_rate * (ts - last_profit_update) // MAX_BPS_EXTENDED + + elif full_profit_unlock_date != 0: + # All shares have been unlocked + unlocked_shares = balance_of_self + + return unlocked_shares + + +@view +@internal +def _total_supply(parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT]) -> uint256: + # Need to account for the shares issued to the vault that have unlocked. + return parameters[ASSETS_PARAM_CNT + 0] -\ + self._unlocked_shares( + parameters[ASSETS_PARAM_CNT + 1], # full_profit_unlock_date + parameters[ASSETS_PARAM_CNT + 2], # profit_unlocking_rate + parameters[ASSETS_PARAM_CNT + 3], # last_profit_update + parameters[ASSETS_PARAM_CNT + 4], # balance_of_self + parameters[ASSETS_PARAM_CNT + 5], # block.timestamp + ) + +@view +@internal +def _total_assets(parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT]) -> uint256: + """ + @notice Total amount of assets that are in the vault and in the strategies. + """ + return parameters[0] + parameters[1] + + +@external +def update_price( + _parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT], +) -> uint256: + """ + @notice Update price using `_parameters` + @param _parameters Parameters of + @return Relative price change of final price with 10^18 precision + """ + assert msg.sender == self.prover + + current_price: uint256 = self._price_per_share(block.timestamp) + new_price: uint256 = self._total_assets(_parameters) * 10 ** 18 //\ + self._total_supply(_parameters) + + # Price is always growing and updates are never from future, + # hence allow only increasing updates + future_price: uint256 = self.price.future + if new_price > future_price: + self.price = Interval(previous=current_price, future=new_price) + + rel_price_change: uint256 = (new_price - current_price) * 10 ** 18 // current_price + 1 # 1 for rounding up + future_ts: uint256 = block.timestamp + rel_price_change // self.max_acceleration + self.time = Interval(previous=block.timestamp, future=future_ts) + + log PriceUpdate(new_price, future_ts) + return new_price * 10 ** 18 // future_price + return 10 ** 18 + + +@external +def set_max_acceleration(_max_acceleration: uint256): + """ + @notice Set maximum acceleration of scrvUSD. + Must be less than StableSwap's minimum fee. + fee / (2 * block_time) is considered to be safe. + @param _max_acceleration Maximum acceleration (per sec) + """ + ownable._check_owner() + + assert 10 ** 8 <= _max_acceleration and _max_acceleration <= 10 ** 18 + self.max_acceleration = _max_acceleration + + +@external +def set_prover(_prover: address): + """ + @notice Set the account with prover permissions. + """ + ownable._check_owner() + + self.prover = _prover + log SetProver(_prover) diff --git a/contracts/provers/ScrvusdProver.sol b/contracts/provers/ScrvusdProver.sol new file mode 100644 index 0000000..670176c --- /dev/null +++ b/contracts/provers/ScrvusdProver.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import {RLPReader} from "hamdiallam/Solidity-RLP@2.0.7/contracts/RLPReader.sol"; +import {StateProofVerifier as Verifier} from "../libs/StateProofVerifier.sol"; + +interface IBlockHashOracle { + function get_block_hash(uint256 _number) external view returns (bytes32); +} + +interface IScrvusdOracle { + function update_price( + uint256[2 + 6] memory _parameters + ) external returns (uint256); +} + +/// @title Scrvusd Prover +/// @author Curve Finance +contract ScrvusdProver { + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + + address constant SCRVUSD = + 0x182863131F9a4630fF9E27830d945B1413e347E8; // Temporary compatible vault + bytes32 constant SCRVUSD_HASH = + keccak256(abi.encodePacked(SCRVUSD)); + + address public immutable BLOCK_HASH_ORACLE; + address public immutable SCRVUSD_ORACLE; + + uint256 constant PARAM_CNT = 2 + 6; + uint256 constant PROOF_CNT = PARAM_CNT - 1; // -1 for timestamp obtained from block header + + constructor(address _block_hash_oracle, address _scrvusd_oracle) { + BLOCK_HASH_ORACLE = _block_hash_oracle; + SCRVUSD_ORACLE = _scrvusd_oracle; + } + + /// Prove parameters of scrvUSD rate. + /// @param _block_header_rlp The block header of any block. + /// @param _proof_rlp The state proof of the parameters. + function prove( + bytes memory _block_header_rlp, + bytes memory _proof_rlp + ) external returns (uint256) { + Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader( + _block_header_rlp + ); + require(block_header.hash != bytes32(0)); // dev: invalid blockhash + require( + block_header.hash == + IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash( + block_header.number + ) + ); // dev: blockhash mismatch + + // convert _proof_rlp into a list of `RLPItem`s + RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList(); + require(proofs.length == 1 + PROOF_CNT); // dev: invalid number of proofs + + // 0th proof is the account proof for the scrvUSD contract + Verifier.Account memory account = Verifier.extractAccountFromProof( + SCRVUSD_HASH, // position of the account is the hash of its address + block_header.stateRootHash, + proofs[0].toList() + ); + require(account.exists); // dev: scrvUSD account does not exist + + // iterate over proofs + uint256[PROOF_CNT] memory PARAM_SLOTS = [ + // Assets parameters + uint256(21), // total_debt + 22, // total_idle + + // Supply parameters + 20, // totalSupply + 38, // full_profit_unlock_date + 39, // profit_unlocking_rate + 40, // last_profit_update + uint256(keccak256(abi.encode(18, SCRVUSD))) // balance_of_self + // ts from block header + ]; + uint256[PARAM_CNT] memory params; + Verifier.SlotValue memory slot; + uint256 i = 0; + for (uint256 idx = 1; idx < 1 + PARAM_CNT-1; idx++) { + slot = Verifier.extractSlotValueFromProof( + keccak256(abi.encode(PARAM_SLOTS[i])), + account.storageRoot, + proofs[idx].toList() + ); + // Some slots may not be used => not exist, e.g. total_idle + // require(slot.exists); + + params[i] = slot.value; + i++; + } + params[i] = block_header.timestamp; + return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params); + } +} diff --git a/scripts/scrvusd_keeper.py b/scripts/scrvusd_keeper.py new file mode 100644 index 0000000..0526cde --- /dev/null +++ b/scripts/scrvusd_keeper.py @@ -0,0 +1,103 @@ +import time +from time import sleep + +from web3 import Web3 +from web3.eth import AsyncEth +import json +import os + +from getpass import getpass +from eth_account import account + +from submit_scrvusd_price import generate_proof + +ETH_NETWORK = f"https://eth-mainnet.alchemyapi.io/v2/{os.environ['WEB3_ETHEREUM_MAINNET_ALCHEMY_API_KEY']}" +L2_NETWORK = f"https://opt-mainnet.g.alchemy.com/v2/{os.environ['WEB3_OPTIMISM_MAINNET_ALCHEMY_API_KEY']}" + +SCRVUSD = "" + +B_ORACLE = "" +S_ORACLE = "" +PROVER = "" + +last_update = 0 + +APPLY_BLOCK_HASH = Web3.keccak(text="ApplyBlockHash(uint256,bytes32)").hex() +COMMIT_BLOCK_HASH = Web3.keccak(text="CommitBlockHash(address,uint256,bytes32)").hex() + + +eth_web3 = Web3( + provider=Web3.HTTPProvider( + ETH_NETWORK, + # {"verify_ssl": False}, + ), + # modules={"eth": (AsyncEth,)}, +) + +l2_web3 = Web3( + provider=Web3.HTTPProvider( + L2_NETWORK, + # {"verify_ssl": False}, + ), + # modules={"eth": (AsyncEth,)}, +) + + +def account_load_pkey(fname): + path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) + with open(path, 'r') as f: + pkey = account.decode_keyfile_json(json.load(f), getpass()) + return pkey +wallet_pk = account_load_pkey("keeper") + + +def prove(boracle, prover): + # Apply latest available blockhash + tx = boracle.functions.apply().build_transaction() + signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet_pk) + tx_hash = l2_web3.eth.send_raw_transaction(signed_tx.rawTransaction) + l2_web3.eth.wait_for_transaction_receipt(tx_hash) + tx_receipt = l2_web3.eth.get_transaction_receipt(tx_hash) + number = -1 + for log in tx_receipt["logs"]: + if log["address"] == boracle.address: + if log["topics"][0].hex() == APPLY_BLOCK_HASH: + number = int(log["topics"][1].hex(), 16) + break + if log["topics"][0].hex() == COMMIT_BLOCK_HASH: + number = int(log["topics"][2].hex(), 16) + break + assert number > 0, "Applied block number not retrieved" + print(f"Applied block: {number}") + + # Generate and submit proof for applied blockhash + proofs = generate_proof(number, eth_web3) + tx = prover.functions.prove(proofs[0], proofs[1]).build_transaction() + signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet_pk) + l2_web3.eth.send_raw_transaction(signed_tx.rawTransaction) + l2_web3.eth.wait_for_transaction_receipt(tx_hash) + print(f"Submitted proof") + + +def time_to_update(): + # can be any relative change or time + return time.time() - last_update >= 4 * 3600 # Every 4 hours + + +def loop(): + boracle = l2_web3.eth.contract(B_ORACLE, abi=[{'name': 'CommitBlockHash', 'inputs': [{'name': 'committer', 'type': 'address', 'indexed': True}, {'name': 'number', 'type': 'uint256', 'indexed': True}, {'name': 'hash', 'type': 'bytes32', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'ApplyBlockHash', 'inputs': [{'name': 'number', 'type': 'uint256', 'indexed': True}, {'name': 'hash', 'type': 'bytes32', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'stateMutability': 'view', 'type': 'function', 'name': 'get_block_hash', 'inputs': [{'name': '_number', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bytes32'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'commit', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'apply', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'block_hash', 'inputs': [{'name': 'arg0', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bytes32'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'commitments', 'inputs': [{'name': 'arg0', 'type': 'address'}, {'name': 'arg1', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bytes32'}]}]) + prover = l2_web3.eth.contract(PROVER, abi=[{"inputs": [{"internalType": "bytes", "name": "_block_header_rlp", "type": "bytes"}, {"internalType": "bytes", "name": "_proof_rlp", "type": "bytes"}], "name": "prove", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}]) + + while True: + if time_to_update(): + try: + prove(boracle, prover) + global last_update + last_update = time.time() + except Exception as e: + print(e) + sleep(12) + + +if __name__ == '__main__': + loop() diff --git a/scripts/submit_scrvusd_price.py b/scripts/submit_scrvusd_price.py new file mode 100644 index 0000000..d34a665 --- /dev/null +++ b/scripts/submit_scrvusd_price.py @@ -0,0 +1,95 @@ +import eth_abi +import rlp +import web3 +from hexbytes import HexBytes + +BLOCK_NUMBER = 18578883 +SCRVUSD = "0x182863131F9a4630fF9E27830d945B1413e347E8" + +PROVER = "" + +ASSET_PARAM_SLOTS = [ + 21, # total_debt + 22, # total_idle, slot doesn't exist +] +SUPPLY_PARAM_SLOTS = [ + 20, # totalSupply + 38, # full_profit_unlock_date + 39, # profit_unlocking_rate + 40, # last_profit_update + web3.Web3.keccak(eth_abi.encode(["(uint256,address)"], [[18, SCRVUSD]])), # balance_of_self + # ts from block header +] + +# https://github.com/ethereum/go-ethereum/blob/master/core/types/block.go#L69 +BLOCK_HEADER = ( + "parentHash", + "sha3Uncles", + "miner", + "stateRoot", + "transactionsRoot", + "receiptsRoot", + "logsBloom", + "difficulty", + "number", + "gasLimit", + "gasUsed", + "timestamp", + "extraData", + "mixHash", + "nonce", + "baseFeePerGas", # added by EIP-1559 and is ignored in legacy headers + "withdrawalsRoot", # added by EIP-4895 and is ignored in legacy headers + "blobGasUsed", # added by EIP-4844 and is ignored in legacy headers + "excessBlobGas", # added by EIP-4844 and is ignored in legacy headers + "parentBeaconBlockRoot", # added by EIP-4788 and is ignored in legacy headers +) + + +def serialize_block(block): + block_header = [ + HexBytes("0x") if isinstance((v := block[k]), int) and v == 0 else HexBytes(v) + for k in BLOCK_HEADER + if k in block + ] + return rlp.encode(block_header) + + +def serialize_proofs(proofs): + account_proof = list(map(rlp.decode, map(HexBytes, proofs["accountProof"]))) + storage_proofs = [ + list(map(rlp.decode, map(HexBytes, proof["proof"]))) for proof in proofs["storageProof"] + ] + return rlp.encode([account_proof, *storage_proofs]) + + +def generate_proof(block_number, eth_web3, log=False): + block_number = block_number or BLOCK_NUMBER + block = eth_web3.eth.get_block(block_number) + if log: + print(f"Generating proof for block {block.number}, {block.hash.hex()}") + block_header_rlp = serialize_block(block) + proof_rlp = serialize_proofs(eth_web3.eth.get_proof(SCRVUSD, ASSET_PARAM_SLOTS + SUPPLY_PARAM_SLOTS, block_number)) + + with open("header.txt", "w") as f: + f.write(block_header_rlp.hex()) + with open("proof.txt", "w") as f: + f.write(proof_rlp.hex()) + + return block_header_rlp.hex(), proof_rlp.hex() + + +def submit_proof(proofs, prover=None): + prover = prover or PROVER + # if isinstance(prover, str): + # prover = boa_solidity.load_partial("contracts/provers/ScrvusdProver.sol").at(prover) + + if proofs: + block_header_rlp, proof_rlp = proofs + else: + with open("header.txt") as f: + block_header_rlp = f.read() + with open("proof.txt") as f: + proof_rlp = f.read() + + prover.prove(bytes.fromhex(block_header_rlp), bytes.fromhex(proof_rlp)) From 1433ac9378a8addad6fe87740a267a1bb3f1698b Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Tue, 29 Oct 2024 21:31:27 +0300 Subject: [PATCH 2/7] style: alias --- contracts/provers/ScrvusdProver.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/provers/ScrvusdProver.sol b/contracts/provers/ScrvusdProver.sol index 670176c..728e49f 100644 --- a/contracts/provers/ScrvusdProver.sol +++ b/contracts/provers/ScrvusdProver.sol @@ -83,7 +83,7 @@ contract ScrvusdProver { uint256[PARAM_CNT] memory params; Verifier.SlotValue memory slot; uint256 i = 0; - for (uint256 idx = 1; idx < 1 + PARAM_CNT-1; idx++) { + for (uint256 idx = 1; idx < 1 + PROOF_CNT; idx++) { slot = Verifier.extractSlotValueFromProof( keccak256(abi.encode(PARAM_SLOTS[i])), account.storageRoot, From a420cd733c7b172760c8367b17583b809026d183 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Mon, 4 Nov 2024 09:59:52 +0300 Subject: [PATCH 3/7] chore: update scrvusd address and script changes --- contracts/provers/ScrvusdProver.sol | 2 +- scripts/scrvusd_keeper.py | 4 ++-- scripts/submit_scrvusd_price.py | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/contracts/provers/ScrvusdProver.sol b/contracts/provers/ScrvusdProver.sol index 728e49f..82ee058 100644 --- a/contracts/provers/ScrvusdProver.sol +++ b/contracts/provers/ScrvusdProver.sol @@ -21,7 +21,7 @@ contract ScrvusdProver { using RLPReader for RLPReader.RLPItem; address constant SCRVUSD = - 0x182863131F9a4630fF9E27830d945B1413e347E8; // Temporary compatible vault + 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367; bytes32 constant SCRVUSD_HASH = keccak256(abi.encodePacked(SCRVUSD)); diff --git a/scripts/scrvusd_keeper.py b/scripts/scrvusd_keeper.py index 0526cde..1cf2919 100644 --- a/scripts/scrvusd_keeper.py +++ b/scripts/scrvusd_keeper.py @@ -14,7 +14,7 @@ ETH_NETWORK = f"https://eth-mainnet.alchemyapi.io/v2/{os.environ['WEB3_ETHEREUM_MAINNET_ALCHEMY_API_KEY']}" L2_NETWORK = f"https://opt-mainnet.g.alchemy.com/v2/{os.environ['WEB3_OPTIMISM_MAINNET_ALCHEMY_API_KEY']}" -SCRVUSD = "" +SCRVUSD = "0x0655977FEb2f289A4aB78af67BAB0d17aAb84367" B_ORACLE = "" S_ORACLE = "" @@ -71,7 +71,7 @@ def prove(boracle, prover): print(f"Applied block: {number}") # Generate and submit proof for applied blockhash - proofs = generate_proof(number, eth_web3) + proofs = generate_proof(eth_web3, number) tx = prover.functions.prove(proofs[0], proofs[1]).build_transaction() signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet_pk) l2_web3.eth.send_raw_transaction(signed_tx.rawTransaction) diff --git a/scripts/submit_scrvusd_price.py b/scripts/submit_scrvusd_price.py index d34a665..3105aaf 100644 --- a/scripts/submit_scrvusd_price.py +++ b/scripts/submit_scrvusd_price.py @@ -4,7 +4,7 @@ from hexbytes import HexBytes BLOCK_NUMBER = 18578883 -SCRVUSD = "0x182863131F9a4630fF9E27830d945B1413e347E8" +SCRVUSD = "0x0655977FEb2f289A4aB78af67BAB0d17aAb84367" PROVER = "" @@ -63,8 +63,7 @@ def serialize_proofs(proofs): return rlp.encode([account_proof, *storage_proofs]) -def generate_proof(block_number, eth_web3, log=False): - block_number = block_number or BLOCK_NUMBER +def generate_proof(eth_web3, block_number=BLOCK_NUMBER, log=False): block = eth_web3.eth.get_block(block_number) if log: print(f"Generating proof for block {block.number}, {block.hash.hex()}") @@ -79,11 +78,7 @@ def generate_proof(block_number, eth_web3, log=False): return block_header_rlp.hex(), proof_rlp.hex() -def submit_proof(proofs, prover=None): - prover = prover or PROVER - # if isinstance(prover, str): - # prover = boa_solidity.load_partial("contracts/provers/ScrvusdProver.sol").at(prover) - +def submit_proof(proofs, prover=PROVER): if proofs: block_header_rlp, proof_rlp = proofs else: @@ -92,4 +87,10 @@ def submit_proof(proofs, prover=None): with open("proof.txt") as f: proof_rlp = f.read() - prover.prove(bytes.fromhex(block_header_rlp), bytes.fromhex(proof_rlp)) + if isinstance(prover, str): + from brownie import accounts, ScrvusdProver + dev = accounts.load("dev") + prover = ScrvusdProver.at(prover) + prover.prove(bytes.fromhex(block_header_rlp), bytes.fromhex(proof_rlp), {"from": dev}) + else: + prover.prove(bytes.fromhex(block_header_rlp), bytes.fromhex(proof_rlp)) From 7c2ac4d401406383a26331b28bd1b7c11ea5abb4 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Tue, 5 Nov 2024 10:21:23 +0300 Subject: [PATCH 4/7] chore: MIT license --- contracts/oracles/ScrvusdOracle.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/oracles/ScrvusdOracle.vy b/contracts/oracles/ScrvusdOracle.vy index f9fc9c2..9b9d35b 100644 --- a/contracts/oracles/ScrvusdOracle.vy +++ b/contracts/oracles/ScrvusdOracle.vy @@ -3,7 +3,7 @@ @title scrvUSD oracle @notice Oracle of scrvUSD share price for StableSwap pool and other integrations. Price updates are linearly smoothed with max acceleration to eliminate sharp changes. -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@license MIT @author curve.fi @custom:version 0.0.1 @custom:security security@curve.fi From 2319b637ae4bed0c65bd54c695eb5464ab108027 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 7 Nov 2024 19:55:26 +0300 Subject: [PATCH 5/7] fix: scrvusd_keeper.py --- scripts/scrvusd_keeper.py | 45 +++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/scripts/scrvusd_keeper.py b/scripts/scrvusd_keeper.py index 1cf2919..aec37c5 100644 --- a/scripts/scrvusd_keeper.py +++ b/scripts/scrvusd_keeper.py @@ -7,7 +7,7 @@ import os from getpass import getpass -from eth_account import account +from eth_account import account, Account from submit_scrvusd_price import generate_proof @@ -16,11 +16,12 @@ SCRVUSD = "0x0655977FEb2f289A4aB78af67BAB0d17aAb84367" -B_ORACLE = "" -S_ORACLE = "" -PROVER = "" +B_ORACLE = "0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7" +S_ORACLE = "0xC772063cE3e622B458B706Dd2e36309418A1aE42" +PROVER = "0x47ca04Ee05f167583122833abfb0f14aC5677Ee4" last_update = 0 +REL_CHANGE_THRESHOLD = 1.00005 # 0.5 bps, should be >1 APPLY_BLOCK_HASH = Web3.keccak(text="ApplyBlockHash(uint256,bytes32)").hex() COMMIT_BLOCK_HASH = Web3.keccak(text="CommitBlockHash(address,uint256,bytes32)").hex() @@ -48,14 +49,19 @@ def account_load_pkey(fname): with open(path, 'r') as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return pkey -wallet_pk = account_load_pkey("keeper") +wallet = Account.from_key(account_load_pkey("keeper")) def prove(boracle, prover): # Apply latest available blockhash - tx = boracle.functions.apply().build_transaction() - signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet_pk) - tx_hash = l2_web3.eth.send_raw_transaction(signed_tx.rawTransaction) + tx = boracle.functions.apply().build_transaction( + { + "from": wallet.address, + "nonce": l2_web3.eth.get_transaction_count(wallet.address), + } + ) + signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet.key) + tx_hash = l2_web3.eth.send_raw_transaction(signed_tx.raw_transaction) l2_web3.eth.wait_for_transaction_receipt(tx_hash) tx_receipt = l2_web3.eth.get_transaction_receipt(tx_hash) number = -1 @@ -72,24 +78,35 @@ def prove(boracle, prover): # Generate and submit proof for applied blockhash proofs = generate_proof(eth_web3, number) - tx = prover.functions.prove(proofs[0], proofs[1]).build_transaction() - signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet_pk) - l2_web3.eth.send_raw_transaction(signed_tx.rawTransaction) + tx = prover.functions.prove(bytes.fromhex(proofs[0]), bytes.fromhex(proofs[1])).build_transaction( + { + "from": wallet.address, + "nonce": l2_web3.eth.get_transaction_count(wallet.address), + } + ) + signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet.key) + l2_web3.eth.send_raw_transaction(signed_tx.raw_transaction) l2_web3.eth.wait_for_transaction_receipt(tx_hash) print(f"Submitted proof") -def time_to_update(): +def time_to_update(scrvusd, soracle): # can be any relative change or time - return time.time() - last_update >= 4 * 3600 # Every 4 hours + if time.time() - last_update >= 4 * 3600: # Every 4 hours + return True + price = scrvusd.functions.pricePerShare().call() + oracle_price = soracle.functions.price().call()[1] # take price.future = latest set + return price / oracle_price > REL_CHANGE_THRESHOLD def loop(): + scrvusd = eth_web3.eth.contract(SCRVUSD, abi=[{'name': 'Deposit', 'inputs': [{'name': 'sender', 'type': 'address', 'indexed': True}, {'name': 'owner', 'type': 'address', 'indexed': True}, {'name': 'assets', 'type': 'uint256', 'indexed': False}, {'name': 'shares', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'Withdraw', 'inputs': [{'name': 'sender', 'type': 'address', 'indexed': True}, {'name': 'receiver', 'type': 'address', 'indexed': True}, {'name': 'owner', 'type': 'address', 'indexed': True}, {'name': 'assets', 'type': 'uint256', 'indexed': False}, {'name': 'shares', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'Transfer', 'inputs': [{'name': 'sender', 'type': 'address', 'indexed': True}, {'name': 'receiver', 'type': 'address', 'indexed': True}, {'name': 'value', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'Approval', 'inputs': [{'name': 'owner', 'type': 'address', 'indexed': True}, {'name': 'spender', 'type': 'address', 'indexed': True}, {'name': 'value', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'StrategyChanged', 'inputs': [{'name': 'strategy', 'type': 'address', 'indexed': True}, {'name': 'change_type', 'type': 'uint256', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'StrategyReported', 'inputs': [{'name': 'strategy', 'type': 'address', 'indexed': True}, {'name': 'gain', 'type': 'uint256', 'indexed': False}, {'name': 'loss', 'type': 'uint256', 'indexed': False}, {'name': 'current_debt', 'type': 'uint256', 'indexed': False}, {'name': 'protocol_fees', 'type': 'uint256', 'indexed': False}, {'name': 'total_fees', 'type': 'uint256', 'indexed': False}, {'name': 'total_refunds', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'DebtUpdated', 'inputs': [{'name': 'strategy', 'type': 'address', 'indexed': True}, {'name': 'current_debt', 'type': 'uint256', 'indexed': False}, {'name': 'new_debt', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'RoleSet', 'inputs': [{'name': 'account', 'type': 'address', 'indexed': True}, {'name': 'role', 'type': 'uint256', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateFutureRoleManager', 'inputs': [{'name': 'future_role_manager', 'type': 'address', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateRoleManager', 'inputs': [{'name': 'role_manager', 'type': 'address', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateAccountant', 'inputs': [{'name': 'accountant', 'type': 'address', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateDepositLimitModule', 'inputs': [{'name': 'deposit_limit_module', 'type': 'address', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateWithdrawLimitModule', 'inputs': [{'name': 'withdraw_limit_module', 'type': 'address', 'indexed': True}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateDefaultQueue', 'inputs': [{'name': 'new_default_queue', 'type': 'address[]', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateUseDefaultQueue', 'inputs': [{'name': 'use_default_queue', 'type': 'bool', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateAutoAllocate', 'inputs': [{'name': 'auto_allocate', 'type': 'bool', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdatedMaxDebtForStrategy', 'inputs': [{'name': 'sender', 'type': 'address', 'indexed': True}, {'name': 'strategy', 'type': 'address', 'indexed': True}, {'name': 'new_debt', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateDepositLimit', 'inputs': [{'name': 'deposit_limit', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateMinimumTotalIdle', 'inputs': [{'name': 'minimum_total_idle', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'UpdateProfitMaxUnlockTime', 'inputs': [{'name': 'profit_max_unlock_time', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'DebtPurchased', 'inputs': [{'name': 'strategy', 'type': 'address', 'indexed': True}, {'name': 'amount', 'type': 'uint256', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'Shutdown', 'inputs': [], 'anonymous': False, 'type': 'event'}, {'stateMutability': 'nonpayable', 'type': 'constructor', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'initialize', 'inputs': [{'name': 'asset', 'type': 'address'}, {'name': 'name', 'type': 'string'}, {'name': 'symbol', 'type': 'string'}, {'name': 'role_manager', 'type': 'address'}, {'name': 'profit_max_unlock_time', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'setName', 'inputs': [{'name': 'name', 'type': 'string'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'setSymbol', 'inputs': [{'name': 'symbol', 'type': 'string'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_accountant', 'inputs': [{'name': 'new_accountant', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_default_queue', 'inputs': [{'name': 'new_default_queue', 'type': 'address[]'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_use_default_queue', 'inputs': [{'name': 'use_default_queue', 'type': 'bool'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_auto_allocate', 'inputs': [{'name': 'auto_allocate', 'type': 'bool'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_deposit_limit', 'inputs': [{'name': 'deposit_limit', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_deposit_limit', 'inputs': [{'name': 'deposit_limit', 'type': 'uint256'}, {'name': 'override', 'type': 'bool'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_deposit_limit_module', 'inputs': [{'name': 'deposit_limit_module', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_deposit_limit_module', 'inputs': [{'name': 'deposit_limit_module', 'type': 'address'}, {'name': 'override', 'type': 'bool'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_withdraw_limit_module', 'inputs': [{'name': 'withdraw_limit_module', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_minimum_total_idle', 'inputs': [{'name': 'minimum_total_idle', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'setProfitMaxUnlockTime', 'inputs': [{'name': 'new_profit_max_unlock_time', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'set_role', 'inputs': [{'name': 'account', 'type': 'address'}, {'name': 'role', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'add_role', 'inputs': [{'name': 'account', 'type': 'address'}, {'name': 'role', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'remove_role', 'inputs': [{'name': 'account', 'type': 'address'}, {'name': 'role', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'transfer_role_manager', 'inputs': [{'name': 'role_manager', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'accept_role_manager', 'inputs': [], 'outputs': []}, {'stateMutability': 'view', 'type': 'function', 'name': 'isShutdown', 'inputs': [], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'unlockedShares', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'pricePerShare', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'get_default_queue', 'inputs': [], 'outputs': [{'name': '', 'type': 'address[]'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'process_report', 'inputs': [{'name': 'strategy', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}, {'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'buy_debt', 'inputs': [{'name': 'strategy', 'type': 'address'}, {'name': 'amount', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'add_strategy', 'inputs': [{'name': 'new_strategy', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'add_strategy', 'inputs': [{'name': 'new_strategy', 'type': 'address'}, {'name': 'add_to_queue', 'type': 'bool'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'revoke_strategy', 'inputs': [{'name': 'strategy', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'force_revoke_strategy', 'inputs': [{'name': 'strategy', 'type': 'address'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'update_max_debt_for_strategy', 'inputs': [{'name': 'strategy', 'type': 'address'}, {'name': 'new_max_debt', 'type': 'uint256'}], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'update_debt', 'inputs': [{'name': 'strategy', 'type': 'address'}, {'name': 'target_debt', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'update_debt', 'inputs': [{'name': 'strategy', 'type': 'address'}, {'name': 'target_debt', 'type': 'uint256'}, {'name': 'max_loss', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'shutdown_vault', 'inputs': [], 'outputs': []}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'deposit', 'inputs': [{'name': 'assets', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'mint', 'inputs': [{'name': 'shares', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'withdraw', 'inputs': [{'name': 'assets', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}, {'name': 'owner', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'withdraw', 'inputs': [{'name': 'assets', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}, {'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'withdraw', 'inputs': [{'name': 'assets', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}, {'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}, {'name': 'strategies', 'type': 'address[]'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'redeem', 'inputs': [{'name': 'shares', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}, {'name': 'owner', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'redeem', 'inputs': [{'name': 'shares', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}, {'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'redeem', 'inputs': [{'name': 'shares', 'type': 'uint256'}, {'name': 'receiver', 'type': 'address'}, {'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}, {'name': 'strategies', 'type': 'address[]'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'approve', 'inputs': [{'name': 'spender', 'type': 'address'}, {'name': 'amount', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'transfer', 'inputs': [{'name': 'receiver', 'type': 'address'}, {'name': 'amount', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'transferFrom', 'inputs': [{'name': 'sender', 'type': 'address'}, {'name': 'receiver', 'type': 'address'}, {'name': 'amount', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'permit', 'inputs': [{'name': 'owner', 'type': 'address'}, {'name': 'spender', 'type': 'address'}, {'name': 'amount', 'type': 'uint256'}, {'name': 'deadline', 'type': 'uint256'}, {'name': 'v', 'type': 'uint8'}, {'name': 'r', 'type': 'bytes32'}, {'name': 's', 'type': 'bytes32'}], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'balanceOf', 'inputs': [{'name': 'addr', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'totalSupply', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'totalAssets', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'totalIdle', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'totalDebt', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'convertToShares', 'inputs': [{'name': 'assets', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'previewDeposit', 'inputs': [{'name': 'assets', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'previewMint', 'inputs': [{'name': 'shares', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'convertToAssets', 'inputs': [{'name': 'shares', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxDeposit', 'inputs': [{'name': 'receiver', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxMint', 'inputs': [{'name': 'receiver', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxWithdraw', 'inputs': [{'name': 'owner', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxWithdraw', 'inputs': [{'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxWithdraw', 'inputs': [{'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}, {'name': 'strategies', 'type': 'address[]'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxRedeem', 'inputs': [{'name': 'owner', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxRedeem', 'inputs': [{'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'maxRedeem', 'inputs': [{'name': 'owner', 'type': 'address'}, {'name': 'max_loss', 'type': 'uint256'}, {'name': 'strategies', 'type': 'address[]'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'previewWithdraw', 'inputs': [{'name': 'assets', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'previewRedeem', 'inputs': [{'name': 'shares', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'FACTORY', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'pure', 'type': 'function', 'name': 'apiVersion', 'inputs': [], 'outputs': [{'name': '', 'type': 'string'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'assess_share_of_unrealised_losses', 'inputs': [{'name': 'strategy', 'type': 'address'}, {'name': 'assets_needed', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'profitMaxUnlockTime', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'fullProfitUnlockDate', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'profitUnlockingRate', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'lastProfitUpdate', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'DOMAIN_SEPARATOR', 'inputs': [], 'outputs': [{'name': '', 'type': 'bytes32'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'asset', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'decimals', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint8'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'strategies', 'inputs': [{'name': 'arg0', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'tuple', 'components': [{'name': 'activation', 'type': 'uint256'}, {'name': 'last_report', 'type': 'uint256'}, {'name': 'current_debt', 'type': 'uint256'}, {'name': 'max_debt', 'type': 'uint256'}]}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'default_queue', 'inputs': [{'name': 'arg0', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'use_default_queue', 'inputs': [], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'auto_allocate', 'inputs': [], 'outputs': [{'name': '', 'type': 'bool'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'allowance', 'inputs': [{'name': 'arg0', 'type': 'address'}, {'name': 'arg1', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'minimum_total_idle', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'deposit_limit', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'accountant', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'deposit_limit_module', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'withdraw_limit_module', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'roles', 'inputs': [{'name': 'arg0', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'role_manager', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'future_role_manager', 'inputs': [], 'outputs': [{'name': '', 'type': 'address'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'name', 'inputs': [], 'outputs': [{'name': '', 'type': 'string'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'symbol', 'inputs': [], 'outputs': [{'name': '', 'type': 'string'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'nonces', 'inputs': [{'name': 'arg0', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint256'}]}]) boracle = l2_web3.eth.contract(B_ORACLE, abi=[{'name': 'CommitBlockHash', 'inputs': [{'name': 'committer', 'type': 'address', 'indexed': True}, {'name': 'number', 'type': 'uint256', 'indexed': True}, {'name': 'hash', 'type': 'bytes32', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'name': 'ApplyBlockHash', 'inputs': [{'name': 'number', 'type': 'uint256', 'indexed': True}, {'name': 'hash', 'type': 'bytes32', 'indexed': False}], 'anonymous': False, 'type': 'event'}, {'stateMutability': 'view', 'type': 'function', 'name': 'get_block_hash', 'inputs': [{'name': '_number', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bytes32'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'commit', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'nonpayable', 'type': 'function', 'name': 'apply', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'block_hash', 'inputs': [{'name': 'arg0', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bytes32'}]}, {'stateMutability': 'view', 'type': 'function', 'name': 'commitments', 'inputs': [{'name': 'arg0', 'type': 'address'}, {'name': 'arg1', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bytes32'}]}]) + soracle = l2_web3.eth.contract(S_ORACLE, abi=[{'anonymous': False, 'inputs': [{'indexed': False, 'name': 'new_price', 'type': 'uint256'}, {'indexed': False, 'name': 'at', 'type': 'uint256'}], 'name': 'PriceUpdate', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': False, 'name': 'prover', 'type': 'address'}], 'name': 'SetProver', 'type': 'event'}, {'anonymous': False, 'inputs': [{'indexed': True, 'name': 'previous_owner', 'type': 'address'}, {'indexed': True, 'name': 'new_owner', 'type': 'address'}], 'name': 'OwnershipTransferred', 'type': 'event'}, {'inputs': [{'name': 'new_owner', 'type': 'address'}], 'name': 'transfer_ownership', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [], 'name': 'renounce_ownership', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'pricePerShare', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': 'ts', 'type': 'uint256'}], 'name': 'pricePerShare', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'pricePerAsset', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': 'ts', 'type': 'uint256'}], 'name': 'pricePerAsset', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'price_oracle', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': 'i', 'type': 'uint256'}], 'name': 'price_oracle', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_parameters', 'type': 'uint256[8]'}], 'name': 'update_price', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [{'name': '_max_acceleration', 'type': 'uint256'}], 'name': 'set_max_acceleration', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [{'name': '_prover', 'type': 'address'}], 'name': 'set_prover', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [], 'name': 'version', 'outputs': [{'name': '', 'type': 'string'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'prover', 'outputs': [{'name': '', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'price', 'outputs': [{'components': [{'name': 'previous', 'type': 'uint256'}, {'name': 'future', 'type': 'uint256'}], 'name': '', 'type': 'tuple'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'time', 'outputs': [{'components': [{'name': 'previous', 'type': 'uint256'}, {'name': 'future', 'type': 'uint256'}], 'name': '', 'type': 'tuple'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'max_acceleration', 'outputs': [{'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_initial_price', 'type': 'uint256'}, {'name': '_max_acceleration', 'type': 'uint256'}], 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'constructor'}]) prover = l2_web3.eth.contract(PROVER, abi=[{"inputs": [{"internalType": "bytes", "name": "_block_header_rlp", "type": "bytes"}, {"internalType": "bytes", "name": "_proof_rlp", "type": "bytes"}], "name": "prove", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}]) while True: - if time_to_update(): + if time_to_update(scrvusd, soracle): try: prove(boracle, prover) global last_update From 3af7a9e8100bfb39ea4a578171f5a073d1e57679 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 7 Nov 2024 20:37:56 +0300 Subject: [PATCH 6/7] chore: add deployment addresses --- scripts/scrvusd_keeper.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/scrvusd_keeper.py b/scripts/scrvusd_keeper.py index aec37c5..0c3ee71 100644 --- a/scripts/scrvusd_keeper.py +++ b/scripts/scrvusd_keeper.py @@ -16,6 +16,14 @@ SCRVUSD = "0x0655977FEb2f289A4aB78af67BAB0d17aAb84367" +# optimism +# 0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7, 0xC772063cE3e622B458B706Dd2e36309418A1aE42, 0x47ca04Ee05f167583122833abfb0f14aC5677Ee4 +# base +# 0x3c0a405E914337139992625D5100Ea141a9C4d11, 0x3d8EADb739D1Ef95dd53D718e4810721837c69c1, 0x6a2691068C7CbdA03292Ba0f9c77A25F658bAeF5 +# fraxtal +# 0xbD2775B8eADaE81501898eB208715f0040E51882, 0x09F8D940EAD55853c51045bcbfE67341B686C071, 0x0094Ad026643994c8fB2136ec912D508B15fe0E5 +# mantle +# 0x004A476B5B76738E34c86C7144554B9d34402F13, 0xbD2775B8eADaE81501898eB208715f0040E51882, 0x09F8D940EAD55853c51045bcbfE67341B686C071 B_ORACLE = "0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7" S_ORACLE = "0xC772063cE3e622B458B706Dd2e36309418A1aE42" PROVER = "0x47ca04Ee05f167583122833abfb0f14aC5677Ee4" From 2441ed4261ccd41c2d26781479da443fc9591292 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Mon, 11 Nov 2024 10:07:08 +0300 Subject: [PATCH 7/7] chore: update keeper script --- scripts/scrvusd_keeper.py | 69 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/scripts/scrvusd_keeper.py b/scripts/scrvusd_keeper.py index 0c3ee71..128a9e0 100644 --- a/scripts/scrvusd_keeper.py +++ b/scripts/scrvusd_keeper.py @@ -16,17 +16,14 @@ SCRVUSD = "0x0655977FEb2f289A4aB78af67BAB0d17aAb84367" -# optimism -# 0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7, 0xC772063cE3e622B458B706Dd2e36309418A1aE42, 0x47ca04Ee05f167583122833abfb0f14aC5677Ee4 -# base -# 0x3c0a405E914337139992625D5100Ea141a9C4d11, 0x3d8EADb739D1Ef95dd53D718e4810721837c69c1, 0x6a2691068C7CbdA03292Ba0f9c77A25F658bAeF5 -# fraxtal -# 0xbD2775B8eADaE81501898eB208715f0040E51882, 0x09F8D940EAD55853c51045bcbfE67341B686C071, 0x0094Ad026643994c8fB2136ec912D508B15fe0E5 -# mantle -# 0x004A476B5B76738E34c86C7144554B9d34402F13, 0xbD2775B8eADaE81501898eB208715f0040E51882, 0x09F8D940EAD55853c51045bcbfE67341B686C071 -B_ORACLE = "0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7" -S_ORACLE = "0xC772063cE3e622B458B706Dd2e36309418A1aE42" -PROVER = "0x47ca04Ee05f167583122833abfb0f14aC5677Ee4" +DEPLOYMENTS = { + "optimism": ("0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7", "0xC772063cE3e622B458B706Dd2e36309418A1aE42", "0x47ca04Ee05f167583122833abfb0f14aC5677Ee4"), + "base": ("0x3c0a405E914337139992625D5100Ea141a9C4d11", "0x3d8EADb739D1Ef95dd53D718e4810721837c69c1", "0x6a2691068C7CbdA03292Ba0f9c77A25F658bAeF5"), + "fraxtal": ("0xbD2775B8eADaE81501898eB208715f0040E51882", "0x09F8D940EAD55853c51045bcbfE67341B686C071", "0x0094Ad026643994c8fB2136ec912D508B15fe0E5"), + "mantle": ("0x004A476B5B76738E34c86C7144554B9d34402F13", "0xbD2775B8eADaE81501898eB208715f0040E51882", "0x09F8D940EAD55853c51045bcbfE67341B686C071"), +} + +B_ORACLE, S_ORACLE, PROVER = DEPLOYMENTS["optimism"] last_update = 0 REL_CHANGE_THRESHOLD = 1.00005 # 0.5 bps, should be >1 @@ -60,32 +57,34 @@ def account_load_pkey(fname): wallet = Account.from_key(account_load_pkey("keeper")) -def prove(boracle, prover): - # Apply latest available blockhash - tx = boracle.functions.apply().build_transaction( - { - "from": wallet.address, - "nonce": l2_web3.eth.get_transaction_count(wallet.address), - } - ) - signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet.key) - tx_hash = l2_web3.eth.send_raw_transaction(signed_tx.raw_transaction) - l2_web3.eth.wait_for_transaction_receipt(tx_hash) - tx_receipt = l2_web3.eth.get_transaction_receipt(tx_hash) - number = -1 - for log in tx_receipt["logs"]: - if log["address"] == boracle.address: - if log["topics"][0].hex() == APPLY_BLOCK_HASH: - number = int(log["topics"][1].hex(), 16) - break - if log["topics"][0].hex() == COMMIT_BLOCK_HASH: - number = int(log["topics"][2].hex(), 16) - break - assert number > 0, "Applied block number not retrieved" - print(f"Applied block: {number}") +def prove(boracle, prover, block_number=None): + if not block_number: + # Apply latest available blockhash + tx = boracle.functions.apply().build_transaction( + { + "from": wallet.address, + "nonce": l2_web3.eth.get_transaction_count(wallet.address), + } + ) + signed_tx = l2_web3.eth.account.sign_transaction(tx, private_key=wallet.key) + tx_hash = l2_web3.eth.send_raw_transaction(signed_tx.raw_transaction) + l2_web3.eth.wait_for_transaction_receipt(tx_hash) + tx_receipt = l2_web3.eth.get_transaction_receipt(tx_hash) + block_number = -1 + for log in tx_receipt["logs"]: + if log["address"] == boracle.address: + if log["topics"][0].hex() == APPLY_BLOCK_HASH: + block_number = int(log["topics"][1].hex(), 16) + break + if log["topics"][0].hex() == COMMIT_BLOCK_HASH: + block_number = int(log["topics"][2].hex(), 16) + break + assert block_number > 0, "Applied block number not retrieved" + print(f"Applied block: {block_number}") + time.sleep(1) # Generate and submit proof for applied blockhash - proofs = generate_proof(eth_web3, number) + proofs = generate_proof(eth_web3, block_number) tx = prover.functions.prove(bytes.fromhex(proofs[0]), bytes.fromhex(proofs[1])).build_transaction( { "from": wallet.address,