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;
}
26 changes: 26 additions & 0 deletions contracts/test/VettedGateStub.sol
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);
}
}
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()
136 changes: 136 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,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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this constraint, a factory named IdentifiedCommunityStakerSetTreeParams cannot be deployed, as its name exceeds the 32-character limit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use ICSSetTreeParams

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s OK, but the name "IdentifiedCommunityStakerSetTreeParams" was used in the test and mentioned in a comment in the contract. If the deploy script doesn’t actually allow creating a factory with that name, it can be a bit misleading.

Copy link
Contributor Author

@vgorkavenko vgorkavenko Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will remove the restriction from the deploy script

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Psirex Added scenario test and removed the restriction

raise ValueError("Factory name must be less than 32 characters")

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

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