GovernorTimelock

Description:

Governance contract for decentralized decision-making with timelock mechanism for delayed execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "contracts/GovernorTimelock.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.25;

import { ERC20Helper } from "../modules/erc20-helper/src/ERC20Helper.sol";

import { IGovernorTimelock } from "./interfaces/IGovernorTimelock.sol";

contract GovernorTimelock is IGovernorTimelock {

    /**************************************************************************************************************************************/
    /*** Storage                                                                                                                        ***/
    /**************************************************************************************************************************************/

    bytes32 public override constant PROPOSER_ROLE  = keccak256("PROPOSER_ROLE");
    bytes32 public override constant EXECUTOR_ROLE  = keccak256("EXECUTOR_ROLE");
    bytes32 public override constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
    bytes32 public override constant ROLE_ADMIN     = keccak256("ROLE_ADMIN");

    uint32 public override constant MIN_DELAY            = 1 days;
    uint32 public override constant MIN_EXECUTION_WINDOW = 1 days;

    address public override pendingTokenWithdrawer;
    address public override tokenWithdrawer;

    uint256 public override latestProposalId;

    TimelockParameters public override defaultTimelockParameters;

    mapping(uint256 => Proposal) public override proposals;

    mapping(address => mapping(bytes32 => bool)) public override hasRole;

    mapping(address => mapping(bytes4 => TimelockParameters)) public override functionTimelockParameters;

    /**************************************************************************************************************************************/
    /*** Modifiers                                                                                                                      ***/
    /**************************************************************************************************************************************/

    modifier onlyRole(bytes32 role_) {
        require(hasRole[msg.sender][role_], "GT:NOT_AUTHORIZED");

        _;
    }

    modifier onlySelf() {
        require(msg.sender == address(this), "GT:NOT_SELF");

        _;
    }

    /**************************************************************************************************************************************/
    /*** Constructor                                                                                                                    ***/
    /**************************************************************************************************************************************/

    constructor(address tokenWithdrawer_, address proposer_, address executor_, address canceller_, address roleAdmin_) {
        tokenWithdrawer           = tokenWithdrawer_;
        defaultTimelockParameters = TimelockParameters({ delay: MIN_DELAY, executionWindow: MIN_EXECUTION_WINDOW });

        emit DefaultTimelockSet(MIN_DELAY, MIN_EXECUTION_WINDOW);

        _updateRole(PROPOSER_ROLE,  proposer_,  true);
        _updateRole(EXECUTOR_ROLE,  executor_,  true);
        _updateRole(CANCELLER_ROLE, canceller_, true);
        _updateRole(ROLE_ADMIN,     roleAdmin_, true);
    }

    /**************************************************************************************************************************************/
    /*** Token Withdrawer Functions                                                                                                     ***/
    /**************************************************************************************************************************************/

    function acceptTokenWithdrawer() external override {
        address newTokenWithdrawer_ = pendingTokenWithdrawer;

        require(msg.sender == newTokenWithdrawer_, "GT:ATW:NOT_AUTHORIZED");

        tokenWithdrawer        = newTokenWithdrawer_;
        pendingTokenWithdrawer = address(0);

        emit TokenWithdrawerAccepted(newTokenWithdrawer_);
    }

    function setPendingTokenWithdrawer(address newPendingTokenWithdrawer_) external override onlyRole(ROLE_ADMIN) {
        pendingTokenWithdrawer = newPendingTokenWithdrawer_;

        emit PendingTokenWithdrawerSet(newPendingTokenWithdrawer_);
    }

    function withdrawERC20Token(address token_, uint256 amount_) external override {
        require(msg.sender == tokenWithdrawer,                     "GT:WET:NOT_AUTHORIZED");
        require(ERC20Helper.transfer(token_, msg.sender, amount_), "GT:WET:TRANSFER_FAILED");

        emit ERC20TokenWithdrawn(token_, msg.sender, amount_);
    }

    /**************************************************************************************************************************************/
    /*** Timelock Configuration                                                                                                         ***/
    /**************************************************************************************************************************************/

    function setDefaultTimelockParameters(uint32 delay_, uint32 executionWindow_) external override onlySelf {
        require(delay_           >= MIN_DELAY,            "GT:SDTP:INVALID_DELAY");
        require(executionWindow_ >= MIN_EXECUTION_WINDOW, "GT:SDTP:INVALID_EXEC_WINDOW");

        defaultTimelockParameters = TimelockParameters({ delay: delay_, executionWindow: executionWindow_ });

        emit DefaultTimelockSet(delay_, executionWindow_);
    }

    function setFunctionTimelockParameters(
        address target_,
        bytes4  functionSelector_,
        uint32  delay_,
        uint32  executionWindow_
    )
        external override onlySelf
    {
         // Both delay_ & executionWindow_ must be zero to use defaults, or both must meet minimums.
        require(
            (delay_ == 0 && executionWindow_ == 0) ||
            (delay_ >= MIN_DELAY && executionWindow_ >= MIN_EXECUTION_WINDOW),
            "GT:SFTP:INVALID_PARAMETERS"
        );

        functionTimelockParameters[target_][functionSelector_] = TimelockParameters({ delay: delay_, executionWindow: executionWindow_ });

        emit FunctionTimelockSet(target_, functionSelector_, delay_, executionWindow_);
    }

    /**************************************************************************************************************************************/
    /*** Role Management                                                                                                                ***/
    /**************************************************************************************************************************************/

    function updateRole(bytes32 role_, address account_, bool grantRole_) external override onlySelf {
        _updateRole(role_, account_, grantRole_);
    }

    function proposeRoleUpdates(
        bytes32[] calldata roles_,
        address[] calldata accounts_,
        bool[]    calldata shouldGrant_
    )
        external override onlyRole(ROLE_ADMIN)
    {
        require(roles_.length > 0,                    "GT:PRU:EMPTY_ARRAY");
        require(roles_.length == accounts_.length,    "GT:PRU:INVALID_ACCOUNTS_LENGTH");
        require(roles_.length == shouldGrant_.length, "GT:PRU:INVALID_SHOULD_GRANT_LENGTH");

        for (uint256 i = 0; i < roles_.length; i++) {
            _scheduleProposal(address(this), this.updateRole.selector, abi.encode(roles_[i], accounts_[i], shouldGrant_[i]));
        }
    }

    /**************************************************************************************************************************************/
    /*** Proposal Management                                                                                                            ***/
    /**************************************************************************************************************************************/

    function executeProposals(
        uint256[] calldata proposalIds_,
        address[] calldata targets_,
        bytes[]   calldata data_
    )
        external override onlyRole(EXECUTOR_ROLE)
    {
        require(proposalIds_.length != 0,               "GT:EP:EMPTY_ARRAY");
        require(proposalIds_.length == targets_.length, "GT:EP:INVALID_TARGETS_LENGTH");
        require(proposalIds_.length == data_.length,    "GT:EP:INVALID_DATA_LENGTH");

        for (uint256 i = 0; i < proposalIds_.length; i++) {
            Proposal memory proposal_     = proposals[proposalIds_[i]];
            bytes32 expectedProposalHash_ = keccak256(abi.encode(targets_[i], data_[i]));

            require(proposals[proposalIds_[i]].proposalHash != bytes32(0), "GT:EP:PROPOSAL_NOT_FOUND");
            require(isExecutable(proposalIds_[i]),                         "GT:EP:NOT_EXECUTABLE");
            require(expectedProposalHash_ == proposal_.proposalHash,       "GT:EP:INVALID_DATA");

            delete proposals[proposalIds_[i]];

            _call(targets_[i], data_[i]);

            emit ProposalExecuted(proposalIds_[i]);
        }
    }

    function scheduleProposals(address[] calldata targets_, bytes[] calldata data_) external override onlyRole(PROPOSER_ROLE) {
        require(targets_.length != 0,            "GT:SP:EMPTY_ARRAY");
        require(targets_.length == data_.length, "GT:SP:ARRAY_LENGTH_MISMATCH");

        for (uint256 i = 0; i < targets_.length; i++) {
            bytes4 selector_     = bytes4(data_[i][:4]);
            bytes memory params_ = data_[i][4:];

            require(!_isUpdatingRoles(targets_[i], selector_), "GT:SP:UPDATE_ROLE_NOT_ALLOWED");

            _scheduleProposal(targets_[i], selector_, params_);
        }
    }

    function unscheduleProposals(uint256[] calldata proposalIds_) external override onlyRole(CANCELLER_ROLE) {
        for (uint256 i = 0; i < proposalIds_.length; i++) {
            require(proposals[proposalIds_[i]].proposalHash != 0, "GT:UP:PROPOSAL_NOT_FOUND");
            require(proposals[proposalIds_[i]].isUnschedulable,   "GT:UP:NOT_UNSCHEDULABLE");

            delete proposals[proposalIds_[i]];

            emit ProposalUnscheduled(proposalIds_[i]);
        }
    }

    /**************************************************************************************************************************************/
    /*** View Functions                                                                                                                 ***/
    /**************************************************************************************************************************************/

    function isExecutable(uint256 proposalId_) public override view returns (bool isExecutable_) {
        isExecutable_ = block.timestamp >= proposals[proposalId_].delayedUntil && block.timestamp <= proposals[proposalId_].validUntil;
    }

    /**************************************************************************************************************************************/
    /*** Internal Functions                                                                                                             ***/
    /**************************************************************************************************************************************/

    function _call(address target_, bytes calldata calldata_) internal {
        ( bool success_, bytes memory returndata_ ) = target_.call(calldata_);

        if (success_) {
            return;
        }

        if (returndata_.length > 0) {
            assembly ("memory-safe") {
                let size_ := mload(returndata_)
                revert(add(32, returndata_), size_)
            }
        } else {
            revert("GT:EP:CALL_FAILED");
        }
    }

    function _getTimelockParameters(
        address target_, bytes4 selector_, bytes memory parameters_
    )
        internal view returns (TimelockParameters memory timelockParameters_)
    {
        // Use prior timelock params if set when updating timelock params.
        if (target_ == address(this) && selector_ == this.setFunctionTimelockParameters.selector) {
            ( target_, selector_, , ) = abi.decode(parameters_, (address, bytes4, uint32, uint32));
        }

        uint32 functionDelay_ = functionTimelockParameters[target_][selector_].delay;

        timelockParameters_ =
            functionDelay_ == 0 ? defaultTimelockParameters : functionTimelockParameters[target_][selector_];
    }

    function _isUpdatingRoles(address target_, bytes4 selector_) internal view returns (bool isUpdatingRoles_) {
        isUpdatingRoles_ = target_ == address(this) && selector_ == this.updateRole.selector;
    }

    function _updateRole(bytes32 role_, address account_, bool grantRole_) internal {
        require(hasRole[account_][role_] != grantRole_, "GT:UR:ROLE_NOT_CHANGED");

        hasRole[account_][role_] = grantRole_;

        emit RoleUpdated(role_, account_, grantRole_);
    }

    function _scheduleProposal(address target_, bytes4 selector_, bytes memory parameters_) internal {
        require(target_.code.length > 0, "GT:SP:EMPTY_ADDRESS");

        TimelockParameters memory timelockParameters_ = _getTimelockParameters(target_, selector_, parameters_);

        Proposal memory proposal_ = Proposal({
            proposalHash:    keccak256(abi.encode(target_, bytes.concat(selector_, parameters_))),
            scheduledAt:     uint32(block.timestamp),
            delayedUntil:    uint32(block.timestamp) + timelockParameters_.delay,
            validUntil:      uint32(block.timestamp) + timelockParameters_.delay + timelockParameters_.executionWindow,
            isUnschedulable: !_isUpdatingRoles(target_, selector_)
        });

        uint256 latestProposalId_ = ++latestProposalId;

        proposals[latestProposalId_] = proposal_;

        emit ProposalScheduled(latestProposalId_, proposal_);
    }

}
"
    },
    "modules/erc20-helper/src/ERC20Helper.sol": {
      "content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.7;

import { IERC20Like } from "./interfaces/IERC20Like.sol";

/**
 * @title Small Library to standardize erc20 token interactions.
 */
library ERC20Helper {

    /**************************************************************************************************************************************/
    /*** Internal Functions                                                                                                             ***/
    /**************************************************************************************************************************************/

    function transfer(address token_, address to_, uint256 amount_) internal returns (bool success_) {
        return _call(token_, abi.encodeWithSelector(IERC20Like.transfer.selector, to_, amount_));
    }

    function transferFrom(address token_, address from_, address to_, uint256 amount_) internal returns (bool success_) {
        return _call(token_, abi.encodeWithSelector(IERC20Like.transferFrom.selector, from_, to_, amount_));
    }

    function approve(address token_, address spender_, uint256 amount_) internal returns (bool success_) {
        // If setting approval to zero fails, return false.
        if (!_call(token_, abi.encodeWithSelector(IERC20Like.approve.selector, spender_, uint256(0)))) return false;

        // If `amount_` is zero, return true as the previous step already did this.
        if (amount_ == uint256(0)) return true;

        // Return the result of setting the approval to `amount_`.
        return _call(token_, abi.encodeWithSelector(IERC20Like.approve.selector, spender_, amount_));
    }

    function _call(address token_, bytes memory data_) private returns (bool success_) {
        if (token_.code.length == uint256(0)) return false;

        bytes memory returnData;
        ( success_, returnData ) = token_.call(data_);

        return success_ && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));
    }

}
"
    },
    "contracts/interfaces/IGovernorTimelock.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.7;

