Skip to content
108 changes: 108 additions & 0 deletions contracts/EVMScriptFactories/CSMSetVettedGateTree.sol
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)
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);
}
}
19 changes: 19 additions & 0 deletions contracts/interfaces/IVettedGate.sol
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;
}
35 changes: 35 additions & 0 deletions contracts/test/VettedGateStub.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.4;

import "OpenZeppelin/[email protected]/contracts/access/AccessControl.sol";

/// @author vgorkavenko
/// @notice Helper contract with stub implementation of VettedGate for testing
contract VettedGateStub is AccessControl {
bytes32 public constant SET_TREE_ROLE = keccak256("SET_TREE_ROLE");

bytes32 public treeRoot;
string public treeCid;

event TreeParamsSet(bytes32 indexed treeRoot, string treeCid);

constructor() {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(SET_TREE_ROLE, msg.sender);
}

/// @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 onlyRole(SET_TREE_ROLE) {
treeRoot = _treeRoot;
treeCid = _treeCid;

emit TreeParamsSet(_treeRoot, _treeCid);
}
}
13 changes: 12 additions & 1 deletion deployed-csm-holesky.json
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
{"CSMSettleElStealingPenalty": {"contract": "CSMSettleElStealingPenalty", "address": "0x07696EA8A5b53C3E35d9cce10cc62c6c79C4691D", "constructorArgs": ["0xc4DAB3a3ef68C6DFd8614a870D64D475bA44F164", "0x4562c3e63c2e586cD1651B958C22F88135aCAd4f"]}}
{
"CSMSettleElStealingPenalty": {
"contract": "CSMSettleElStealingPenalty",
"address": "0x07696EA8A5b53C3E35d9cce10cc62c6c79C4691D",
"constructorArgs": ["0xc4DAB3a3ef68C6DFd8614a870D64D475bA44F164", "0x4562c3e63c2e586cD1651B958C22F88135aCAd4f"]
},
"CSMSetVettedGateTree": {
"contract": "CSMSetVettedGateTree",
"address": "0x26CDa8f9D84956efC743c8432658Ae9a5B7939da",
"constructorArgs": ["0xc4DAB3a3ef68C6DFd8614a870D64D475bA44F164", "IdentifiedCommunityStakerSetTreeParams", "0x92A5aB5e4f98e67Fb7295fe439A652d0E51033bf"]
}
}
9 changes: 9 additions & 0 deletions deployed-csm-hoodi.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,14 @@
"0x4af43ee34a6fcd1feca1e1f832124c763561da53",
"0x79CEf36D84743222f37765204Bec41E92a93E59d"
]
},
"CSMSetVettedGateTree": {
"contract": "CSMSetVettedGateTree",
"address": "0xa890fc73e1b771Ee6073e2402E631c312FF92Cd9",
"constructorArgs": [
"0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53",
"IdentifiedCommunityStakerSetTreeParams",
"0x10a254E724fe2b7f305F76f3F116a3969c53845f"
]
}
}
55 changes: 55 additions & 0 deletions scripts/acceptance_test_csm_set_vetted_gate_tree_setup.py
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()
141 changes: 141 additions & 0 deletions scripts/deploy_csm_set_vetted_gate_tree_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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")

return factory_name


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...")

artifacts_path = f"deployed-csm-{network_name}.json"

if os.path.exists(artifacts_path):
with open(artifacts_path, "r") as previous_artifacts:
existing_artifacts = json.load(previous_artifacts)
deployment_artifacts.update(existing_artifacts)

with open(artifacts_path, "w") as outfile:
json.dump(deployment_artifacts, outfile, indent=4)

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()
Loading
Loading