-
Notifications
You must be signed in to change notification settings - Fork 9
feat: factory for setting tree in CSM's VettedGate
#91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 4 commits
22036b7
37c3299
1a4b4fc
e36fee1
631e27f
a59a904
571111f
baf4798
86327d6
12cd20d
9e11df4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // SPDX-FileCopyrightText: 2025 Lido <[email protected]> | ||
| // SPDX-License-Identifier: GPL-3.0 | ||
|
|
||
| pragma solidity 0.8.6; | ||
|
|
||
| import "../TrustedCaller.sol"; | ||
| import "../libraries/EVMScriptCreator.sol"; | ||
| import "../interfaces/IEVMScriptFactory.sol"; | ||
| import "../interfaces/IVettedGate.sol"; | ||
|
|
||
| /// @author vgorkavenko | ||
| /// @notice Creates EVMScript to set tree for CSM's VettedGate | ||
| contract CSMSetVettedGateTree is TrustedCaller, IEVMScriptFactory { | ||
|
|
||
| // ------------- | ||
| // ERRORS | ||
| // ------------- | ||
|
|
||
| string private constant ERROR_EMPTY_TREE_ROOT = | ||
| "EMPTY_TREE_ROOT"; | ||
| string private constant ERROR_EMPTY_TREE_CID = | ||
| "EMPTY_TREE_CID"; | ||
| string private constant ERROR_SAME_TREE_CID = | ||
| "SAME_TREE_CID"; | ||
| string private constant ERROR_SAME_TREE_ROOT = | ||
| "SAME_TREE_ROOT"; | ||
|
|
||
| // ------------- | ||
| // VARIABLES | ||
| // ------------- | ||
|
|
||
| /// @notice Alias for factory (e.g. "IdentifiedCommunityStakerSetTreeParams") | ||
| string public name; | ||
|
|
||
| /// @notice Address of VettedGate | ||
| IVettedGate public immutable vettedGate; | ||
|
|
||
| // ------------- | ||
| // CONSTRUCTOR | ||
| // ------------- | ||
|
|
||
| constructor(address _trustedCaller, string memory _name, address _vettedGate) | ||
dgusakov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| TrustedCaller(_trustedCaller) | ||
| { | ||
| name = _name; | ||
| vettedGate = IVettedGate(_vettedGate); | ||
| } | ||
|
|
||
| // ------------- | ||
| // EXTERNAL METHODS | ||
| // ------------- | ||
|
|
||
| /// @notice Creates EVMScript to set treeRoot and treeCid for CSM's VettedGate | ||
| /// @param _creator Address who creates EVMScript | ||
| /// @param _evmScriptCallData Encoded: bytes32 treeRoot and string treeCid | ||
| function createEVMScript(address _creator, bytes calldata _evmScriptCallData) | ||
| external | ||
| view | ||
| override | ||
| onlyTrustedCaller(_creator) | ||
| returns (bytes memory) | ||
| { | ||
| (bytes32 treeRoot, string memory treeCid) = _decodeEVMScriptCallData(_evmScriptCallData); | ||
|
|
||
| _validateInputData(treeRoot, treeCid); | ||
|
|
||
| return | ||
| EVMScriptCreator.createEVMScript( | ||
| address(vettedGate), | ||
| IVettedGate.setTreeParams.selector, | ||
| _evmScriptCallData | ||
| ); | ||
| } | ||
|
|
||
| /// @notice Decodes call data used by createEVMScript method | ||
| /// @param _evmScriptCallData Encoded: bytes32 treeRoot and string treeCid | ||
| /// @return treeRoot The root of the tree | ||
| /// @return treeCid The CID of the tree | ||
| function decodeEVMScriptCallData(bytes calldata _evmScriptCallData) | ||
| external | ||
| pure | ||
| returns (bytes32, string memory) | ||
| { | ||
| return _decodeEVMScriptCallData(_evmScriptCallData); | ||
| } | ||
|
|
||
| // ------------------ | ||
| // PRIVATE METHODS | ||
| // ------------------ | ||
|
|
||
| function _decodeEVMScriptCallData(bytes calldata _evmScriptCallData) | ||
| private | ||
| pure | ||
| returns (bytes32, string memory) | ||
| { | ||
| return abi.decode(_evmScriptCallData, (bytes32, string)); | ||
| } | ||
|
|
||
| function _validateInputData( | ||
| bytes32 treeRoot, | ||
| string memory treeCid | ||
| ) private view { | ||
| require(treeRoot != bytes32(0), ERROR_EMPTY_TREE_ROOT); | ||
| require(bytes(treeCid).length > 0, ERROR_EMPTY_TREE_CID); | ||
| require(treeRoot != vettedGate.treeRoot(), ERROR_SAME_TREE_ROOT); | ||
| require(keccak256(bytes(treeCid)) != keccak256(bytes(vettedGate.treeCid())), ERROR_SAME_TREE_CID); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // SPDX-FileCopyrightText: 2025 Lido <[email protected]> | ||
| // SPDX-License-Identifier: GPL-3.0 | ||
|
|
||
| pragma solidity 0.8.6; | ||
|
|
||
| /// @title Lido's Community Staking Module Vetted Gate interface | ||
| interface IVettedGate { | ||
|
|
||
| function treeRoot() external view returns (bytes32); | ||
| function treeCid() external view returns (string memory); | ||
|
|
||
| /// @notice Set the root of the eligible members Merkle Tree | ||
| /// @param _treeRoot New root of the Merkle Tree | ||
| /// @param _treeCid New CID of the Merkle Tree | ||
| function setTreeParams( | ||
| bytes32 _treeRoot, | ||
| string calldata _treeCid | ||
| ) external; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // SPDX-FileCopyrightText: 2025 Lido <[email protected]> | ||
| // SPDX-License-Identifier: GPL-3.0 | ||
|
|
||
| pragma solidity ^0.8.4; | ||
|
|
||
| /// @author vgorkavenko | ||
| /// @notice Helper contract with stub implementation of VettedGate for testing | ||
| contract VettedGateStub { | ||
| bytes32 public treeRoot; | ||
| string public treeCid; | ||
|
|
||
| event TreeParamsSet(bytes32 indexed treeRoot, string treeCid); | ||
|
|
||
| /// @notice Set the root of the eligible members Merkle Tree | ||
| /// @param _treeRoot New root of the Merkle Tree | ||
| /// @param _treeCid New CID of the Merkle Tree | ||
| function setTreeParams( | ||
| bytes32 _treeRoot, | ||
| string calldata _treeCid | ||
| ) external { | ||
| treeRoot = _treeRoot; | ||
| treeCid = _treeCid; | ||
|
|
||
| emit TreeParamsSet(_treeRoot, _treeCid); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| from dataclasses import dataclass | ||
|
|
||
| from brownie import chain, CSMSetVettedGateTree | ||
|
|
||
| from utils import log | ||
|
|
||
|
|
||
| @dataclass | ||
| class DeployConfig: | ||
| trusted_caller: str | ||
| factory_name: str | ||
| vetted_gate_address: str | ||
|
|
||
|
|
||
| deploy_config = DeployConfig( | ||
| trusted_caller="", | ||
| factory_name="", | ||
| vetted_gate_address="" | ||
| ) | ||
|
|
||
|
|
||
| deployment_tx_hash = "" | ||
|
|
||
|
|
||
| def main(): | ||
|
|
||
| tx = chain.get_transaction(deployment_tx_hash) | ||
|
|
||
| log.br() | ||
|
|
||
| log.nb("tx of creation", deployment_tx_hash) | ||
|
|
||
| log.br() | ||
|
|
||
| log.nb("trusted_caller", deploy_config.trusted_caller) | ||
| log.nb("factory_name", deploy_config.factory_name) | ||
| log.nb("vetted_gate_address", deploy_config.vetted_gate_address) | ||
|
|
||
| log.br() | ||
|
|
||
| set_vetted_gate_tree_factory = CSMSetVettedGateTree.at(tx.contract_address) | ||
| log.nb('CSMSetVettedGateTree address (from tx)', set_vetted_gate_tree_factory) | ||
|
|
||
| log.br() | ||
|
|
||
| assert set_vetted_gate_tree_factory.vettedGate() == deploy_config.vetted_gate_address | ||
| log.nb('VettedGate address is correct') | ||
|
|
||
| assert set_vetted_gate_tree_factory.trustedCaller() == deploy_config.trusted_caller | ||
| log.nb('Trusted caller is correct') | ||
|
|
||
| assert set_vetted_gate_tree_factory.name() == deploy_config.factory_name | ||
| log.nb('Factory name is correct') | ||
|
|
||
| log.br() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import json | ||
| import os | ||
|
|
||
| from brownie import ( | ||
| chain, | ||
| web3, | ||
| accounts, | ||
| CSMSetVettedGateTree | ||
| ) | ||
|
|
||
| from utils import log | ||
| from utils.config import ( | ||
| get_env, | ||
| get_is_live, | ||
| get_network_name, | ||
| get_deployer_account, | ||
| prompt_bool, | ||
| ) | ||
|
|
||
| def check_etherscan_token(): | ||
| if "ETHERSCAN_TOKEN" not in os.environ: | ||
| raise EnvironmentError("Please set ETHERSCAN_TOKEN env variable") | ||
| etherscan_token = os.environ["ETHERSCAN_TOKEN"] | ||
|
|
||
| assert etherscan_token, "Etherscan API token is not valid" | ||
|
|
||
| return etherscan_token | ||
|
|
||
|
|
||
| def get_trusted_caller(): | ||
| if "TRUSTED_CALLER" not in os.environ: | ||
| raise EnvironmentError("Please set TRUSTED_CALLER env variable") | ||
| trusted_caller = os.environ["TRUSTED_CALLER"] | ||
|
|
||
| assert web3.is_address(trusted_caller), "Trusted caller address is not valid" | ||
|
|
||
| return trusted_caller | ||
|
|
||
|
|
||
| def get_factory_name(): | ||
| if "FACTORY_NAME" not in os.environ: | ||
| raise EnvironmentError("Please set FACTORY_NAME env variable") | ||
|
|
||
| factory_name = os.environ["FACTORY_NAME"] | ||
|
|
||
| if not factory_name: | ||
| raise ValueError("Factory name cannot be empty") | ||
| if not isinstance(factory_name, str): | ||
| raise TypeError("Factory name must be a string") | ||
| if len(factory_name) > 32: | ||
|
||
| raise ValueError("Factory name must be less than 32 characters") | ||
|
|
||
| return factory_name | ||
Psirex marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def get_vetted_gate_address(): | ||
| if "VETTED_GATE_ADDRESS" not in os.environ: | ||
| raise EnvironmentError("Please set VETTED_GATE_ADDRESS env variable") | ||
| vetted_gate_address = os.environ["VETTED_GATE_ADDRESS"] | ||
|
|
||
| assert web3.is_address(vetted_gate_address), "VettedGate address is not valid" | ||
|
|
||
| return vetted_gate_address | ||
|
|
||
|
|
||
| def main(): | ||
| if get_is_live() and get_env("FORCE_VERIFY", False): | ||
| check_etherscan_token() | ||
|
|
||
| network_name = get_network_name() | ||
|
|
||
| deployer = get_deployer_account(get_is_live(), network=network_name, dev_ldo_transfer=False) | ||
| trusted_caller = get_trusted_caller() | ||
| factory_name = get_factory_name() | ||
| vetted_gate_address = get_vetted_gate_address() | ||
|
|
||
| log.br() | ||
|
|
||
| log.nb("Current network", network_name, color_hl=log.color_magenta) | ||
| log.nb("chain id", chain.id) | ||
|
|
||
| log.br() | ||
|
|
||
| log.ok("Deployer", deployer) | ||
|
|
||
| log.br() | ||
|
|
||
| log.ok("VettedGate address", vetted_gate_address) | ||
| log.ok("Trusted caller", trusted_caller) | ||
| log.ok("Factory name", factory_name) | ||
|
|
||
| log.br() | ||
|
|
||
| print("Proceed? [yes/no]: ") | ||
|
|
||
| if not prompt_bool(): | ||
| log.nb("Aborting") | ||
| return | ||
|
|
||
| log.br() | ||
|
|
||
| deployment_artifacts = {} | ||
|
|
||
| # Gas parameters following project conventions | ||
| tx_params = {"from": deployer, "priority_fee": "2 gwei", "max_fee": "50 gwei"} | ||
|
|
||
| log.nb("Deploying CSMSetVettedGateTree...") | ||
|
|
||
| csm_set_vetted_gate_tree = CSMSetVettedGateTree.deploy( | ||
| trusted_caller, | ||
| factory_name, | ||
| vetted_gate_address, | ||
| tx_params | ||
| ) | ||
| deployment_artifacts["CSMSetVettedGateTree"] = { | ||
| "contract": "CSMSetVettedGateTree", | ||
| "address": csm_set_vetted_gate_tree.address, | ||
| "constructorArgs": [trusted_caller, factory_name, vetted_gate_address], | ||
| } | ||
|
|
||
| log.ok("Deployed CSMSetVettedGateTree", csm_set_vetted_gate_tree.address) | ||
|
|
||
| log.br() | ||
| log.nb("All factories have been deployed.") | ||
| log.nb("Saving artifacts...") | ||
|
|
||
| with open(f"deployed-csm-{network_name}.json", "w") as outfile: | ||
| json.dump(deployment_artifacts, outfile) | ||
|
|
||
| if get_is_live() and get_env("FORCE_VERIFY", False): | ||
| log.nb("Starting code verification.") | ||
| log.br() | ||
|
|
||
| CSMSetVettedGateTree.publish_source(csm_set_vetted_gate_tree) | ||
|
|
||
| log.br() | ||
Uh oh!
There was an error while loading. Please reload this page.