interface IGovernorTimelock {

    /**************************************************************************************************************************************/
    /*** Structs                                                                                                                        ***/
    /**************************************************************************************************************************************/

    struct Proposal {
        bytes32 proposalHash;
        bool    isUnschedulable;
        uint32  scheduledAt;
        uint32  delayedUntil;
        uint32  validUntil;
    }

    struct TimelockParameters {
        uint32 delay;
        uint32 executionWindow;
    }

    /**************************************************************************************************************************************/
    /*** Events                                                                                                                         ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Emitted when the default timelock parameters are set
     * @param  delay           The new default delay
     * @param  executionWindow The new default execution window
     */
    event DefaultTimelockSet(uint32 delay, uint32 executionWindow);

    /**
     * @notice Emitted when tokens are withdrawn from the governor timelock contract
     * @param  token    The address of the token withdrawn
     * @param  receiver The address of the receiver of the tokens
     * @param  amount   The amount of tokens withdrawn
     */
    event ERC20TokenWithdrawn(address indexed token, address indexed receiver, uint256 amount);

    /**
     * @notice Emitted when the function timelock parameters are set
     * @param  target           The target of the function
     * @param  functionSelector The function selector
     * @param  delay            The new delay
     * @param  executionWindow  The new execution window
     */
    event FunctionTimelockSet(address indexed target, bytes4  indexed functionSelector, uint32 delay, uint32 executionWindow);

    /**
     * @notice Emitted when the pending token withdrawer is set
     * @param  newPendingTokenWithdrawer The address of the new pending token withdrawer
     */
    event PendingTokenWithdrawerSet(address indexed newPendingTokenWithdrawer);

    /**
     * @notice Emitted when a proposal is executed
     * @param  proposalId The id of the proposal
     */
    event ProposalExecuted(uint256 indexed proposalId);

    /**
     * @notice Emitted when a proposal is scheduled
     * @param  proposalId The id of the proposal
     * @param  proposal   The proposal
     */
    event ProposalScheduled(uint256 indexed proposalId, Proposal proposal);

    /**
     * @notice Emitted when a proposal is unscheduled
     * @param  proposalId The id of the proposal
     */
    event ProposalUnscheduled(uint256 indexed proposalId);

    /**
     * @notice Emitted when a role is updated
     * @param  role      The role updated
     * @param  account   The account updated the role for
     * @param  grantRole Whether the role is granted or revoked
     */
    event RoleUpdated(bytes32 indexed role, address indexed account, bool grantRole);

    /**
     * @notice Emitted when the token withdrawer is accepted
     * @param  tokenWithdrawer The address of the new token withdrawer
     */
    event TokenWithdrawerAccepted(address indexed tokenWithdrawer);

    /**************************************************************************************************************************************/
    /*** Role constants                                                                                                                 ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Returns the bytes32 representation of the canceler role
     * @dev    Address that has the canceler role can unschedule proposals but can not unschedule role updates
     * @return cancelerRole The canceler role
     */
    function CANCELLER_ROLE() external view returns (bytes32 cancelerRole);

    /**
     * @notice Returns the bytes32 representation of the executor role
     * @dev    Address that has the executor role can execute all proposals including role updates
     * @return executorRole The executor role
     */
    function EXECUTOR_ROLE() external view returns (bytes32 executorRole);

    /**
     * @notice Returns the bytes32 representation of the proposer role
     * @dev    Address that has the proposer role can schedule proposals but can not schedule role updates
     * @return proposerRole The proposer role
     */
    function PROPOSER_ROLE() external view returns (bytes32 proposerRole);

    /**
     * @notice Returns the bytes32 representation of the role admin role
     * @dev    Address that has the role admin role can update roles including the role admin role itself
     * @return roleAdmin The role admin role
     */
    function ROLE_ADMIN() external view returns (bytes32 roleAdmin);

    /**************************************************************************************************************************************/
    /*** Timelock constants                                                                                                             ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Returns the minimum delay for a proposal
     * @return minDelay The minimum delay for a proposal
     */
    function MIN_DELAY() external view returns (uint32 minDelay);

    /**
     * @notice Returns the minimum execution window for a proposal
     * @return minExecutionWindow The minimum execution window for a proposal
     */
    function MIN_EXECUTION_WINDOW() external view returns (uint32 minExecutionWindow);

    /**************************************************************************************************************************************/
    /*** View Functions                                                                                                                 ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Returns the default timelock parameters
     * @return delay           The delay
     * @return executionWindow The execution window
     */
    function defaultTimelockParameters() external view returns (uint32 delay, uint32 executionWindow);

    /**
     * @notice Returns the timelock parameters for a given target and function selector
     * @param  target           The target of the function
     * @param  functionSelector The function selector
     * @return delay            The delay
     * @return executionWindow  The execution window
     */
    function functionTimelockParameters(
        address target,
        bytes4 functionSelector
    ) external view returns (uint32 delay, uint32 executionWindow);

    /**
     * @notice Checks if an account has a role
     * @param  account      The account to check
     * @param  role         The role to check
     * @return doesHaveRole Whether the account has the role
     */
    function hasRole(address account, bytes32 role) external view returns (bool doesHaveRole);

    /**
     * @notice Checks if a proposal is executable
     * @param  proposalId   The id of the proposal
     * @return isExecutable Whether the proposal is executable
     */
    function isExecutable(uint256 proposalId) external view returns (bool isExecutable);

    /**
     * @notice Returns the latest proposal id
     * @return latestProposalId The latest proposal id
     */
    function latestProposalId() external view returns (uint256 latestProposalId);

    /**
     * @notice Returns the pending token withdrawer
     * @return pendingTokenWithdrawer The address of the pending token withdrawer
     */
    function pendingTokenWithdrawer() external view returns (address pendingTokenWithdrawer);

    /**
     * @notice Returns the proposal for a given proposal id
     * @param  proposalId The id of the proposal
     * @return proposalHash    The hash of the proposal
     * @return isUnschedulable Whether the proposal is unschedulable
     * @return scheduledAt     The timestamp when the proposal was scheduled
     * @return delayedUntil    The timestamp when the proposal was delayed
     * @return validUntil      The timestamp when the proposal was valid
     */
    function proposals(uint256 proposalId) external view returns (
        bytes32 proposalHash,
        bool    isUnschedulable,
        uint32  scheduledAt,
        uint32  delayedUntil,
        uint32  validUntil
    );

    /**
     * @notice Returns the token withdrawer
     * @return tokenWithdrawer The address of the token withdrawer
     */
    function tokenWithdrawer() external view returns (address tokenWithdrawer);

    /**************************************************************************************************************************************/
    /*** Token Withdrawer Functions                                                                                                     ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Accepts the token withdrawer role and sets pending token withdrawer to zero address
     * @dev    Only the pending token withdrawer can accept the token withdrawer role
     */
    function acceptTokenWithdrawer() external;

    /**
     * @notice Sets the pending token withdrawer
     * @dev    Only the token withdrawer can set the pending token withdrawer
     * @param  newPendingTokenWithdrawer The address of the new pending token withdrawer
     */
    function setPendingTokenWithdrawer(address newPendingTokenWithdrawer) external;

    /**
     * @notice Withdraws tokens from the governor timelock contract
     * @dev    Only the token withdrawer can withdraw tokens and tokens are sent to the token withdrawer
     * @param  token  The address of the token to withdraw
     * @param  amount The amount of tokens to withdraw
     */
    function withdrawERC20Token(address token, uint256 amount) external;

    /**************************************************************************************************************************************/
    /*** Timelock Configuration                                                                                                         ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Sets the default timelock parameters
     * @param  delay           The new default delay
     * @param  executionWindow The new default execution window
     */
    function setDefaultTimelockParameters(uint32 delay, uint32 executionWindow) external;

    /**
     * @notice Sets the function timelock parameters
     * @param  target           The target of the function
     * @param  functionSelector The function selector
     * @param  delay            The new delay
     * @param  executionWindow  The new execution window
     */
    function setFunctionTimelockParameters(address target, bytes4 functionSelector, uint32 delay, uint32 executionWindow) external;

    /**************************************************************************************************************************************/
    /*** Role Management                                                                                                                ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Updates a role
     * @dev    The role updating needs to be done through the timelock contract
     * @param  role      The role to update
     * @param  account   The account to update the role for
     * @param  grantRole Whether the role is granted or revoked
     */
    function updateRole(bytes32 role, address account, bool grantRole) external;

    /**************************************************************************************************************************************/
    /*** Proposal Management                                                                                                            ***/
    /**************************************************************************************************************************************/

    /**
     * @notice Executes proposals
     * @dev    The proposalIds, targets and data arrays must have the same length
     * @param  proposalIds The ids of the proposals to execute
     * @param  targets     The targets of the proposals
     * @param  data        The calldata of the proposals that the contract is going to execute
     */
    function executeProposals(
        uint256[] calldata proposalIds,
        address[] calldata targets,
        bytes[]   calldata data
    ) external;

    /**
     * @notice Proposes to update roles
     * @dev    The role updating needs to be done through the timelock contract
     * @param  roles       The roles to update
     * @param  accounts    The accounts to update the roles for
     * @param  shouldGrant Whether to grant or revoke the roles
     */
    function proposeRoleUpdates(bytes32[] calldata roles, address[] calldata accounts, bool[] calldata shouldGrant) external;

    /**
     * @notice Schedules proposals
     * @dev    The targets and data arrays must have the same length
     * @param  targets The targets of the proposals
     * @param  data    The calldata of the proposals that the contract is going to execute
     */
    function scheduleProposals(address[] calldata targets, bytes[] calldata data) external;

    /**
     * @notice Unschedule proposals
     * @dev    The proposalIds array must not contain duplicates
     * @param  proposalIds The ids of the proposals to unschedule
     */
    function unscheduleProposals(uint256[] calldata proposalIds) external;

}
"
    },
    "modules/erc20-helper/src/interfaces/IERC20Like.sol": {
      "content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.7;

/// @title Interface of the ERC20 standard as needed by ERC20Helper.
interface IERC20Like {

    function approve(address spender_, uint256 amount_) external returns (bool success_);

    function transfer(address recipient_, uint256 amount_) external returns (bool success_);

    function transferFrom(address owner_, address recipient_, uint256 amount_) external returns (bool success_);

}
"
    }
  },
  "settings": {
    "remappings": [
      "contract-test-utils/=modules/non-transparent-proxy/modules/contract-test-utils/contracts/",
      "ds-test/=modules/forge-std/lib/ds-test/src/",
      "erc20-helper/=modules/erc20-helper/src/",
      "forge-std/=modules/forge-std/src/",
      "non-transparent-proxy/=modules/non-transparent-proxy/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": false
  }
}}

Tags:
Governance, Voting, Timelock, Factory|addr:0x2efff88747eb5a3ff00d4d8d0f0800e306c0426b|verified:true|block:23418541|tx:0xb53b5403408d3b3f06f2015fdce4195fa1fc27a850a3f4c54b96bd2a0ef6567b|first_check:1758547425

Submitted on: 2025-09-22 15:23:45

Comments

Log in to comment.

No comments yet.