diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..baa5a015 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +MAINNET_RPC_URL= diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml new file mode 100644 index 00000000..e04a6b5b --- /dev/null +++ b/.github/workflows/slither.yml @@ -0,0 +1,37 @@ +name: Slither Analysis + +on: + push: + branches: [develop, master] + pull_request: + branches: [develop, master] + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + + - name: Run Slither + uses: crytic/slither-action@v0.4.0 + id: slither + with: + sarif: results.sarif + fail-on: none + + - name: Check results.sarif presence + id: results + if: always() + shell: bash + run: > + test -f results.sarif && + echo 'value=present' >> $GITHUB_OUTPUT || + echo 'value=not' >> $GITHUB_OUTPUT + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.slither.outputs.sarif }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b75e4b7d..8c09d4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules/ .env -coverage/ -coverage.json +coverage-report/ .DS_Store # Hardhat files @@ -16,3 +15,4 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ +.vscode/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..80a9956e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.16.0 diff --git a/README.md b/README.md index c3958e54..39ce3557 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,47 @@ See [this research forum discussion](https://research.lido.fi/t/ldo-steth-dual-g This project uses NPM for dependency management and Forge for tests so you'll need to have Node.js, NPM, and Foundry installed. -Installing the dependencies: +* Install NVM https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script -```sh -npm install -``` +* Install specific Node.js version + ```sh + nvm install + ``` + +* Install the dependencies: + ```sh + npm ci + ``` + +* Install Foundry and `forge` https://book.getfoundry.sh/getting-started/installation + +* Create `.env` file + ```sh + cp .env.example .env + ``` + + and specify there your `MAINNET_RPC_URL`. + + > **_NOTE:_** You may need to specify manually maximum allowed requests per second (rps) value for an API key/RPC url for some providers. In our experience max 100 rps will be enough to run tests. ## Running tests ```sh forge test ``` + +## Test coverage HTML report generation + +1. Install `lcov` package in your OS + ```sh + brew install lcov + + -OR- + + apt-get install lcov + ``` +2. Run + ```sh + npm run cov-report + ``` +3. Open `./coverage-report/index.html` in your browser. diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol deleted file mode 100644 index d4b4b57e..00000000 --- a/contracts/Configuration.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Durations, Duration} from "./types/Duration.sol"; -import {IConfiguration, DualGovernanceConfig} from "./interfaces/IConfiguration.sol"; - -uint256 constant PERCENT = 10 ** 16; - -contract Configuration is IConfiguration { - error MaxSealablesLimitOverflow(uint256 count, uint256 limit); - - uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE = 8; - uint256 public immutable MAX_WITHDRAWALS_BATCH_SIZE = 128; - - // --- - // Dual Governance State Properties - // --- - uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT = 3 * PERCENT; - uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT = 15 * PERCENT; - - Duration public immutable DYNAMIC_TIMELOCK_MIN_DURATION = Durations.from(3 days); - Duration public immutable DYNAMIC_TIMELOCK_MAX_DURATION = Durations.from(30 days); - - Duration public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION = Durations.from(5 hours); - Duration public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = Durations.from(5 days); - Duration public immutable RAGE_QUIT_ACCUMULATION_MAX_DURATION = Durations.from(3 days); - - Duration public immutable VETO_COOLDOWN_DURATION = Durations.from(4 days); - - Duration public immutable RAGE_QUIT_EXTENSION_DELAY = Durations.from(7 days); - Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = Durations.from(60 days); - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; - - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A = 0; - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B = 0; - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C = 0; - // --- - - address public immutable ADMIN_EXECUTOR; - address public immutable EMERGENCY_GOVERNANCE; - - Duration public immutable AFTER_SUBMIT_DELAY = Durations.from(3 days); - Duration public immutable AFTER_SCHEDULE_DELAY = Durations.from(2 days); - - Duration public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = Durations.from(5 hours); - - Duration public immutable TIE_BREAK_ACTIVATION_TIMEOUT = Durations.from(365 days); - - // Sealables Array Representation - uint256 private immutable MAX_SELABLES_COUNT = 5; - - uint256 private immutable SEALABLES_COUNT; - - address private immutable SEALABLE_0; - address private immutable SEALABLE_1; - address private immutable SEALABLE_2; - address private immutable SEALABLE_3; - address private immutable SEALABLE_4; - - constructor(address adminExecutor, address emergencyGovernance, address[] memory sealableWithdrawalBlockers_) { - ADMIN_EXECUTOR = adminExecutor; - EMERGENCY_GOVERNANCE = emergencyGovernance; - - if (sealableWithdrawalBlockers_.length > MAX_SELABLES_COUNT) { - revert MaxSealablesLimitOverflow(sealableWithdrawalBlockers_.length, MAX_SELABLES_COUNT); - } - - SEALABLES_COUNT = sealableWithdrawalBlockers_.length; - if (SEALABLES_COUNT > 0) SEALABLE_0 = sealableWithdrawalBlockers_[0]; - if (SEALABLES_COUNT > 1) SEALABLE_1 = sealableWithdrawalBlockers_[1]; - if (SEALABLES_COUNT > 2) SEALABLE_2 = sealableWithdrawalBlockers_[2]; - if (SEALABLES_COUNT > 3) SEALABLE_3 = sealableWithdrawalBlockers_[3]; - if (SEALABLES_COUNT > 4) SEALABLE_4 = sealableWithdrawalBlockers_[4]; - } - - function sealableWithdrawalBlockers() external view returns (address[] memory sealableWithdrawalBlockers_) { - sealableWithdrawalBlockers_ = new address[](SEALABLES_COUNT); - if (SEALABLES_COUNT > 0) sealableWithdrawalBlockers_[0] = SEALABLE_0; - if (SEALABLES_COUNT > 1) sealableWithdrawalBlockers_[1] = SEALABLE_1; - if (SEALABLES_COUNT > 2) sealableWithdrawalBlockers_[2] = SEALABLE_2; - if (SEALABLES_COUNT > 3) sealableWithdrawalBlockers_[3] = SEALABLE_3; - if (SEALABLES_COUNT > 4) sealableWithdrawalBlockers_[4] = SEALABLE_4; - } - - function getSignallingThresholdData() - external - view - returns ( - uint256 firstSealRageQuitSupport, - uint256 secondSealRageQuitSupport, - Duration dynamicTimelockMinDuration, - Duration dynamicTimelockMaxDuration - ) - { - firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; - secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; - dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; - dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; - } - - function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config) { - config.firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; - config.secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; - config.dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; - config.dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; - config.vetoSignallingMinActiveDuration = VETO_SIGNALLING_MIN_ACTIVE_DURATION; - config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; - config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; - config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; - config.rageQuitEthWithdrawalsMinTimelock = RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; - config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; - config.rageQuitEthWithdrawalsTimelockGrowthCoeffs = [ - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A, - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B, - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C - ]; - } -} diff --git a/contracts/ConfigurationProvider.sol b/contracts/ConfigurationProvider.sol deleted file mode 100644 index a8d9af22..00000000 --- a/contracts/ConfigurationProvider.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {IConfiguration} from "./interfaces/IConfiguration.sol"; - -contract ConfigurationProvider { - error NotAdminExecutor(address account); - - IConfiguration public immutable CONFIG; - - constructor(address config) { - CONFIG = IConfiguration(config); - } - - function _checkAdminExecutor(address account) internal view { - if (CONFIG.ADMIN_EXECUTOR() != account) { - revert NotAdminExecutor(account); - } - } -} diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 5d8936dd..5fb93df8 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -3,81 +3,156 @@ pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; -import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; -import {ISealable} from "./interfaces/ISealable.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; -import {ConfigurationProvider} from "./ConfigurationProvider.sol"; -import {Proposers, Proposer} from "./libraries/Proposers.sol"; -import {ExecutorCall} from "./libraries/Proposals.sol"; -import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; -import {State, DualGovernanceState} from "./libraries/DualGovernanceState.sol"; -import {TiebreakerProtection} from "./libraries/TiebreakerProtection.sol"; +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {Proposers} from "./libraries/Proposers.sol"; +import {Tiebreaker} from "./libraries/Tiebreaker.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; +import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; + +import {Escrow} from "./Escrow.sol"; + +contract DualGovernance is IDualGovernance { + using Proposers for Proposers.Context; + using Tiebreaker for Tiebreaker.Context; + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + // --- + // Errors + // --- -contract DualGovernance is IGovernance, ConfigurationProvider { - using Proposers for Proposers.State; - using DualGovernanceState for DualGovernanceState.Store; - using TiebreakerProtection for TiebreakerProtection.Tiebreaker; + error NotAdminProposer(); + error UnownedAdminExecutor(); + error CallerIsNotResealCommittee(address caller); + error CallerIsNotAdminExecutor(address caller); + error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider); + error ProposalSubmissionBlocked(); + error ProposalSchedulingBlocked(uint256 proposalId); + error ResealIsNotAllowedInNormalState(); - event ProposalScheduled(uint256 proposalId); + // --- + // Events + // --- - error NotResealCommitttee(address account); + event EscrowMasterCopyDeployed(address escrowMasterCopy); + event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + + // --- + // Tiebreaker Sanity Check Param Immutables + // --- + + struct SanityCheckParams { + uint256 minWithdrawalsBatchSize; + Duration minTiebreakerActivationTimeout; + Duration maxTiebreakerActivationTimeout; + uint256 maxSealableWithdrawalBlockersCount; + } + + Duration public immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration public immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 public immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + + // --- + // External Parts Immutables + + struct ExternalDependencies { + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + ITimelock timelock; + IResealManager resealManager; + IDualGovernanceConfigProvider configProvider; + } ITimelock public immutable TIMELOCK; + IResealManager public immutable RESEAL_MANAGER; + address public immutable ESCROW_MASTER_COPY; + + // --- + // Aspects + // --- + + Proposers.Context internal _proposers; + Tiebreaker.Context internal _tiebreaker; + DualGovernanceStateMachine.Context internal _stateMachine; - TiebreakerProtection.Tiebreaker internal _tiebreaker; - Proposers.State internal _proposers; - DualGovernanceState.Store internal _dgState; - EmergencyProtection.State internal _emergencyProtection; + // --- + // Standalone State Variables + // --- + IDualGovernanceConfigProvider internal _configProvider; address internal _resealCommittee; - IResealManager internal _resealManager; - constructor( - address config, - address timelock, - address escrowMasterCopy, - address adminProposer - ) ConfigurationProvider(config) { - TIMELOCK = ITimelock(timelock); + constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { + TIMELOCK = dependencies.timelock; + RESEAL_MANAGER = dependencies.resealManager; - _dgState.initialize(escrowMasterCopy); - _proposers.register(adminProposer, CONFIG.ADMIN_EXECUTOR()); + MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; + MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; + + _setConfigProvider(dependencies.configProvider); + + ESCROW_MASTER_COPY = address( + new Escrow({ + dualGovernance: this, + stETH: dependencies.stETH, + wstETH: dependencies.wstETH, + withdrawalQueue: dependencies.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + }) + ); + emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); + + _stateMachine.initialize(dependencies.configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); } - function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { - _proposers.checkProposer(msg.sender); - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - _dgState.checkProposalsCreationAllowed(); - Proposer memory proposer = _proposers.get(msg.sender); + // --- + // Proposals Flow + // --- + + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + if (!_stateMachine.canSubmitProposal()) { + revert ProposalSubmissionBlocked(); + } + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); proposalId = TIMELOCK.submit(proposer.executor, calls); } function scheduleProposal(uint256 proposalId) external { - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - - Timestamp proposalSubmissionTime = TIMELOCK.getProposalSubmissionTime(proposalId); - _dgState.checkCanScheduleProposal(proposalSubmissionTime); - + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + if (!_stateMachine.canScheduleProposal(submittedAt)) { + revert ProposalSchedulingBlocked(proposalId); + } TIMELOCK.schedule(proposalId); - - emit ProposalScheduled(proposalId); } function cancelAllPendingProposals() external { - _proposers.checkAdminProposer(CONFIG, msg.sender); + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + if (proposer.executor != TIMELOCK.getAdminExecutor()) { + revert NotAdminProposer(); + } TIMELOCK.cancelAllNonExecutedProposals(); } - function getVetoSignallingEscrow() external view returns (address) { - return address(_dgState.signallingEscrow); - } - - function getRageQuitEscrow() external view returns (address) { - return address(_dgState.rageQuitEscrow); + function canSubmitProposal() public view returns (bool) { + return _stateMachine.canSubmitProposal(); } - function canSchedule(uint256 proposalId) external view returns (bool) { - return _dgState.isProposalsAdoptionAllowed() && TIMELOCK.canSchedule(proposalId); + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); } // --- @@ -85,35 +160,42 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // --- function activateNextState() external { - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { + _checkCallerIsAdminExecutor(); + _setConfigProvider(newConfigProvider); + + /// @dev the minAssetsLockDuration is kept as a storage variable in the signalling Escrow instance + /// to sync the new value with current signalling escrow, it's value must be manually updated + _stateMachine.signallingEscrow.setMinAssetsLockDuration( + newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ); } - function getCurrentState() external view returns (State) { - return _dgState.currentState(); + function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { + return _configProvider; } - function getVetoSignallingState() - external - view - returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) - { - (isActive, duration, activatedAt, enteredAt) = _dgState.getVetoSignallingState(CONFIG.getDualGovernanceConfig()); + function getVetoSignallingEscrow() external view returns (address) { + return address(_stateMachine.signallingEscrow); } - function getVetoSignallingDeactivationState() - external - view - returns (bool isActive, Duration duration, Timestamp enteredAt) - { - (isActive, duration, enteredAt) = _dgState.getVetoSignallingDeactivationState(CONFIG.getDualGovernanceConfig()); + function getRageQuitEscrow() external view returns (address) { + return address(_stateMachine.rageQuitEscrow); } - function getVetoSignallingDuration() external view returns (Duration) { - return _dgState.getVetoSignallingDuration(CONFIG.getDualGovernanceConfig()); + function getCurrentState() external view returns (State currentState) { + currentState = _stateMachine.getCurrentState(); } - function isSchedulingEnabled() external view returns (bool) { - return _dgState.isProposalsAdoptionAllowed(); + function getCurrentStateContext() external view returns (DualGovernanceStateMachine.Context memory) { + return _stateMachine.getCurrentContext(); + } + + function getDynamicDelayDuration() external view returns (Duration) { + return _stateMachine.getDynamicDelayDuration(_configProvider.getDualGovernanceConfig()); } // --- @@ -121,25 +203,30 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // --- function registerProposer(address proposer, address executor) external { - _checkAdminExecutor(msg.sender); + _checkCallerIsAdminExecutor(); _proposers.register(proposer, executor); } function unregisterProposer(address proposer) external { - _checkAdminExecutor(msg.sender); - _proposers.unregister(CONFIG, proposer); + _checkCallerIsAdminExecutor(); + _proposers.unregister(proposer); + + /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer + if (!_proposers.isExecutor(TIMELOCK.getAdminExecutor())) { + revert UnownedAdminExecutor(); + } } - function getProposer(address account) external view returns (Proposer memory proposer) { - proposer = _proposers.get(account); + function isProposer(address account) external view returns (bool) { + return _proposers.isProposer(account); } - function getProposers() external view returns (Proposer[] memory proposers) { - proposers = _proposers.all(); + function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { + proposer = _proposers.getProposer(account); } - function isProposer(address account) external view returns (bool) { - return _proposers.isProposer(account); + function getProposers() external view returns (Proposers.Proposer[] memory proposers) { + proposers = _proposers.getAllProposers(); } function isExecutor(address account) external view returns (bool) { @@ -150,38 +237,93 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // Tiebreaker Protection // --- + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); + } + + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); + } + + function setTiebreakerCommittee(address tiebreakerCommittee) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); + } + + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerActivationTimeout( + MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT + ); + } + function tiebreakerResumeSealable(address sealable) external { - _tiebreaker.checkTiebreakerCommittee(msg.sender); - _dgState.checkTiebreak(CONFIG); - _tiebreaker.resumeSealable(sealable); + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + RESEAL_MANAGER.resume(sealable); } function tiebreakerScheduleProposal(uint256 proposalId) external { - _tiebreaker.checkTiebreakerCommittee(msg.sender); - _dgState.checkTiebreak(CONFIG); + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); TIMELOCK.schedule(proposalId); } - function setTiebreakerProtection(address newTiebreaker, address resealManager) external { - _checkAdminExecutor(msg.sender); - _tiebreaker.setTiebreaker(newTiebreaker, resealManager); + struct TiebreakerState { + address tiebreakerCommittee; + Duration tiebreakerActivationTimeout; + address[] sealableWithdrawalBlockers; + } + + function getTiebreakerState() external view returns (TiebreakerState memory tiebreakerState) { + ( + tiebreakerState.tiebreakerCommittee, + tiebreakerState.tiebreakerActivationTimeout, + tiebreakerState.sealableWithdrawalBlockers + ) = _tiebreaker.getTiebreakerInfo(); } // --- // Reseal executor // --- - function resealSealables(address[] memory sealables) external { + function resealSealable(address sealable) external { if (msg.sender != _resealCommittee) { - revert NotResealCommitttee(msg.sender); + revert CallerIsNotResealCommittee(msg.sender); + } + if (_stateMachine.getCurrentState() == State.Normal) { + revert ResealIsNotAllowedInNormalState(); } - _dgState.checkResealState(); - _resealManager.reseal(sealables); + RESEAL_MANAGER.reseal(sealable); } - function setReseal(address resealManager, address resealCommittee) external { - _checkAdminExecutor(msg.sender); + function setResealCommittee(address resealCommittee) external { + _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; - _resealManager = IResealManager(resealManager); + } + + // --- + // Private methods + // --- + + function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { + if (address(newConfigProvider) == address(0)) { + revert InvalidConfigProvider(newConfigProvider); + } + + if (newConfigProvider == _configProvider) { + return; + } + + _configProvider = IDualGovernanceConfigProvider(newConfigProvider); + emit ConfigProviderSet(newConfigProvider); + } + + function _checkCallerIsAdminExecutor() internal view { + if (TIMELOCK.getAdminExecutor() != msg.sender) { + revert CallerIsNotAdminExecutor(msg.sender); + } } } diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/DualGovernanceConfigProvider.sol new file mode 100644 index 00000000..806e3dd5 --- /dev/null +++ b/contracts/DualGovernanceConfigProvider.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {PercentD16} from "./types/PercentD16.sol"; +import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; +import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol"; + +contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider { + PercentD16 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT; + PercentD16 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT; + + Duration public immutable MIN_ASSETS_LOCK_DURATION; + Duration public immutable DYNAMIC_TIMELOCK_MIN_DURATION; + Duration public immutable DYNAMIC_TIMELOCK_MAX_DURATION; + + Duration public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION; + Duration public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + + Duration public immutable VETO_COOLDOWN_DURATION; + + Duration public immutable RAGE_QUIT_EXTENSION_DELAY; + Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C; + + constructor(DualGovernanceConfig.Context memory dualGovernanceConfig) { + FIRST_SEAL_RAGE_QUIT_SUPPORT = dualGovernanceConfig.firstSealRageQuitSupport; + SECOND_SEAL_RAGE_QUIT_SUPPORT = dualGovernanceConfig.secondSealRageQuitSupport; + + MIN_ASSETS_LOCK_DURATION = dualGovernanceConfig.minAssetsLockDuration; + DYNAMIC_TIMELOCK_MIN_DURATION = dualGovernanceConfig.dynamicTimelockMinDuration; + DYNAMIC_TIMELOCK_MAX_DURATION = dualGovernanceConfig.dynamicTimelockMaxDuration; + + VETO_SIGNALLING_MIN_ACTIVE_DURATION = dualGovernanceConfig.vetoSignallingMinActiveDuration; + VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = dualGovernanceConfig.vetoSignallingDeactivationMaxDuration; + + VETO_COOLDOWN_DURATION = dualGovernanceConfig.vetoCooldownDuration; + + RAGE_QUIT_EXTENSION_DELAY = dualGovernanceConfig.rageQuitExtensionDelay; + RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = dualGovernanceConfig.rageQuitEthWithdrawalsMinTimelock; + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = + dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; + + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A = + dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0]; + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B = + dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1]; + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C = + dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2]; + } + + function getDualGovernanceConfig() external view returns (DualGovernanceConfig.Context memory config) { + config.firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; + config.secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; + + config.minAssetsLockDuration = MIN_ASSETS_LOCK_DURATION; + config.dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; + config.dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; + config.vetoSignallingMinActiveDuration = VETO_SIGNALLING_MIN_ACTIVE_DURATION; + config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; + config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; + config.rageQuitEthWithdrawalsMinTimelock = RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; + config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs = [ + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A, + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B, + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C + ]; + } +} diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index b01d761f..8ef807b1 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -5,33 +5,63 @@ import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; import {IOwnable} from "./interfaces/IOwnable.sol"; -import {ITimelock} from "./interfaces/ITimelock.sol"; +import {ITimelock, ProposalStatus} from "./interfaces/ITimelock.sol"; -import {Proposal, Proposals, ExecutorCall} from "./libraries/Proposals.sol"; -import {EmergencyProtection, EmergencyState} from "./libraries/EmergencyProtection.sol"; - -import {ConfigurationProvider} from "./ConfigurationProvider.sol"; +import {TimelockState} from "./libraries/TimelockState.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; /// @title EmergencyProtectedTimelock /// @dev A timelock contract with emergency protection functionality. /// The contract allows for submitting, scheduling, and executing proposals, /// while providing emergency protection features to prevent unauthorized /// execution during emergency situations. -contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { - using Proposals for Proposals.State; - using EmergencyProtection for EmergencyProtection.State; +contract EmergencyProtectedTimelock is ITimelock { + using TimelockState for TimelockState.Context; + using ExecutableProposals for ExecutableProposals.Context; + using EmergencyProtection for EmergencyProtection.Context; + + error CallerIsNotAdminExecutor(address value); + + // --- + // Sanity Check Params Immutables + // --- + struct SanityCheckParams { + Duration maxAfterSubmitDelay; + Duration maxAfterScheduleDelay; + Duration maxEmergencyModeDuration; + Duration maxEmergencyProtectionDuration; + } + + Duration public immutable MAX_AFTER_SUBMIT_DELAY; + Duration public immutable MAX_AFTER_SCHEDULE_DELAY; - error InvalidGovernance(address governance); - error NotGovernance(address account, address governance); + Duration public immutable MAX_EMERGENCY_MODE_DURATION; + Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; - event GovernanceSet(address governance); + // --- + // Admin Executor Immutables + // --- - address internal _governance; + address private immutable _ADMIN_EXECUTOR; - Proposals.State internal _proposals; - EmergencyProtection.State internal _emergencyProtection; + // --- + // Aspects + // --- - constructor(address config) ConfigurationProvider(config) {} + TimelockState.Context internal _timelockState; + ExecutableProposals.Context internal _proposals; + EmergencyProtection.Context internal _emergencyProtection; + + constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + + MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; + MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; + MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; + MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyModeDuration; + } // --- // Main Timelock Functionality @@ -40,10 +70,10 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { /// @dev Submits a new proposal to execute a series of calls through an executor. /// Only the governance contract can call this function. /// @param executor The address of the executor contract that will execute the calls. - /// @param calls An array of `ExecutorCall` structs representing the calls to be executed. + /// @param calls An array of `ExternalCall` structs representing the calls to be executed. /// @return newProposalId The ID of the newly created proposal. - function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId) { - _checkGovernance(msg.sender); + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { + _timelockState.checkCallerIsGovernance(); newProposalId = _proposals.submit(executor, calls); } @@ -51,177 +81,206 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { /// Only the governance contract can call this function. /// @param proposalId The ID of the proposal to be scheduled. function schedule(uint256 proposalId) external { - _checkGovernance(msg.sender); - _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); + _timelockState.checkCallerIsGovernance(); + _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); } /// @dev Executes a scheduled proposal. /// Checks if emergency mode is active and prevents execution if it is. /// @param proposalId The ID of the proposal to be executed. function execute(uint256 proposalId) external { - _emergencyProtection.checkEmergencyModeActive(false); - _proposals.execute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); + _emergencyProtection.checkEmergencyMode({isActive: false}); + _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); } /// @dev Cancels all non-executed proposals. /// Only the governance contract can call this function. function cancelAllNonExecutedProposals() external { - _checkGovernance(msg.sender); + _timelockState.checkCallerIsGovernance(); _proposals.cancelAll(); } + // --- + // Timelock Management + // --- + + function setGovernance(address newGovernance) external { + _checkCallerIsAdminExecutor(); + _timelockState.setGovernance(newGovernance); + } + + function setDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + _checkCallerIsAdminExecutor(); + _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); + _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); + } + /// @dev Transfers ownership of the executor contract to a new owner. /// Only the admin executor can call this function. /// @param executor The address of the executor contract. /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { - _checkAdminExecutor(msg.sender); + _checkCallerIsAdminExecutor(); IOwnable(executor).transferOwnership(owner); } - /// @dev Sets a new governance contract address. - /// Only the admin executor can call this function. - /// @param newGovernance The address of the new governance contract. - function setGovernance(address newGovernance) external { - _checkAdminExecutor(msg.sender); - _setGovernance(newGovernance); - } - // --- // Emergency Protection Functionality // --- + function setupEmergencyProtection( + address emergencyGovernance, + address emergencyActivationCommittee, + address emergencyExecutionCommittee, + Timestamp emergencyProtectionEndDate, + Duration emergencyModeDuration + ) external { + _checkCallerIsAdminExecutor(); + + _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + _emergencyProtection.setEmergencyProtectionEndDate( + emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + ); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + /// @dev Activates the emergency mode. /// Only the activation committee can call this function. function activateEmergencyMode() external { - _emergencyProtection.checkActivationCommittee(msg.sender); - _emergencyProtection.checkEmergencyModeActive(false); - _emergencyProtection.activate(); + _emergencyProtection.checkCallerIsEmergencyActivationCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: false}); + _emergencyProtection.activateEmergencyMode(); } /// @dev Executes a proposal during emergency mode. /// Checks if emergency mode is active and if the caller is part of the execution committee. /// @param proposalId The ID of the proposal to be executed. function emergencyExecute(uint256 proposalId) external { - _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkExecutionCommittee(msg.sender); - _proposals.execute(proposalId, /* afterScheduleDelay */ Duration.wrap(0)); + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); } /// @dev Deactivates the emergency mode. /// If the emergency mode has not passed, only the admin executor can call this function. function deactivateEmergencyMode() external { - _emergencyProtection.checkEmergencyModeActive(true); - if (!_emergencyProtection.isEmergencyModePassed()) { - _checkAdminExecutor(msg.sender); + _emergencyProtection.checkEmergencyMode({isActive: true}); + if (!_emergencyProtection.isEmergencyModeDurationPassed()) { + _checkCallerIsAdminExecutor(); } - _emergencyProtection.deactivate(); + _emergencyProtection.deactivateEmergencyMode(); _proposals.cancelAll(); } /// @dev Resets the system after entering the emergency mode. /// Only the execution committee can call this function. function emergencyReset() external { - _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkExecutionCommittee(msg.sender); - _emergencyProtection.deactivate(); - _setGovernance(CONFIG.EMERGENCY_GOVERNANCE()); + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.deactivateEmergencyMode(); + + _timelockState.setGovernance(_emergencyProtection.emergencyGovernance); _proposals.cancelAll(); } - /// @dev Sets the parameters for the emergency protection functionality. - /// Only the admin executor can call this function. - /// @param activator The address of the activation committee. - /// @param enactor The address of the execution committee. - /// @param protectionDuration The duration of the protection period. - /// @param emergencyModeDuration The duration of the emergency mode. - function setEmergencyProtection( - address activator, - address enactor, - Duration protectionDuration, - Duration emergencyModeDuration - ) external { - _checkAdminExecutor(msg.sender); - _emergencyProtection.setup(activator, enactor, protectionDuration, emergencyModeDuration); + function getEmergencyProtectionContext() external view returns (EmergencyProtection.Context memory) { + return _emergencyProtection; } - /// @dev Checks if the emergency protection functionality is enabled. - /// @return A boolean indicating if the emergency protection is enabled. - function isEmergencyProtectionEnabled() external view returns (bool) { + function isEmergencyProtectionEnabled() public view returns (bool) { return _emergencyProtection.isEmergencyProtectionEnabled(); } - /// @dev Retrieves the current emergency state. - /// @return res The EmergencyState struct containing the current emergency state. - function getEmergencyState() external view returns (EmergencyState memory res) { - res = _emergencyProtection.getEmergencyState(); + function isEmergencyModeActive() public view returns (bool isActive) { + isActive = _emergencyProtection.isEmergencyModeActive(); } // --- // Timelock View Methods // --- - /// @dev Retrieves the address of the current governance contract. - /// @return The address of the current governance contract. function getGovernance() external view returns (address) { - return _governance; + return _timelockState.governance; + } + + function getAdminExecutor() external view returns (address) { + return _ADMIN_EXECUTOR; + } + + function getAfterSubmitDelay() external view returns (Duration) { + return _timelockState.getAfterSubmitDelay(); + } + + function getAfterScheduleDelay() external view returns (Duration) { + return _timelockState.getAfterScheduleDelay(); } /// @dev Retrieves the details of a proposal. /// @param proposalId The ID of the proposal. /// @return proposal The Proposal struct containing the details of the proposal. function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { - proposal = _proposals.get(proposalId); + proposal.id = proposalId; + (proposal.status, proposal.executor, proposal.submittedAt, proposal.scheduledAt) = + _proposals.getProposalInfo(proposalId); + proposal.calls = _proposals.getProposalCalls(proposalId); + } + + /// @notice Retrieves information about a proposal, excluding the external calls associated with it. + /// @param proposalId The ID of the proposal to retrieve information for. + /// @return id The ID of the proposal. + /// @return status The current status of the proposal. Possible values are: + /// 0 - The proposal does not exist. + /// 1 - The proposal was submitted but not scheduled. + /// 2 - The proposal was submitted and scheduled but not yet executed. + /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. + /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. + /// This is the final state of the proposal. + /// @return executor The address of the executor responsible for executing the proposal's external calls. + /// @return submittedAt The timestamp when the proposal was submitted. + /// @return scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// was submitted but not yet scheduled. + function getProposalInfo(uint256 proposalId) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + id = proposalId; + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + } + + /// @notice Retrieves the external calls associated with the specified proposal. + /// @param proposalId The ID of the proposal to retrieve external calls for. + /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + calls = _proposals.getProposalCalls(proposalId); } /// @dev Retrieves the total number of proposals. /// @return count The total number of proposals. function getProposalsCount() external view returns (uint256 count) { - count = _proposals.count(); - } - - /// @dev Retrieves the submission time of a proposal. - /// @param proposalId The ID of the proposal. - /// @return submittedAt The submission time of the proposal. - function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt) { - submittedAt = _proposals.getProposalSubmissionTime(proposalId); + count = _proposals.getProposalsCount(); } /// @dev Checks if a proposal can be executed. /// @param proposalId The ID of the proposal. /// @return A boolean indicating if the proposal can be executed. function canExecute(uint256 proposalId) external view returns (bool) { - return !_emergencyProtection.isEmergencyModeActivated() - && _proposals.canExecute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); + return !_emergencyProtection.isEmergencyModeActive() + && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); } /// @dev Checks if a proposal can be scheduled. /// @param proposalId The ID of the proposal. /// @return A boolean indicating if the proposal can be scheduled. function canSchedule(uint256 proposalId) external view returns (bool) { - return _proposals.canSchedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); - } - - // --- - // Internal Methods - // --- - - /// @dev Internal function to set the governance contract address. - /// @param newGovernance The address of the new governance contract. - function _setGovernance(address newGovernance) internal { - address prevGovernance = _governance; - if (newGovernance == prevGovernance || newGovernance == address(0)) { - revert InvalidGovernance(newGovernance); - } - _governance = newGovernance; - emit GovernanceSet(newGovernance); + return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); } - /// @dev Internal function to check if the caller is the governance contract. - /// @param account The address to check. - function _checkGovernance(address account) internal view { - if (_governance != account) { - revert NotGovernance(account, _governance); + function _checkCallerIsAdminExecutor() internal view { + if (msg.sender != _ADMIN_EXECUTOR) { + revert CallerIsNotAdminExecutor(msg.sender); } } } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index a92de85b..e604017c 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -4,37 +4,28 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Duration} from "./types/Duration.sol"; -import {Timestamp, Timestamps} from "./types/Timestamp.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ETHValue, ETHValues} from "./types/ETHValue.sol"; +import {SharesValue, SharesValues} from "./types/SharesValue.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; import {IEscrow} from "./interfaces/IEscrow.sol"; -import {IEscrowConfigration} from "./interfaces/IConfiguration.sol"; - import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; -import { - ETHValue, - ETHValues, - SharesValue, - SharesValues, - HolderAssets, - StETHAccounting, - UnstETHAccounting, - AssetsAccounting -} from "./libraries/AssetsAccounting.sol"; +import {EscrowState} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; - -interface IDualGovernance { - function activateNextState() external; -} - -enum EscrowState { - NotInitialized, - SignallingEscrow, - RageQuitEscrow -} - +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; + +/// @notice Summary of the total locked assets in the Escrow +/// @param stETHLockedShares Total number of stETH shares locked in the Escrow +/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow +/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been +/// marked as finalized +/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked +/// as finalized struct LockedAssetsTotals { uint256 stETHLockedShares; uint256 stETHClaimedETH; @@ -50,129 +41,177 @@ struct VetoerState { } contract Escrow is IEscrow { - using AssetsAccounting for AssetsAccounting.State; - using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.State; + using EscrowState for EscrowState.Context; + using AssetsAccounting for AssetsAccounting.Context; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + // --- + // Errors + // --- + + error UnclaimedBatches(); error UnexpectedUnstETHId(); - error InvalidHintsLength(uint256 actual, uint256 expected); - error ClaimingIsFinished(); + error UnfinalizedUnstETHIds(); + error NonProxyCallsForbidden(); + error BatchesQueueIsNotClosed(); error InvalidBatchSize(uint256 size); - error WithdrawalsTimelockNotPassed(); + error CallerIsNotDualGovernance(address caller); + error InvalidHintsLength(uint256 actual, uint256 expected); error InvalidETHSender(address actual, address expected); - error NotDualGovernance(address actual, address expected); - error MasterCopyCallForbidden(); - error InvalidState(EscrowState actual, EscrowState expected); - error RageQuitExtraTimelockNotStarted(); - address public immutable MASTER_COPY; + // --- + // Events + // --- + + event ConfigProviderSet(address newConfigProvider); + + // --- + // Sanity check params immutables + // --- - uint256 public immutable MIN_WITHDRAWAL_REQUEST_AMOUNT; - uint256 public immutable MAX_WITHDRAWAL_REQUEST_AMOUNT; + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + + // --- + // Dependencies immutables + // --- IStETH public immutable ST_ETH; IWstETH public immutable WST_ETH; IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; - IEscrowConfigration public immutable CONFIG; - - EscrowState internal _escrowState; - IDualGovernance private _dualGovernance; - AssetsAccounting.State private _accounting; - WithdrawalsBatchesQueue.State private _batchesQueue; - - Duration internal _rageQuitExtensionDelay; - Duration internal _rageQuitWithdrawalsTimelock; - Timestamp internal _rageQuitTimelockStartedAt; - - constructor(address stETH, address wstETH, address withdrawalQueue, address config) { - ST_ETH = IStETH(stETH); - WST_ETH = IWstETH(wstETH); - MASTER_COPY = address(this); - CONFIG = IEscrowConfigration(config); - WITHDRAWAL_QUEUE = IWithdrawalQueue(withdrawalQueue); - MIN_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); - MAX_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + // --- + // Implementation immutables + + address private immutable _SELF; + IDualGovernance public immutable DUAL_GOVERNANCE; + + // --- + // Aspects + // --- + + EscrowState.Context internal _escrowState; + AssetsAccounting.Context private _accounting; + WithdrawalsBatchesQueue.Context private _batchesQueue; + + // --- + // Construction & initializing + // --- + + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) { + _SELF = address(this); + DUAL_GOVERNANCE = dualGovernance; + + ST_ETH = stETH; + WST_ETH = wstETH; + WITHDRAWAL_QUEUE = withdrawalQueue; + + MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; } - function initialize(address dualGovernance) external { - if (address(this) == MASTER_COPY) { - revert MasterCopyCallForbidden(); + function initialize(Duration minAssetsLockDuration) external { + if (address(this) == _SELF) { + revert NonProxyCallsForbidden(); } - _checkEscrowState(EscrowState.NotInitialized); + _checkCallerIsDualGovernance(); - _escrowState = EscrowState.SignallingEscrow; - _dualGovernance = IDualGovernance(dualGovernance); + _escrowState.initialize(minAssetsLockDuration); ST_ETH.approve(address(WST_ETH), type(uint256).max); ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } // --- - // Lock & Unlock stETH + // Lock & unlock stETH // --- function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); - _activateNextGovernanceState(); + + DUAL_GOVERNANCE.activateNextState(); } function unlockStETH() external returns (uint256 unlockedStETHShares) { - _activateNextGovernanceState(); - _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + _escrowState.checkSignallingEscrow(); + + DUAL_GOVERNANCE.activateNextState(); + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); ST_ETH.transferShares(msg.sender, unlockedStETHShares); - _activateNextGovernanceState(); + + DUAL_GOVERNANCE.activateNextState(); } // --- - // Lock / Unlock wstETH + // Lock & unlock wstETH // --- function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + WST_ETH.transferFrom(msg.sender, address(this), amount); lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); - _activateNextGovernanceState(); + + DUAL_GOVERNANCE.activateNextState(); } function unlockWstETH() external returns (uint256 unlockedStETHShares) { - _activateNextGovernanceState(); - _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); WST_ETH.transfer(msg.sender, unlockedStETHShares); - _activateNextGovernanceState(); + + DUAL_GOVERNANCE.activateNextState(); } // --- - // Lock / Unlock unstETH + // Lock & unlock unstETH // --- function lockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); - uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); } - _activateNextGovernanceState(); + + DUAL_GOVERNANCE.activateNextState(); } function unlockUnstETH(uint256[] memory unstETHIds) external { - _activateNextGovernanceState(); - _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); } - _activateNextGovernanceState(); + + DUAL_GOVERNANCE.activateNextState(); } function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { - _checkEscrowState(EscrowState.SignallingEscrow); + _escrowState.checkSignallingEscrow(); uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); @@ -182,8 +221,10 @@ contract Escrow is IEscrow { // Convert to NFT // --- - function requestWithdrawals(uint256[] calldata stEthAmounts) external returns (uint256[] memory unstETHIds) { - unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stEthAmounts, address(this)); + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + _escrowState.checkSignallingEscrow(); + + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); uint256 sharesTotal = 0; @@ -195,98 +236,136 @@ contract Escrow is IEscrow { } // --- - // State Updates + // Start rage quit // --- function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { - _checkDualGovernance(msg.sender); - _checkEscrowState(EscrowState.SignallingEscrow); - - _batchesQueue.open(); - _escrowState = EscrowState.RageQuitEscrow; - _rageQuitExtensionDelay = rageQuitExtensionDelay; - _rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + _checkCallerIsDualGovernance(); + _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); } - function requestNextWithdrawalsBatch(uint256 maxBatchSize) external { - _checkEscrowState(EscrowState.RageQuitEscrow); - _batchesQueue.checkOpened(); + // --- + // Request withdrawal batches + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { + _escrowState.checkRageQuitEscrow(); - if (maxBatchSize < CONFIG.MIN_WITHDRAWALS_BATCH_SIZE() || maxBatchSize > CONFIG.MAX_WITHDRAWALS_BATCH_SIZE()) { - revert InvalidBatchSize(maxBatchSize); + if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { + revert InvalidBatchSize(batchSize); } uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); - if (stETHRemaining < MIN_WITHDRAWAL_REQUEST_AMOUNT) { + uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { return _batchesQueue.close(); } uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ - minRequestAmount: MIN_WITHDRAWAL_REQUEST_AMOUNT, - requestAmount: MAX_WITHDRAWAL_REQUEST_AMOUNT, - amount: Math.min(stETHRemaining, MAX_WITHDRAWAL_REQUEST_AMOUNT * maxBatchSize) + minRequestAmount: minStETHWithdrawalRequestAmount, + maxRequestAmount: maxStETHWithdrawalRequestAmount, + remainingAmount: Math.min(stETHRemaining, maxStETHWithdrawalRequestAmount * batchSize) }); - _batchesQueue.add(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); } + // --- + // Claim requested withdrawal batches + // --- + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { - _checkEscrowState(EscrowState.RageQuitEscrow); - if (!_rageQuitTimelockStartedAt.isZero()) { - revert ClaimingIsFinished(); - } + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); _claimNextWithdrawalsBatch( - unstETHIds, WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + unstETHIds[0], + unstETHIds, + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) ); } function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { - _checkEscrowState(EscrowState.RageQuitEscrow); - if (!_rageQuitTimelockStartedAt.isZero()) { - revert ClaimingIsFinished(); - } + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); - if (unstETHIds.length > 0 && fromUnstETHId != unstETHIds[0]) { - revert UnexpectedUnstETHId(); + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + // --- + // Start rage quit extension delay + // --- + + function startRageQuitExtensionDelay() external { + if (!_batchesQueue.isClosed()) { + revert BatchesQueueIsNotClosed(); } - if (hints.length != unstETHIds.length) { - revert InvalidHintsLength(hints.length, unstETHIds.length); + + /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. + if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + revert UnfinalizedUnstETHIds(); } - _claimNextWithdrawalsBatch(unstETHIds, hints); + if (!_batchesQueue.isAllBatchesClaimed()) { + revert UnclaimedBatches(); + } + + _escrowState.startRageQuitExtensionDelay(); } + // --- + // Claim locked unstETH NFTs + // --- + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { - _checkEscrowState(EscrowState.RageQuitEscrow); + _escrowState.checkRageQuitEscrow(); uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); - uint256 ethBalanceBefore = address(this).balance; + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); - uint256 ethBalanceAfter = address(this).balance; + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); - assert(totalAmountClaimed == ETHValues.from(ethBalanceAfter - ethBalanceBefore)); + assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); } // --- - // Withdraw Logic + // Escrow management + // --- + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Withdraw logic // --- function withdrawETH() external { - _checkEscrowState(EscrowState.RageQuitEscrow); - _checkWithdrawalsTimelockPassed(); + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); ethToWithdraw.sendTo(payable(msg.sender)); } function withdrawETH(uint256[] calldata unstETHIds) external { - _checkEscrowState(EscrowState.RageQuitEscrow); - _checkWithdrawalsTimelockPassed(); + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); ethToWithdraw.sendTo(payable(msg.sender)); } @@ -314,6 +393,10 @@ contract Escrow is IEscrow { state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); } + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { return _batchesQueue.getNextWithdrawalsBatches(limit); } @@ -322,33 +405,29 @@ contract Escrow is IEscrow { return _batchesQueue.isClosed(); } - function isWithdrawalsClaimed() external view returns (bool) { - return !_rageQuitTimelockStartedAt.isZero(); + function isRageQuitExtensionDelayStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionDelayStarted(); } - function getRageQuitTimelockStartedAt() external view returns (Timestamp) { - return _rageQuitTimelockStartedAt; + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionDelayStartedAt; } - function getRageQuitSupport() external view returns (uint256 rageQuitSupport) { + function getRageQuitSupport() external view returns (PercentD16) { StETHAccounting memory stETHTotals = _accounting.stETHTotals; UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); - uint256 ufinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); - rageQuitSupport = ( - 10 ** 18 * (ST_ETH.getPooledEthByShares(ufinalizedShares) + finalizedETH) - / (ST_ETH.totalSupply() + finalizedETH) - ); + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); } function isRageQuitFinalized() external view returns (bool) { - return ( - _escrowState == EscrowState.RageQuitEscrow && _batchesQueue.isClosed() - && !_rageQuitTimelockStartedAt.isZero() - && Timestamps.now() > _rageQuitExtensionDelay.addTo(_rageQuitTimelockStartedAt) - ); + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); } // --- @@ -362,46 +441,32 @@ contract Escrow is IEscrow { } // --- - // Internal Methods + // Internal methods // --- - function _claimNextWithdrawalsBatch(uint256[] memory unstETHIds, uint256[] memory hints) internal { - uint256 ethBalanceBefore = address(this).balance; - WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); - uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; - - if (ethAmountClaimed > 0) { - _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + function _claimNextWithdrawalsBatch( + uint256 fromUnstETHId, + uint256[] memory unstETHIds, + uint256[] memory hints + ) internal { + if (fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); } - if (_batchesQueue.isClosed() && _batchesQueue.isAllUnstETHClaimed()) { - _rageQuitTimelockStartedAt = Timestamps.now(); + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); } - } - - function _activateNextGovernanceState() internal { - _dualGovernance.activateNextState(); - } - function _checkEscrowState(EscrowState expected) internal view { - if (_escrowState != expected) { - revert InvalidState(_escrowState, expected); - } - } + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); - function _checkDualGovernance(address account) internal view { - if (account != address(_dualGovernance)) { - revert NotDualGovernance(account, address(_dualGovernance)); - } + _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); } - function _checkWithdrawalsTimelockPassed() internal view { - if (_rageQuitTimelockStartedAt.isZero()) { - revert RageQuitExtraTimelockNotStarted(); - } - Duration withdrawalsTimelock = _rageQuitExtensionDelay + _rageQuitWithdrawalsTimelock; - if (Timestamps.now() <= withdrawalsTimelock.addTo(_rageQuitTimelockStartedAt)) { - revert WithdrawalsTimelockNotPassed(); + function _checkCallerIsDualGovernance() internal view { + if (msg.sender != address(DUAL_GOVERNANCE)) { + revert CallerIsNotDualGovernance(msg.sender); } } } diff --git a/contracts/Executor.sol b/contracts/Executor.sol index d58cb2b0..1497a326 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -4,16 +4,17 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IExecutor} from "./interfaces/IExecutor.sol"; +import {IExternalExecutor} from "./interfaces/IExternalExecutor.sol"; -contract Executor is IExecutor, Ownable { +contract Executor is IExternalExecutor, Ownable { constructor(address owner) Ownable(owner) {} function execute( address target, uint256 value, bytes calldata payload - ) external payable onlyOwner returns (bytes memory result) { + ) external payable returns (bytes memory result) { + _checkOwner(); result = Address.functionCallWithValue(target, payload, value); } diff --git a/contracts/ResealManager.sol b/contracts/ResealManager.sol index 1c5593ff..b7a79ef8 100644 --- a/contracts/ResealManager.sol +++ b/contracts/ResealManager.sol @@ -2,36 +2,53 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ISealable} from "./interfaces/ISealable.sol"; -interface IEmergencyProtectedTimelock { - function getGovernance() external view returns (address); -} +import {ISealable} from "./interfaces/ISealable.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; -contract ResealManager { +/// @title ResealManager +/// @dev Allows to extend pause of temporarily paused contracts to permanent pause or resume it. +contract ResealManager is IResealManager { error SealableWrongPauseState(); - error SenderIsNotGovernance(); - error NotAllowed(); + error CallerIsNotGovernance(address caller); uint256 public constant PAUSE_INFINITELY = type(uint256).max; - address public immutable EMERGENCY_PROTECTED_TIMELOCK; + ITimelock public immutable EMERGENCY_PROTECTED_TIMELOCK; - constructor(address emergencyProtectedTimelock) { - EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; + /// @notice Initializes the ResealManager contract. + /// @param emergencyProtectedTimelock The address of the EmergencyProtectedTimelock contract. + constructor(ITimelock emergencyProtectedTimelock) { + EMERGENCY_PROTECTED_TIMELOCK = ITimelock(emergencyProtectedTimelock); } - function reseal(address[] memory sealables) public onlyGovernance { - for (uint256 i = 0; i < sealables.length; ++i) { - uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); - if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { - revert SealableWrongPauseState(); - } - Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.resume.selector)); - Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.pauseFor.selector, PAUSE_INFINITELY)); + /// @notice Extends the pause of the specified sealable contract. + /// @dev Works only if conditions are met: + /// - ResealManager has PAUSE_ROLE for target contract; + /// - Contract are paused until timestamp after current timestamp and not for infinite time; + /// - The DAO governance is blocked by DualGovernance; + /// - Function is called by the governance contract. + /// @param sealable The address of the sealable contract. + function reseal(address sealable) public { + _checkCallerIsGovernance(); + + uint256 sealableResumeSinceTimestamp = ISealable(sealable).getResumeSinceTimestamp(); + if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { + revert SealableWrongPauseState(); } + Address.functionCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); + Address.functionCall(sealable, abi.encodeWithSelector(ISealable.pauseFor.selector, PAUSE_INFINITELY)); } - function resume(address sealable) public onlyGovernance { + /// @notice Resumes the specified sealable contract if it is scheduled to resume in the future. + /// @dev Works only if conditions are met: + /// - ResealManager has RESUME_ROLE for target contract; + /// - Contract are paused until timestamp after current timestamp; + /// - Function is called by the governance contract. + /// @param sealable The address of the sealable contract. + function resume(address sealable) public { + _checkCallerIsGovernance(); + uint256 sealableResumeSinceTimestamp = ISealable(sealable).getResumeSinceTimestamp(); if (sealableResumeSinceTimestamp < block.timestamp) { revert SealableWrongPauseState(); @@ -39,11 +56,12 @@ contract ResealManager { Address.functionCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); } - modifier onlyGovernance() { - address governance = IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).getGovernance(); + /// @notice Ensures that the function can only be called by the governance address. + /// @dev Reverts if the sender is not the governance address. + function _checkCallerIsGovernance() internal view { + address governance = EMERGENCY_PROTECTED_TIMELOCK.getGovernance(); if (msg.sender != governance) { - revert SenderIsNotGovernance(); + revert CallerIsNotGovernance(msg.sender); } - _; } } diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol deleted file mode 100644 index 476b251c..00000000 --- a/contracts/SingleGovernance.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {IGovernance, ITimelock} from "./interfaces/ITimelock.sol"; - -import {ConfigurationProvider} from "./ConfigurationProvider.sol"; -import {ExecutorCall} from "./libraries/Proposals.sol"; - -contract SingleGovernance is IGovernance, ConfigurationProvider { - error NotGovernance(address account); - - address public immutable GOVERNANCE; - ITimelock public immutable TIMELOCK; - - constructor(address config, address governance, address timelock) ConfigurationProvider(config) { - GOVERNANCE = governance; - TIMELOCK = ITimelock(timelock); - } - - function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { - _checkGovernance(msg.sender); - return TIMELOCK.submit(CONFIG.ADMIN_EXECUTOR(), calls); - } - - function scheduleProposal(uint256 proposalId) external { - TIMELOCK.schedule(proposalId); - } - - function executeProposal(uint256 proposalId) external { - TIMELOCK.execute(proposalId); - } - - function canSchedule(uint256 proposalId) external view returns (bool) { - return TIMELOCK.canSchedule(proposalId); - } - - function cancelAllPendingProposals() external { - _checkGovernance(msg.sender); - TIMELOCK.cancelAllNonExecutedProposals(); - } - - function _checkGovernance(address account) internal view { - if (account != GOVERNANCE) { - revert NotGovernance(account); - } - } -} diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol new file mode 100644 index 00000000..4dc05c93 --- /dev/null +++ b/contracts/TimelockedGovernance.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {IGovernance} from "./interfaces/IGovernance.sol"; + +import {ExternalCall} from "./libraries/ExternalCalls.sol"; + +/// @title TimelockedGovernance +/// @dev A contract that serves as the interface for submitting and scheduling the execution of governance proposals. +contract TimelockedGovernance is IGovernance { + error CallerIsNotGovernance(address caller); + + address public immutable GOVERNANCE; + ITimelock public immutable TIMELOCK; + + /// @dev Initializes the TimelockedGovernance contract. + /// @param governance The address of the governance contract. + /// @param timelock The address of the timelock contract. + constructor(address governance, ITimelock timelock) { + GOVERNANCE = governance; + TIMELOCK = timelock; + } + + /// @dev Submits a proposal to the timelock. + /// @param calls An array of ExternalCall structs representing the calls to be executed in the proposal. + /// @return proposalId The ID of the submitted proposal. + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + _checkCallerIsGovernance(); + return TIMELOCK.submit(TIMELOCK.getAdminExecutor(), calls); + } + + /// @dev Schedules a submitted proposal. + /// @param proposalId The ID of the proposal to be scheduled. + function scheduleProposal(uint256 proposalId) external { + TIMELOCK.schedule(proposalId); + } + + /// @dev Executes a scheduled proposal. + /// @param proposalId The ID of the proposal to be executed. + function executeProposal(uint256 proposalId) external { + TIMELOCK.execute(proposalId); + } + + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal to check. + /// @return A boolean indicating whether the proposal can be scheduled. + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + return TIMELOCK.canSchedule(proposalId); + } + + /// @dev Cancels all pending proposals that have not been executed. + function cancelAllPendingProposals() external { + _checkCallerIsGovernance(); + TIMELOCK.cancelAllNonExecutedProposals(); + } + + /// @dev Checks if the msg.sender is the governance address. + function _checkCallerIsGovernance() internal view { + if (msg.sender != GOVERNANCE) { + revert CallerIsNotGovernance(msg.sender); + } + } +} diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index d9710ce5..13f1a0bd 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -2,12 +2,13 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {HashConsensus} from "./HashConsensus.sol"; -interface IEmergencyProtectedTimelock { - function emergencyActivate() external; -} +import {HashConsensus} from "./HashConsensus.sol"; +import {ITimelock} from "../interfaces/ITimelock.sol"; +/// @title Emergency Activation Committee Contract +/// @notice This contract allows a committee to approve and execute an emergency activation +/// @dev Inherits from HashConsensus to utilize voting and consensus mechanisms contract EmergencyActivationCommittee is HashConsensus { address public immutable EMERGENCY_PROTECTED_TIMELOCK; @@ -18,15 +19,24 @@ contract EmergencyActivationCommittee is HashConsensus { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { + ) HashConsensus(owner, 0) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; + + _addMembers(committeeMembers, executionQuorum); } - function approveEmergencyActivate() public onlyMember { + /// @notice Approves the emergency activation by casting a vote + /// @dev Only callable by committee members + function approveActivateEmergencyMode() public { + _checkCallerIsMember(); _vote(EMERGENCY_ACTIVATION_HASH, true); } - function getEmergencyActivateState() + /// @notice Gets the current state of the emergency activation vote + /// @return support The number of votes in support of the activation + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the activation has been executed + function getActivateEmergencyModeState() public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) @@ -34,10 +44,12 @@ contract EmergencyActivationCommittee is HashConsensus { return _getHashState(EMERGENCY_ACTIVATION_HASH); } - function executeEmergencyActivate() external { + /// @notice Executes the emergency activation if the quorum is reached + /// @dev Calls the emergencyActivate function on the Emergency Protected Timelock contract + function executeActivateEmergencyMode() external { _markUsed(EMERGENCY_ACTIVATION_HASH); Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyActivate.selector) + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(ITimelock.activateEmergencyMode.selector) ); } } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 25a8b868..c6c0e704 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -4,17 +4,16 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; - -interface IEmergencyProtectedTimelock { - function emergencyExecute(uint256 proposalId) external; - function emergencyReset() external; -} +import {ITimelock} from "../interfaces/ITimelock.sol"; enum ProposalType { EmergencyExecute, EmergencyReset } +/// @title Emergency Execution Committee Contract +/// @notice This contract allows a committee to vote on and execute emergency proposals +/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { address public immutable EMERGENCY_PROTECTED_TIMELOCK; @@ -23,18 +22,32 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { + ) HashConsensus(owner, 0) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; + + _addMembers(committeeMembers, executionQuorum); } + // --- // Emergency Execution + // --- - function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember { + /// @notice Votes on an emergency execution proposal + /// @dev Only callable by committee members + /// @param proposalId The ID of the proposal to vote on + /// @param _supports Indicates whether the member supports the proposal execution + function voteEmergencyExecute(uint256 proposalId, bool _supports) public { + _checkCallerIsMember(); (bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId); _vote(key, _supports); _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); } + /// @notice Gets the current state of an emergency execution proposal + /// @param proposalId The ID of the proposal + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed function getEmergencyExecuteState(uint256 proposalId) public view @@ -44,15 +57,20 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { return _getHashState(key); } + /// @notice Executes an approved emergency execution proposal + /// @param proposalId The ID of the proposal to execute function executeEmergencyExecute(uint256 proposalId) public { (, bytes32 key) = _encodeEmergencyExecute(proposalId); _markUsed(key); Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, - abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(ITimelock.emergencyExecute.selector, proposalId) ); } + /// @dev Encodes the proposal data and generates the proposal key for an emergency execution + /// @param proposalId The ID of the proposal to encode + /// @return proposalData The encoded proposal data + /// @return key The generated proposal key function _encodeEmergencyExecute(uint256 proposalId) private pure @@ -62,14 +80,23 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { key = keccak256(proposalData); } + // --- // Governance reset + // --- - function approveEmergencyReset() public onlyMember { + /// @notice Approves an emergency reset proposal + /// @dev Only callable by committee members + function approveEmergencyReset() public { + _checkCallerIsMember(); bytes32 proposalKey = _encodeEmergencyResetProposalKey(); _vote(proposalKey, true); _pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes("")); } + /// @notice Gets the current state of an emergency reset opprosal + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed function getEmergencyResetState() public view @@ -79,14 +106,15 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { return _getHashState(proposalKey); } + /// @notice Executes an approved emergency reset proposal function executeEmergencyReset() external { bytes32 proposalKey = _encodeEmergencyResetProposalKey(); _markUsed(proposalKey); - Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) - ); + Address.functionCall(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(ITimelock.emergencyReset.selector)); } + /// @notice Encodes the proposal key for an emergency reset + /// @return The generated proposal key function _encodeEmergencyResetProposalKey() internal pure returns (bytes32) { return keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))); } diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 0318b2ca..31825a1b 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.26; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +/// @title HashConsensus Contract +/// @notice This contract provides a consensus mechanism based on hash voting among members +/// @dev Inherits from Ownable for access control and uses EnumerableSet for member management abstract contract HashConsensus is Ownable { using EnumerableSet for EnumerableSet.AddressSet; @@ -14,12 +17,12 @@ abstract contract HashConsensus is Ownable { event Voted(address indexed signer, bytes32 hash, bool support); event TimelockDurationSet(uint256 timelockDuration); - error IsNotMember(); - error SenderIsNotMember(); - error HashAlreadyUsed(); + error DuplicatedMember(address account); + error AccountIsNotMember(address account); + error CallerIsNotMember(address caller); + error HashAlreadyUsed(bytes32 hash); error QuorumIsNotReached(); error InvalidQuorum(); - error DuplicatedMember(address member); error TimelockNotPassed(); struct HashState { @@ -34,24 +37,18 @@ abstract contract HashConsensus is Ownable { EnumerableSet.AddressSet private _members; mapping(address signer => mapping(bytes32 => bool)) public approves; - constructor(address owner, address[] memory newMembers, uint256 executionQuorum, uint256 timelock) Ownable(owner) { - if (executionQuorum == 0) { - revert InvalidQuorum(); - } - quorum = executionQuorum; - emit QuorumSet(executionQuorum); - + constructor(address owner, uint256 timelock) Ownable(owner) { timelockDuration = timelock; emit TimelockDurationSet(timelock); - - for (uint256 i = 0; i < newMembers.length; ++i) { - _addMember(newMembers[i]); - } } + /// @notice Casts a vote on a given hash if hash has not been used + /// @dev Only callable by members + /// @param hash The hash to vote on + /// @param support Indicates whether the member supports the hash function _vote(bytes32 hash, bool support) internal { if (_hashStates[hash].usedAt > 0) { - revert HashAlreadyUsed(); + revert HashAlreadyUsed(hash); } if (approves[msg.sender][hash] == support) { @@ -67,11 +64,17 @@ abstract contract HashConsensus is Ownable { emit Voted(msg.sender, hash, support); } + /// @notice Marks a hash as used if quorum is reached and timelock has passed + /// @dev Internal function that handles marking a hash as used + /// @param hash The hash to mark as used function _markUsed(bytes32 hash) internal { if (_hashStates[hash].usedAt > 0) { - revert HashAlreadyUsed(); + revert HashAlreadyUsed(hash); } - if (_getSupport(hash) < quorum) { + + uint256 support = _getSupport(hash); + + if (support == 0 || support < quorum) { revert QuorumIsNotReached(); } if (block.timestamp < _hashStates[hash].quorumAt + timelockDuration) { @@ -83,6 +86,12 @@ abstract contract HashConsensus is Ownable { emit HashUsed(hash); } + /// @notice Gets the state of a given hash + /// @dev Internal function to retrieve the state of a hash + /// @param hash The hash to get the state for + /// @return support The number of votes in support of the hash + /// @return execuitionQuorum The required number of votes for execution + /// @return isUsed Whether the hash has been used function _getHashState(bytes32 hash) internal view @@ -93,60 +102,103 @@ abstract contract HashConsensus is Ownable { isUsed = _hashStates[hash].usedAt > 0; } - function addMember(address newMember, uint256 newQuorum) public onlyOwner { - _addMember(newMember); + /// @notice Adds new members to the contract and sets the execution quorum. + /// @dev This function allows the contract owner to add multiple new members and set the execution quorum. + /// The function reverts if the caller is not the owner, if the execution quorum is set to zero, + /// or if it exceeds the total number of members. + /// @param newMembers The array of addresses to be added as new members + /// @param executionQuorum The minimum number of members required for executing certain operations + function addMembers(address[] memory newMembers, uint256 executionQuorum) public { + _checkOwner(); - if (newQuorum == 0 || newQuorum > _members.length()) { - revert InvalidQuorum(); - } - quorum = newQuorum; - emit QuorumSet(newQuorum); + _addMembers(newMembers, executionQuorum); } - function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner { - if (!_members.contains(memberToRemove)) { - revert IsNotMember(); + /// @notice Removes specified members from the contract and updates the execution quorum. + /// @dev This function can only be called by the contract owner. It removes multiple members from + /// the contract. If any of the specified members are not found in the members list, the + /// function will revert. The quorum is also updated and must not be zero or greater than + /// the new total number of members. + /// @param membersToRemove The array of addresses to be removed from the members list. + /// @param newQuorum The updated minimum number of members required for executing certain operations. + function removeMembers(address[] memory membersToRemove, uint256 newQuorum) public { + _checkOwner(); + + for (uint256 i = 0; i < membersToRemove.length; ++i) { + if (!_members.contains(membersToRemove[i])) { + revert AccountIsNotMember(membersToRemove[i]); + } + _members.remove(membersToRemove[i]); + emit MemberRemoved(membersToRemove[i]); } - _members.remove(memberToRemove); - emit MemberRemoved(memberToRemove); - if (newQuorum == 0 || newQuorum > _members.length()) { - revert InvalidQuorum(); - } - quorum = newQuorum; - emit QuorumSet(newQuorum); + _setQuorum(newQuorum); } + /// @notice Gets the list of committee members + /// @dev Public function to return the list of members + /// @return An array of addresses representing the committee members function getMembers() public view returns (address[] memory) { return _members.values(); } + /// @notice Checks if an address is a member of the committee + /// @dev Public function to check membership status + /// @param member The address to check + /// @return A boolean indicating whether the address is a member function isMember(address member) public view returns (bool) { return _members.contains(member); } - function setTimelockDuration(uint256 timelock) public onlyOwner { + /// @notice Sets the timelock duration + /// @dev Only callable by the owner + /// @param timelock The new timelock duration in seconds + function setTimelockDuration(uint256 timelock) public { + _checkOwner(); timelockDuration = timelock; emit TimelockDurationSet(timelock); } - function setQuorum(uint256 newQuorum) public onlyOwner { - if (newQuorum == 0 || newQuorum > _members.length()) { + /// @notice Sets the quorum value + /// @dev Only callable by the owner + /// @param newQuorum The new quorum value + function setQuorum(uint256 newQuorum) public { + _checkOwner(); + _setQuorum(newQuorum); + } + + /// @notice Sets the execution quorum required for certain operations. + /// @dev The quorum value must be greater than zero and not exceed the current number of members. + /// @param executionQuorum The new quorum value to be set. + function _setQuorum(uint256 executionQuorum) internal { + if (executionQuorum == 0 || executionQuorum > _members.length()) { revert InvalidQuorum(); } - - quorum = newQuorum; - emit QuorumSet(newQuorum); + quorum = executionQuorum; + emit QuorumSet(executionQuorum); } - function _addMember(address newMember) internal { - if (_members.contains(newMember)) { - revert DuplicatedMember(newMember); + /// @notice Adds new members to the contract and sets the execution quorum. + /// @dev This internal function adds multiple new members and sets the execution quorum. + /// The function reverts if the execution quorum is set to zero or exceeds the total number of members. + /// @param newMembers The array of addresses to be added as new members. + /// @param executionQuorum The minimum number of members required for executing certain operations. + function _addMembers(address[] memory newMembers, uint256 executionQuorum) internal { + for (uint256 i = 0; i < newMembers.length; ++i) { + if (_members.contains(newMembers[i])) { + revert DuplicatedMember(newMembers[i]); + } + _members.add(newMembers[i]); + emit MemberAdded(newMembers[i]); } - _members.add(newMember); - emit MemberAdded(newMember); + + _setQuorum(executionQuorum); } + /// @notice Gets the number of votes in support of a given hash + /// @dev Internal function to count the votes in support of a hash + /// @param hash The hash to check + /// @return support The number of votes in support of the hash function _getSupport(bytes32 hash) internal view returns (uint256 support) { for (uint256 i = 0; i < _members.length(); ++i) { if (approves[_members.at(i)][hash]) { @@ -155,10 +207,11 @@ abstract contract HashConsensus is Ownable { } } - modifier onlyMember() { + /// @notice Restricts access to only committee members + /// @dev Reverts if the sender is not a member + function _checkCallerIsMember() internal view { if (!_members.contains(msg.sender)) { - revert SenderIsNotMember(); + revert CallerIsNotMember(msg.sender); } - _; } } diff --git a/contracts/committees/ProposalsList.sol b/contracts/committees/ProposalsList.sol index e4934ba0..75b434a7 100644 --- a/contracts/committees/ProposalsList.sol +++ b/contracts/committees/ProposalsList.sol @@ -3,11 +3,19 @@ pragma solidity 0.8.26; import {EnumerableProposals, Proposal} from "../libraries/EnumerableProposals.sol"; +/// @title Proposals List Contract +/// @notice This contract manages a list of proposals using an enumerable map +/// @dev Uses the EnumerableProposals library for managing proposals contract ProposalsList { using EnumerableProposals for EnumerableProposals.Bytes32ToProposalMap; EnumerableProposals.Bytes32ToProposalMap internal _proposals; + /// @notice Retrieves a list of proposals with pagination + /// @dev Fetches an ordered list of proposals based on the offset and limit + /// @param offset The starting index for the list of proposals + /// @param limit The maximum number of proposals to return + /// @return proposals An array of Proposal structs function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) { bytes32[] memory keys = _proposals.getOrderedKeys(offset, limit); @@ -19,22 +27,43 @@ contract ProposalsList { } } + /// @notice Retrieves a proposal at a specific index + /// @dev Fetches the proposal located at the specified index in the map + /// @param index The index of the proposal to retrieve + /// @return The Proposal struct at the given index function getProposalAt(uint256 index) public view returns (Proposal memory) { return _proposals.at(index); } + /// @notice Retrieves a proposal by its key + /// @dev Fetches the proposal associated with the given key + /// @param key The key of the proposal to retrieve + /// @return The Proposal struct associated with the given key function getProposal(bytes32 key) public view returns (Proposal memory) { return _proposals.get(key); } + /// @notice Retrieves the total number of proposals + /// @dev Fetches the length of the proposals map + /// @return The total number of proposals function getProposalsLength() public view returns (uint256) { return _proposals.length(); } + /// @notice Retrieves an ordered list of proposal keys with pagination + /// @dev Fetches the keys of the proposals based on the offset and limit + /// @param offset The starting index for the list of keys + /// @param limit The maximum number of keys to return + /// @return An array of proposal keys function getOrderedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) { return _proposals.getOrderedKeys(offset, limit); } + /// @notice Adds a new proposal to the list + /// @dev Internal function to push a new proposal into the map + /// @param key The key of the proposal + /// @param proposalType The type of the proposal + /// @param data The data associated with the proposal function _pushProposal(bytes32 key, uint256 proposalType, bytes memory data) internal { _proposals.push(key, proposalType, data); } diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index 8d495eec..b2050ea8 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -2,13 +2,14 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; -interface IDualGovernance { - function reseal(address[] memory sealables) external; -} - +/// @title Reseal Committee Contract +/// @notice This contract allows a committee to vote on and execute resealing proposals +/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management contract ResealCommittee is HashConsensus, ProposalsList { address public immutable DUAL_GOVERNANCE; @@ -20,38 +21,59 @@ contract ResealCommittee is HashConsensus, ProposalsList { uint256 executionQuorum, address dualGovernance, uint256 timelock - ) HashConsensus(owner, committeeMembers, executionQuorum, timelock) { + ) HashConsensus(owner, timelock) { DUAL_GOVERNANCE = dualGovernance; + + _addMembers(committeeMembers, executionQuorum); } - function voteReseal(address[] memory sealables, bool support) public onlyMember { - (bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealables); + /// @notice Votes on a reseal proposal + /// @dev Allows committee members to vote on resealing a sealed address + /// @param sealable The address to reseal + /// @param support Indicates whether the member supports the proposal + function voteReseal(address sealable, bool support) public { + _checkCallerIsMember(); + (bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealable); _vote(key, support); _pushProposal(key, 0, proposalData); } - function getResealState(address[] memory sealables) + /// @notice Gets the current state of a reseal proposal + /// @dev Retrieves the state of the reseal proposal for a sealed address + /// @param sealable The addresses for the reseal proposal + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed + function getResealState(address sealable) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - (, bytes32 key) = _encodeResealProposal(sealables); + (, bytes32 key) = _encodeResealProposal(sealable); return _getHashState(key); } - function executeReseal(address[] memory sealables) external { - (, bytes32 key) = _encodeResealProposal(sealables); + /// @notice Executes an approved reseal proposal + /// @dev Executes the reseal proposal by calling the reseal function on the Dual Governance contract + /// @param sealable The address to reseal + function executeReseal(address sealable) external { + (, bytes32 key) = _encodeResealProposal(sealable); _markUsed(key); - Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.reseal.selector, sealables)); + Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); - bytes32 resealNonceHash = keccak256(abi.encode(sealables)); + bytes32 resealNonceHash = keccak256(abi.encode(sealable)); _resealNonces[resealNonceHash]++; } - function _encodeResealProposal(address[] memory sealables) internal view returns (bytes memory data, bytes32 key) { - bytes32 resealNonceHash = keccak256(abi.encode(sealables)); - data = abi.encode(sealables, _resealNonces[resealNonceHash]); + /// @notice Encodes a reseal proposal + /// @dev Internal function to encode the proposal data and generate the proposal key + /// @param sealable The address to reseal + /// @return data The encoded proposal data + /// @return key The generated proposal key + function _encodeResealProposal(address sealable) internal view returns (bytes memory data, bytes32 key) { + bytes32 resealNonceHash = keccak256(abi.encode(sealable)); + data = abi.encode(sealable, _resealNonces[resealNonceHash]); key = keccak256(data); } } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index e340d759..b82380f7 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -2,44 +2,51 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; +import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; -interface IDualGovernance { - function tiebreakerScheduleProposal(uint256 proposalId) external; - function tiebreakerResumeSealable(address sealable) external; -} - enum ProposalType { ScheduleProposal, ResumeSelable } -contract TiebreakerCore is HashConsensus, ProposalsList { +/// @title Tiebreaker Core Contract +/// @notice This contract allows a committee to vote on and execute proposals for scheduling and resuming sealable addresses +/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management +contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { error ResumeSealableNonceMismatch(); address immutable DUAL_GOVERNANCE; mapping(address => uint256) private _sealableResumeNonces; - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address dualGovernance, - uint256 timelock - ) HashConsensus(owner, committeeMembers, executionQuorum, timelock) { + constructor(address owner, address dualGovernance, uint256 timelock) HashConsensus(owner, timelock) { DUAL_GOVERNANCE = dualGovernance; } + // --- // Schedule proposal + // --- - function scheduleProposal(uint256 proposalId) public onlyMember { + /// @notice Votes on a proposal to schedule + /// @dev Allows committee members to vote on scheduling a proposal + /// @param proposalId The ID of the proposal to schedule + function scheduleProposal(uint256 proposalId) public { + _checkCallerIsMember(); (bytes memory proposalData, bytes32 key) = _encodeScheduleProposal(proposalId); _vote(key, true); _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); } + /// @notice Gets the current state of a schedule proposal + /// @dev Retrieves the state of the schedule proposal for a given proposal ID + /// @param proposalId The ID of the proposal + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed function getScheduleProposalState(uint256 proposalId) public view @@ -49,6 +56,9 @@ contract TiebreakerCore is HashConsensus, ProposalsList { return _getHashState(key); } + /// @notice Executes an approved schedule proposal + /// @dev Executes the schedule proposal by calling the tiebreakerScheduleProposal function on the Dual Governance contract + /// @param proposalId The ID of the proposal to schedule function executeScheduleProposal(uint256 proposalId) public { (, bytes32 key) = _encodeScheduleProposal(proposalId); _markUsed(key); @@ -57,18 +67,30 @@ contract TiebreakerCore is HashConsensus, ProposalsList { ); } + /// @notice Encodes a schedule proposal + /// @dev Internal function to encode the proposal data and generate the proposal key + /// @param proposalId The ID of the proposal to schedule + /// @return data The encoded proposal data + /// @return key The generated proposal key function _encodeScheduleProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { data = abi.encode(ProposalType.ScheduleProposal, proposalId); key = keccak256(data); } + // --- // Resume sealable + // --- + /// @notice Gets the current resume nonce for a sealable address + /// @dev Retrieves the resume nonce for the given sealable address + /// @param sealable The address of the sealable to get the nonce for + /// @return The current resume nonce for the sealable address function getSealableResumeNonce(address sealable) public view returns (uint256) { return _sealableResumeNonces[sealable]; } - function sealableResume(address sealable, uint256 nonce) public onlyMember { + function sealableResume(address sealable, uint256 nonce) public { + _checkCallerIsMember(); if (nonce != _sealableResumeNonces[sealable]) { revert ResumeSealableNonceMismatch(); } @@ -77,6 +99,13 @@ contract TiebreakerCore is HashConsensus, ProposalsList { _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); } + /// @notice Gets the current state of a resume sealable proposal + /// @dev Retrieves the state of the resume sealable proposal for a given address and nonce + /// @param sealable The address to resume + /// @param nonce The nonce for the resume proposal + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed function getSealableResumeState( address sealable, uint256 nonce @@ -85,6 +114,9 @@ contract TiebreakerCore is HashConsensus, ProposalsList { return _getHashState(key); } + /// @notice Executes an approved resume sealable proposal + /// @dev Executes the resume sealable proposal by calling the tiebreakerResumeSealable function on the Dual Governance contract + /// @param sealable The address to resume function executeSealableResume(address sealable) external { (, bytes32 key) = _encodeSealableResume(sealable, _sealableResumeNonces[sealable]); _markUsed(key); @@ -94,6 +126,12 @@ contract TiebreakerCore is HashConsensus, ProposalsList { ); } + /// @notice Encodes a resume sealable proposal + /// @dev Internal function to encode the proposal data and generate the proposal key + /// @param sealable The address to resume + /// @param nonce The nonce for the resume proposal + /// @return data The encoded proposal data + /// @return key The generated proposal key function _encodeSealableResume( address sealable, uint256 nonce diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index abd5b356..810fdff9 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -2,20 +2,19 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; -interface ITiebreakerCore { - function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); - function scheduleProposal(uint256 _proposalId) external; - function sealableResume(address sealable, uint256 nonce) external; -} - enum ProposalType { ScheduleProposal, ResumeSelable } +/// @title Tiebreaker SubCommittee Contract +/// @notice This contract allows a subcommittee to vote on and execute proposals for scheduling and resuming sealable addresses +/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management contract TiebreakerSubCommittee is HashConsensus, ProposalsList { address immutable TIEBREAKER_CORE; @@ -24,18 +23,32 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address tiebreakerCore - ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { + ) HashConsensus(owner, 0) { TIEBREAKER_CORE = tiebreakerCore; + + _addMembers(committeeMembers, executionQuorum); } + // --- // Schedule proposal + // --- - function scheduleProposal(uint256 proposalId) public onlyMember { + /// @notice Votes on a proposal to schedule + /// @dev Allows committee members to vote on scheduling a proposal + /// @param proposalId The ID of the proposal to schedule + function scheduleProposal(uint256 proposalId) public { + _checkCallerIsMember(); (bytes memory proposalData, bytes32 key) = _encodeAproveProposal(proposalId); _vote(key, true); _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); } + /// @notice Gets the current state of a schedule proposal + /// @dev Retrieves the state of the schedule proposal for a given proposal ID + /// @param proposalId The ID of the proposal + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed function getScheduleProposalState(uint256 proposalId) public view @@ -45,6 +58,9 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { return _getHashState(key); } + /// @notice Executes an approved schedule proposal + /// @dev Executes the schedule proposal by calling the scheduleProposal function on the Tiebreaker Core contract + /// @param proposalId The ID of the proposal to schedule function executeScheduleProposal(uint256 proposalId) public { (, bytes32 key) = _encodeAproveProposal(proposalId); _markUsed(key); @@ -53,19 +69,35 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { ); } + /// @notice Encodes a schedule proposal + /// @dev Internal function to encode the proposal data and generate the proposal key + /// @param proposalId The ID of the proposal to schedule + /// @return data The encoded proposal data + /// @return key The generated proposal key function _encodeAproveProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { - data = abi.encode(ProposalType.ScheduleProposal, data); + data = abi.encode(ProposalType.ScheduleProposal, proposalId); key = keccak256(data); } + // --- // Sealable resume + // --- + /// @notice Votes on a proposal to resume a sealable address + /// @dev Allows committee members to vote on resuming a sealable address + /// @param sealable The address to resume function sealableResume(address sealable) public { (bytes memory proposalData, bytes32 key,) = _encodeSealableResume(sealable); _vote(key, true); _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); } + /// @notice Gets the current state of a resume sealable proposal + /// @dev Retrieves the state of the resume sealable proposal for a given address + /// @param sealable The address to resume + /// @return support The number of votes in support of the proposal + /// @return execuitionQuorum The required number of votes for execution + /// @return isExecuted Whether the proposal has been executed function getSealableResumeState(address sealable) public view @@ -75,6 +107,9 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { return _getHashState(key); } + /// @notice Executes an approved resume sealable proposal + /// @dev Executes the resume sealable proposal by calling the sealableResume function on the Tiebreaker Core contract + /// @param sealable The address to resume function executeSealableResume(address sealable) public { (, bytes32 key, uint256 nonce) = _encodeSealableResume(sealable); _markUsed(key); @@ -83,6 +118,12 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { ); } + /// @notice Encodes a resume sealable proposal + /// @dev Internal function to encode the proposal data and generate the proposal key + /// @param sealable The address to resume + /// @return data The encoded proposal data + /// @return key The generated proposal key + /// @return nonce The current resume nonce for the sealable address function _encodeSealableResume(address sealable) internal view diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol deleted file mode 100644 index 1ccee4ef..00000000 --- a/contracts/interfaces/IConfiguration.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "../types/Duration.sol"; - -struct DualGovernanceConfig { - uint256 firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport; - Duration dynamicTimelockMaxDuration; - Duration dynamicTimelockMinDuration; - Duration vetoSignallingMinActiveDuration; - Duration vetoSignallingDeactivationMaxDuration; - Duration vetoCooldownDuration; - Duration rageQuitExtraTimelock; - Duration rageQuitExtensionDelay; - Duration rageQuitEthWithdrawalsMinTimelock; - uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; - uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; -} - -interface IEscrowConfigration { - function MIN_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); - function MAX_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); - function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (Duration); -} - -interface IAdminExecutorConfiguration { - function ADMIN_EXECUTOR() external view returns (address); -} - -interface ITimelockConfiguration { - function AFTER_SUBMIT_DELAY() external view returns (Duration); - function AFTER_SCHEDULE_DELAY() external view returns (Duration); - function EMERGENCY_GOVERNANCE() external view returns (address); -} - -interface IDualGovernanceConfiguration { - function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (Duration); - - function VETO_COOLDOWN_DURATION() external view returns (Duration); - function VETO_SIGNALLING_MIN_ACTIVE_DURATION() external view returns (Duration); - - function VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() external view returns (Duration); - - function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (Duration); - function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (Duration); - - function FIRST_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); - function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); - - function RAGE_QUIT_EXTENSION_DELAY() external view returns (Duration); - function RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK() external view returns (Duration); - function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (Duration); - function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); - - function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); - function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); - function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); - - function sealableWithdrawalBlockers() external view returns (address[] memory); - - function getSignallingThresholdData() - external - view - returns ( - uint256 firstSealThreshold, - uint256 secondSealThreshold, - Duration signallingMinDuration, - Duration signallingMaxDuration - ); - - function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); -} - -interface IConfiguration is - IEscrowConfigration, - ITimelockConfiguration, - IAdminExecutorConfiguration, - IDualGovernanceConfiguration -{} diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol new file mode 100644 index 00000000..1ee4e000 --- /dev/null +++ b/contracts/interfaces/IDualGovernance.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IGovernance} from "./IGovernance.sol"; + +interface IDualGovernance is IGovernance { + function activateNextState() external; + + function resealSealable(address sealables) external; + + function tiebreakerScheduleProposal(uint256 proposalId) external; + function tiebreakerResumeSealable(address sealable) external; +} diff --git a/contracts/interfaces/IDualGovernanceConfigProvider.sol b/contracts/interfaces/IDualGovernanceConfigProvider.sol new file mode 100644 index 00000000..e9c6988c --- /dev/null +++ b/contracts/interfaces/IDualGovernanceConfigProvider.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {DualGovernanceConfig} from "../libraries/DualGovernanceConfig.sol"; + +interface IDualGovernanceConfigProvider { + function getDualGovernanceConfig() external view returns (DualGovernanceConfig.Context memory config); +} diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index e4474b73..dc0f8878 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -2,13 +2,14 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; interface IEscrow { - function initialize(address dualGovernance) external; + function initialize(Duration minAssetsLockDuration) external; function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external; - function MASTER_COPY() external view returns (address); function isRageQuitFinalized() external view returns (bool); - function getRageQuitSupport() external view returns (uint256 rageQuitSupport); + function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; } diff --git a/contracts/interfaces/IExecutor.sol b/contracts/interfaces/IExternalExecutor.sol similarity index 57% rename from contracts/interfaces/IExecutor.sol rename to contracts/interfaces/IExternalExecutor.sol index f54f433c..6999aa53 100644 --- a/contracts/interfaces/IExecutor.sol +++ b/contracts/interfaces/IExternalExecutor.sol @@ -1,18 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -struct ExecutorCall { - address target; - uint96 value; // ~ 7.9 billion ETH - bytes payload; -} - -interface IExecutor { +interface IExternalExecutor { function execute( address target, uint256 value, bytes calldata payload ) external payable returns (bytes memory result); - - receive() external payable; } diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol new file mode 100644 index 00000000..b29c19f8 --- /dev/null +++ b/contracts/interfaces/IGovernance.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ExternalCall} from "../libraries/ExternalCalls.sol"; + +interface IGovernance { + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId); + function scheduleProposal(uint256 proposalId) external; + function cancelAllPendingProposals() external; + + function canScheduleProposal(uint256 proposalId) external view returns (bool); +} diff --git a/contracts/interfaces/IResealManager.sol b/contracts/interfaces/IResealManager.sol index bea725ef..844bd8e1 100644 --- a/contracts/interfaces/IResealManager.sol +++ b/contracts/interfaces/IResealManager.sol @@ -3,5 +3,5 @@ pragma solidity 0.8.26; interface IResealManager { function resume(address sealable) external; - function reseal(address[] memory sealables) external; + function reseal(address sealable) external; } diff --git a/contracts/interfaces/IStETH.sol b/contracts/interfaces/IStETH.sol index fc1eecc4..207793a5 100644 --- a/contracts/interfaces/IStETH.sol +++ b/contracts/interfaces/IStETH.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IStETH is IERC20 { - function getTotalShares() external view returns (uint256); function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256); function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256); diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol new file mode 100644 index 00000000..0993fc31 --- /dev/null +++ b/contracts/interfaces/ITiebreaker.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface ITiebreakerCore { + function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); + function scheduleProposal(uint256 _proposalId) external; + function sealableResume(address sealable, uint256 nonce) external; +} diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index e7926a11..f3c192f0 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -2,18 +2,21 @@ pragma solidity 0.8.26; import {Timestamp} from "../types/Timestamp.sol"; -import {ExecutorCall} from "./IExecutor.sol"; -interface IGovernance { - function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId); - function scheduleProposal(uint256 proposalId) external; - function cancelAllPendingProposals() external; - - function canSchedule(uint256 proposalId) external view returns (bool); -} +import {ExternalCall} from "../libraries/ExternalCalls.sol"; +import {Status as ProposalStatus} from "../libraries/ExecutableProposals.sol"; interface ITimelock { - function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId); + struct Proposal { + uint256 id; + ProposalStatus status; + address executor; + Timestamp submittedAt; + Timestamp scheduledAt; + ExternalCall[] calls; + } + + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId); function schedule(uint256 proposalId) external; function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; @@ -21,5 +24,18 @@ interface ITimelock { function canSchedule(uint256 proposalId) external view returns (bool); function canExecute(uint256 proposalId) external view returns (bool); - function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt); + function getAdminExecutor() external view returns (address); + + function getProposal(uint256 proposalId) external view returns (Proposal memory proposal); + function getProposalInfo(uint256 proposalId) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt); + + function getGovernance() external view returns (address); + function setGovernance(address governance) external; + + function activateEmergencyMode() external; + function emergencyExecute(uint256 proposalId) external; + function emergencyReset() external; } diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index dab8edd0..3ec50762 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; + struct WithdrawalRequestStatus { uint256 amountOfStETH; uint256 amountOfShares; @@ -10,23 +12,22 @@ struct WithdrawalRequestStatus { bool isClaimed; } -interface IWithdrawalQueue { +interface IWithdrawalQueue is IERC721 { function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external; - function getLastFinalizedRequestId() external view returns (uint256); - function transferFrom(address from, address to, uint256 requestId) external; + function getLastRequestId() external view returns (uint256); + function getLastFinalizedRequestId() external view returns (uint256); + function getWithdrawalStatus(uint256[] calldata _requestIds) external view returns (WithdrawalRequestStatus[] memory statuses); - function getLastRequestId() external view returns (uint256); - /// @notice Returns amount of ether available for claim for each provided request id /// @param _requestIds array of request ids /// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())` @@ -44,19 +45,8 @@ interface IWithdrawalQueue { ) external view returns (uint256[] memory hintIds); function getLastCheckpointIndex() external view returns (uint256); - function balanceOf(address owner) external view returns (uint256); - function requestWithdrawals( uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds); - - function requestWithdrawalsWstETH( - uint256[] calldata _amounts, - address _owner - ) external returns (uint256[] memory requestIds); - - function grantRole(bytes32 role, address account) external; - function pauseFor(uint256 duration) external; - function isPaused() external returns (bool); } diff --git a/contracts/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol index 6b6d6e17..557553ad 100644 --- a/contracts/interfaces/IWstETH.sol +++ b/contracts/interfaces/IWstETH.sol @@ -5,6 +5,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IWstETH is IERC20 { function wrap(uint256 stETHAmount) external returns (uint256); - function unwrap(uint256 wstETHAmount) external returns (uint256); + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256); } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index f8c2d0c9..93bb42f2 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -1,40 +1,57 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; import {ETHValue, ETHValues} from "../types/ETHValue.sol"; import {SharesValue, SharesValues} from "../types/SharesValue.sol"; import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; -import {Duration} from "../types/Duration.sol"; -import {Timestamps, Timestamp} from "../types/Timestamp.sol"; - +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user struct HolderAssets { - // The total shares amount of stETH/wstETH accounted to the holder + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] SharesValue stETHLockedShares; - // The total shares amount of unstETH NFTs accounted to the holder + /// @dev slot1: [0..127] SharesValue unstETHLockedShares; - // The timestamp when the last time was accounted lock of shares or unstETHs - Timestamp lastAssetsLockTimestamp; - // The ids of the unstETH NFTs accounted to the holder + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot uint256[] unstETHIds; } +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH struct UnstETHAccounting { - // The cumulative amount of unfinalized unstETH shares locked in the Escrow + /// @dev slot0: [0..127] SharesValue unfinalizedShares; - // The total amount of ETH claimable from the finalized unstETH locked in the Escrow + /// @dev slot1: [128..255] ETHValue finalizedETH; } +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares struct StETHAccounting { - // The total amount of shares of locked stETH and wstETH tokens + /// @dev slot0: [0..127] SharesValue lockedShares; - // The total amount of ETH received during the claiming of the locked stETH + /// @dev slot0: [128..255] ETHValue claimedETH; } +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim enum UnstETHRecordStatus { NotLocked, Locked, @@ -43,24 +60,42 @@ enum UnstETHRecordStatus { Withdrawn } +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. struct UnstETHRecord { - // The one based index of the unstETH record in the UnstETHAccounting.unstETHIds list + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] IndexOneBased index; - // The address of the holder who locked unstETH + /// @dev slot 0: [40..199] address lockedBy; - // The current status of the unstETH - UnstETHRecordStatus status; - // The amount of shares contained in the unstETH + /// @dev slot 1: [0..127] SharesValue shares; - // The amount of ETH contained in the unstETH (this value equals to 0 until NFT is mark as finalized or claimed) + /// @dev slot 1: [128..255] ETHValue claimableAmount; } +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract library AssetsAccounting { - struct State { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] StETHAccounting stETHTotals; + /// @dev slot1: [0..255] UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; } @@ -88,14 +123,14 @@ library AssetsAccounting { error InvalidSharesValue(SharesValue value); error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); - error AssetsUnlockDelayNotPassed(Timestamp unlockTimelockExpiresAt); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); // --- // stETH shares operations accounting // --- - function accountStETHSharesLock(State storage self, address holder, SharesValue shares) internal { + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { _checkNonZeroShares(shares); self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; HolderAssets storage assets = self.assets[holder]; @@ -104,12 +139,12 @@ library AssetsAccounting { emit StETHSharesLocked(holder, shares); } - function accountStETHSharesUnlock(State storage self, address holder) internal returns (SharesValue shares) { + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { shares = self.assets[holder].stETHLockedShares; accountStETHSharesUnlock(self, holder, shares); } - function accountStETHSharesUnlock(State storage self, address holder, SharesValue shares) internal { + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { _checkNonZeroShares(shares); HolderAssets storage assets = self.assets[holder]; @@ -122,7 +157,10 @@ library AssetsAccounting { emit StETHSharesUnlocked(holder, shares); } - function accountStETHSharesWithdraw(State storage self, address holder) internal returns (ETHValue ethWithdrawn) { + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { HolderAssets storage assets = self.assets[holder]; SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; @@ -135,7 +173,7 @@ library AssetsAccounting { emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); } - function accountClaimedStETH(State storage self, ETHValue amount) internal { + function accountClaimedStETH(Context storage self, ETHValue amount) internal { self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; emit ETHClaimed(amount); } @@ -145,7 +183,7 @@ library AssetsAccounting { // --- function accountUnstETHLock( - State storage self, + Context storage self, address holder, uint256[] memory unstETHIds, WithdrawalRequestStatus[] memory statuses @@ -164,7 +202,7 @@ library AssetsAccounting { emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); } - function accountUnstETHUnlock(State storage self, address holder, uint256[] memory unstETHIds) internal { + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { SharesValue totalSharesUnlocked; SharesValue totalFinalizedSharesUnlocked; ETHValue totalFinalizedAmountUnlocked; @@ -188,7 +226,7 @@ library AssetsAccounting { } function accountUnstETHFinalized( - State storage self, + Context storage self, uint256[] memory unstETHIds, uint256[] memory claimableAmounts ) internal { @@ -211,7 +249,7 @@ library AssetsAccounting { } function accountUnstETHClaimed( - State storage self, + Context storage self, uint256[] memory unstETHIds, uint256[] memory claimableAmounts ) internal returns (ETHValue totalAmountClaimed) { @@ -225,9 +263,9 @@ library AssetsAccounting { } function accountUnstETHWithdraw( - State storage self, + Context storage self, address holder, - uint256[] calldata unstETHIds + uint256[] memory unstETHIds ) internal returns (ETHValue amountWithdrawn) { uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { @@ -240,23 +278,23 @@ library AssetsAccounting { // Getters // --- - function getLockedAssetsTotals(State storage self) + function getLockedAssetsTotals(Context storage self) internal view - returns (SharesValue ufinalizedShares, ETHValue finalizedETH) + returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { finalizedETH = self.unstETHTotals.finalizedETH; - ufinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; } - function checkAssetsUnlockDelayPassed( - State storage self, + function checkMinAssetsLockDurationPassed( + Context storage self, address holder, - Duration assetsUnlockDelay + Duration minAssetsLockDuration ) internal view { - Timestamp assetsUnlockAllowedAfter = assetsUnlockDelay.addTo(self.assets[holder].lastAssetsLockTimestamp); + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); if (Timestamps.now() <= assetsUnlockAllowedAfter) { - revert AssetsUnlockDelayNotPassed(assetsUnlockAllowedAfter); + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); } } @@ -265,7 +303,7 @@ library AssetsAccounting { // --- function _addUnstETHRecord( - State storage self, + Context storage self, address holder, uint256 unstETHId, WithdrawalRequestStatus memory status @@ -287,14 +325,14 @@ library AssetsAccounting { self.unstETHRecords[unstETHId] = UnstETHRecord({ lockedBy: holder, status: UnstETHRecordStatus.Locked, - index: IndicesOneBased.from(assets.unstETHIds.length), + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), shares: shares, claimableAmount: ETHValues.ZERO }); } function _removeUnstETHRecord( - State storage self, + Context storage self, address holder, uint256 unstETHId ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { @@ -315,11 +353,11 @@ library AssetsAccounting { HolderAssets storage assets = self.assets[holder]; IndexOneBased unstETHIdIndex = unstETHRecord.index; - IndexOneBased lastUnstETHIdIndex = IndicesOneBased.from(assets.unstETHIds.length); + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); if (lastUnstETHIdIndex != unstETHIdIndex) { - uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.value()]; - assets.unstETHIds[unstETHIdIndex.value()] = lastUnstETHId; + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; } assets.unstETHIds.pop(); @@ -327,7 +365,7 @@ library AssetsAccounting { } function _finalizeUnstETHRecord( - State storage self, + Context storage self, uint256 unstETHId, uint256 claimableAmount ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { @@ -344,7 +382,7 @@ library AssetsAccounting { self.unstETHRecords[unstETHId] = unstETHRecord; } - function _claimUnstETHRecord(State storage self, uint256 unstETHId, ETHValue claimableAmount) private { + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) { @@ -363,7 +401,7 @@ library AssetsAccounting { } function _withdrawUnstETHRecord( - State storage self, + Context storage self, address holder, uint256 unstETHId ) private returns (ETHValue amountWithdrawn) { diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol new file mode 100644 index 00000000..c1913bd9 --- /dev/null +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {PercentD16} from "../types/PercentD16.sol"; +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +library DualGovernanceConfig { + struct Context { + PercentD16 firstSealRageQuitSupport; + PercentD16 secondSealRageQuitSupport; + Duration minAssetsLockDuration; + Duration dynamicTimelockMinDuration; + Duration dynamicTimelockMaxDuration; + Duration vetoSignallingMinActiveDuration; + Duration vetoSignallingDeactivationMaxDuration; + Duration vetoCooldownDuration; + Duration rageQuitExtensionDelay; + Duration rageQuitEthWithdrawalsMinTimelock; + uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; + uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; + } + + function isFirstSealRageQuitSupportCrossed( + Context memory self, + PercentD16 rageQuitSupport + ) internal pure returns (bool) { + return rageQuitSupport > self.firstSealRageQuitSupport; + } + + function isSecondSealRageQuitSupportCrossed( + Context memory self, + PercentD16 rageQuitSupport + ) internal pure returns (bool) { + return rageQuitSupport > self.secondSealRageQuitSupport; + } + + function isDynamicTimelockMaxDurationPassed( + Context memory self, + Timestamp vetoSignallingActivatedAt + ) internal view returns (bool) { + return Timestamps.now() > self.dynamicTimelockMaxDuration.addTo(vetoSignallingActivatedAt); + } + + function isDynamicTimelockDurationPassed( + Context memory self, + Timestamp vetoSignallingActivatedAt, + PercentD16 rageQuitSupport + ) internal view returns (bool) { + return Timestamps.now() > calcDynamicDelayDuration(self, rageQuitSupport).addTo(vetoSignallingActivatedAt); + } + + function isVetoSignallingReactivationDurationPassed( + Context memory self, + Timestamp vetoSignallingReactivationTime + ) internal view returns (bool) { + return Timestamps.now() > self.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivationTime); + } + + function isVetoSignallingDeactivationMaxDurationPassed( + Context memory self, + Timestamp vetoSignallingDeactivationEnteredAt + ) internal view returns (bool) { + return Timestamps.now() > self.vetoSignallingDeactivationMaxDuration.addTo(vetoSignallingDeactivationEnteredAt); + } + + function isVetoCooldownDurationPassed( + Context memory self, + Timestamp vetoCooldownEnteredAt + ) internal view returns (bool) { + return Timestamps.now() > self.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); + } + + function calcDynamicDelayDuration( + Context memory self, + PercentD16 rageQuitSupport + ) internal pure returns (Duration duration_) { + PercentD16 firstSealRageQuitSupport = self.firstSealRageQuitSupport; + PercentD16 secondSealRageQuitSupport = self.secondSealRageQuitSupport; + + Duration dynamicTimelockMinDuration = self.dynamicTimelockMinDuration; + Duration dynamicTimelockMaxDuration = self.dynamicTimelockMaxDuration; + + if (rageQuitSupport <= firstSealRageQuitSupport) { + return Durations.ZERO; + } + + if (rageQuitSupport >= secondSealRageQuitSupport) { + return dynamicTimelockMaxDuration; + } + + duration_ = dynamicTimelockMinDuration + + Durations.from( + PercentD16.unwrap(rageQuitSupport - firstSealRageQuitSupport) + * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() + / PercentD16.unwrap(secondSealRageQuitSupport - firstSealRageQuitSupport) + ); + } + + function calcRageQuitWithdrawalsTimelock( + Context memory self, + uint256 rageQuitRound + ) internal pure returns (Duration) { + if (rageQuitRound < self.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { + return self.rageQuitEthWithdrawalsMinTimelock; + } + return self.rageQuitEthWithdrawalsMinTimelock + + Durations.from( + ( + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound + + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] + ) / 10 ** 18 + ); // TODO: rewrite in a prettier way + } +} diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol deleted file mode 100644 index 664697ef..00000000 --- a/contracts/libraries/DualGovernanceState.sol +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; - -import {IEscrow} from "../interfaces/IEscrow.sol"; -import {ISealable} from "../interfaces/ISealable.sol"; -import {IDualGovernanceConfiguration as IConfiguration, DualGovernanceConfig} from "../interfaces/IConfiguration.sol"; - -import {Duration, Durations} from "../types/Duration.sol"; -import {Timestamp, Timestamps} from "../types/Timestamp.sol"; - -enum State { - Normal, - VetoSignalling, - VetoSignallingDeactivation, - VetoCooldown, - RageQuit -} - -library DualGovernanceState { - // TODO: Optimize storage layout efficiency - struct Store { - State state; - Timestamp enteredAt; - // the time the veto signalling state was entered - Timestamp vetoSignallingActivationTime; - IEscrow signallingEscrow; // 248 - // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - Timestamp vetoSignallingReactivationTime; - // the last time a proposal was submitted to the DG subsystem - Timestamp lastAdoptableStateExitedAt; - IEscrow rageQuitEscrow; - uint8 rageQuitRound; - } - - error NotTie(); - error AlreadyInitialized(); - error ProposalsCreationSuspended(); - error ProposalsAdoptionSuspended(); - error ResealIsNotAllowedInNormalState(); - - event NewSignallingEscrowDeployed(address indexed escrow); - event DualGovernanceStateChanged(State oldState, State newState); - - function initialize(Store storage self, address escrowMasterCopy) internal { - if (address(self.signallingEscrow) != address(0)) { - revert AlreadyInitialized(); - } - _deployNewSignallingEscrow(self, escrowMasterCopy); - } - - function activateNextState( - Store storage self, - DualGovernanceConfig memory config - ) internal returns (State newState) { - State oldState = self.state; - if (oldState == State.Normal) { - newState = _fromNormalState(self, config); - } else if (oldState == State.VetoSignalling) { - newState = _fromVetoSignallingState(self, config); - } else if (oldState == State.VetoSignallingDeactivation) { - newState = _fromVetoSignallingDeactivationState(self, config); - } else if (oldState == State.VetoCooldown) { - newState = _fromVetoCooldownState(self, config); - } else if (oldState == State.RageQuit) { - newState = _fromRageQuitState(self, config); - } else { - assert(false); - } - - if (oldState != newState) { - self.state = newState; - _handleStateTransitionSideEffects(self, config, oldState, newState); - emit DualGovernanceStateChanged(oldState, newState); - } - } - - // --- - // View Methods - // --- - - function checkProposalsCreationAllowed(Store storage self) internal view { - if (!isProposalsCreationAllowed(self)) { - revert ProposalsCreationSuspended(); - } - } - - function checkProposalsAdoptionAllowed(Store storage self) internal view { - if (!isProposalsAdoptionAllowed(self)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkCanScheduleProposal(Store storage self, Timestamp proposalSubmittedAt) internal view { - if (!canScheduleProposal(self, proposalSubmittedAt)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkTiebreak(Store storage self, IConfiguration config) internal view { - if (!isTiebreak(self, config)) { - revert NotTie(); - } - } - - function checkResealState(Store storage self) internal view { - if (self.state == State.Normal) { - revert ResealIsNotAllowedInNormalState(); - } - } - - function currentState(Store storage self) internal view returns (State) { - return self.state; - } - - function canScheduleProposal(Store storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { - State state = self.state; - if (state == State.Normal) return true; - if (state == State.VetoCooldown) { - return proposalSubmissionTime <= self.vetoSignallingActivationTime; - } - return false; - } - - function isProposalsCreationAllowed(Store storage self) internal view returns (bool) { - State state = self.state; - return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; - } - - function isProposalsAdoptionAllowed(Store storage self) internal view returns (bool) { - State state = self.state; - return state == State.Normal || state == State.VetoCooldown; - } - - function isTiebreak(Store storage self, IConfiguration config) internal view returns (bool) { - if (isProposalsAdoptionAllowed(self)) return false; - - // for the governance is locked for long period of time - if (Timestamps.now() >= config.TIE_BREAK_ACTIVATION_TIMEOUT().addTo(self.lastAdoptableStateExitedAt)) { - return true; - } - - if (self.state != State.RageQuit) return false; - - address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); - for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { - if (ISealable(sealableWithdrawalBlockers[i]).isPaused()) return true; - } - return false; - } - - function getVetoSignallingState( - Store storage self, - DualGovernanceConfig memory config - ) internal view returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) { - isActive = self.state == State.VetoSignalling; - duration = isActive ? getVetoSignallingDuration(self, config) : Duration.wrap(0); - enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; - activatedAt = isActive ? self.vetoSignallingActivationTime : Timestamps.ZERO; - } - - function getVetoSignallingDuration( - Store storage self, - DualGovernanceConfig memory config - ) internal view returns (Duration) { - uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); - return _calcDynamicTimelockDuration(config, totalSupport); - } - - struct VetoSignallingDeactivationState { - uint256 duration; - uint256 enteredAt; - } - - function getVetoSignallingDeactivationState( - Store storage self, - DualGovernanceConfig memory config - ) internal view returns (bool isActive, Duration duration, Timestamp enteredAt) { - isActive = self.state == State.VetoSignallingDeactivation; - duration = config.vetoSignallingDeactivationMaxDuration; - enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; - } - - // --- - // State Transitions - // --- - - function _fromNormalState(Store storage self, DualGovernanceConfig memory config) private view returns (State) { - return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) - ? State.VetoSignalling - : State.Normal; - } - - function _fromVetoSignallingState( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - - if (!_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { - return State.VetoSignalling; - } - - if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { - return State.RageQuit; - } - - return _isVetoSignallingReactivationDurationPassed(self, config) - ? State.VetoSignallingDeactivation - : State.VetoSignalling; - } - - function _fromVetoSignallingDeactivationState( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - - if (!_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { - return State.VetoSignalling; - } - - if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { - return State.RageQuit; - } - - if (_isVetoSignallingDeactivationMaxDurationPassed(self, config)) { - return State.VetoCooldown; - } - - return State.VetoSignallingDeactivation; - } - - function _fromVetoCooldownState( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (State) { - if (!_isVetoCooldownDurationPassed(self, config)) { - return State.VetoCooldown; - } - return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) - ? State.VetoSignalling - : State.Normal; - } - - function _fromRageQuitState(Store storage self, DualGovernanceConfig memory config) private view returns (State) { - if (!self.rageQuitEscrow.isRageQuitFinalized()) { - return State.RageQuit; - } - return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) - ? State.VetoSignalling - : State.VetoCooldown; - } - - // --- - // Helper Methods - // --- - - function _handleStateTransitionSideEffects( - Store storage self, - DualGovernanceConfig memory config, - State oldState, - State newState - ) private { - Timestamp timestamp = Timestamps.now(); - self.enteredAt = timestamp; - // track the time when the governance state allowed execution - if (oldState == State.Normal || oldState == State.VetoCooldown) { - self.lastAdoptableStateExitedAt = timestamp; - } - - if (newState == State.Normal && self.rageQuitRound != 0) { - self.rageQuitRound = 0; - } - - if (newState == State.VetoSignalling && oldState != State.VetoSignallingDeactivation) { - self.vetoSignallingActivationTime = timestamp; - } - - if (oldState == State.VetoSignallingDeactivation && newState == State.VetoSignalling) { - self.vetoSignallingReactivationTime = timestamp; - } - - if (newState == State.RageQuit) { - IEscrow signallingEscrow = self.signallingEscrow; - signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, _calcRageQuitWithdrawalsTimelock(config, self.rageQuitRound) - ); - self.rageQuitEscrow = signallingEscrow; - _deployNewSignallingEscrow(self, signallingEscrow.MASTER_COPY()); - self.rageQuitRound += 1; - } - } - - function _isFirstSealRageQuitSupportCrossed( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) private pure returns (bool) { - return rageQuitSupport > config.firstSealRageQuitSupport; - } - - function _isSecondSealRageQuitSupportCrossed( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) private pure returns (bool) { - return rageQuitSupport > config.secondSealRageQuitSupport; - } - - function _isDynamicTimelockMaxDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.dynamicTimelockMaxDuration.addTo(self.vetoSignallingActivationTime); - } - - function _isDynamicTimelockDurationPassed( - Store storage self, - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) private view returns (bool) { - Duration dynamicTimelock = _calcDynamicTimelockDuration(config, rageQuitSupport); - return Timestamps.now() > dynamicTimelock.addTo(self.vetoSignallingActivationTime); - } - - function _isVetoSignallingReactivationDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.vetoSignallingMinActiveDuration.addTo(self.vetoSignallingReactivationTime); - } - - function _isVetoSignallingDeactivationMaxDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.vetoSignallingDeactivationMaxDuration.addTo(self.enteredAt); - } - - function _isVetoCooldownDurationPassed( - Store storage self, - DualGovernanceConfig memory config - ) private view returns (bool) { - return Timestamps.now() > config.vetoCooldownDuration.addTo(self.enteredAt); - } - - function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { - IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); - clone.initialize(address(this)); - self.signallingEscrow = clone; - emit NewSignallingEscrowDeployed(address(clone)); - } - - function _calcRageQuitWithdrawalsTimelock( - DualGovernanceConfig memory config, - uint256 rageQuitRound - ) private pure returns (Duration) { - if (rageQuitRound < config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { - return config.rageQuitEthWithdrawalsMinTimelock; - } - return config.rageQuitEthWithdrawalsMinTimelock - + Durations.from( - ( - config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound - + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] - ) / 10 ** 18 - ); // TODO: rewrite in a prettier way - } - - function _calcDynamicTimelockDuration( - DualGovernanceConfig memory config, - uint256 rageQuitSupport - ) internal pure returns (Duration duration_) { - uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; - Duration dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; - Duration dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; - - if (rageQuitSupport < firstSealRageQuitSupport) { - return Durations.ZERO; - } - - if (rageQuitSupport >= secondSealRageQuitSupport) { - return dynamicTimelockMaxDuration; - } - - duration_ = dynamicTimelockMinDuration - + Durations.from( - (rageQuitSupport - firstSealRageQuitSupport) - * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() - / (secondSealRageQuitSupport - firstSealRageQuitSupport) - ); - } -} diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol new file mode 100644 index 00000000..59a9c810 --- /dev/null +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index e911977a..4adc7bcd 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -4,129 +4,181 @@ pragma solidity 0.8.26; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -struct EmergencyState { - address executionCommittee; - address activationCommittee; - Timestamp protectedTill; - bool isEmergencyModeActivated; - Duration emergencyModeDuration; - Timestamp emergencyModeEndsAfter; -} - +/// @title EmergencyProtection +/// @dev This library manages emergency protection functionality, allowing for +/// the activation and deactivation of emergency mode by designated committees. library EmergencyProtection { - error NotEmergencyActivator(address account); - error NotEmergencyEnactor(address account); - error EmergencyCommitteeExpired(Timestamp timestamp, Timestamp protectedTill); - error InvalidEmergencyModeActiveValue(bool actual, bool expected); - - event EmergencyModeActivated(Timestamp timestamp); - event EmergencyModeDeactivated(Timestamp timestamp); - event EmergencyActivationCommitteeSet(address indexed activationCommittee); - event EmergencyExecutionCommitteeSet(address indexed executionCommittee); - event EmergencyModeDurationSet(Duration emergencyModeDuration); - event EmergencyCommitteeProtectedTillSet(Timestamp newProtectedTill); - - struct State { - // has rights to activate emergency mode - address activationCommittee; - Timestamp protectedTill; - // till this time, the committee may activate the emergency mode + error CallerIsNotEmergencyActivationCommittee(address caller); + error CallerIsNotEmergencyExecutionCommittee(address caller); + error EmergencyProtectionExpired(Timestamp protectedTill); + error InvalidEmergencyModeDuration(Duration value); + error InvalidEmergencyProtectionEndDate(Timestamp value); + error UnexpectedEmergencyModeState(bool value); + + event EmergencyModeActivated(); + event EmergencyModeDeactivated(); + event EmergencyGovernanceSet(address newEmergencyGovernance); + event EmergencyActivationCommitteeSet(address newActivationCommittee); + event EmergencyExecutionCommitteeSet(address newActivationCommittee); + event EmergencyModeDurationSet(Duration newEmergencyModeDuration); + event EmergencyProtectionEndDateSet(Timestamp newEmergencyProtectionEndDate); + + struct Context { + /// @dev slot0 [0..39] Timestamp emergencyModeEndsAfter; + /// @dev slot0 [40..199] + address emergencyActivationCommittee; + /// @dev slot0 [200..240] + Timestamp emergencyProtectionEndsAfter; + /// @dev slot1 [0..159] + address emergencyExecutionCommittee; + /// @dev slot1 [160..191] Duration emergencyModeDuration; - // has rights to execute proposals in emergency mode - address executionCommittee; + /// @dev slot2 [0..160] + address emergencyGovernance; } - function setup( - State storage self, - address activationCommittee, - address executionCommittee, - Duration protectionDuration, - Duration emergencyModeDuration - ) internal { - address prevActivationCommittee = self.activationCommittee; - if (activationCommittee != prevActivationCommittee) { - self.activationCommittee = activationCommittee; - emit EmergencyActivationCommitteeSet(activationCommittee); - } - - address prevExecutionCommittee = self.executionCommittee; - if (executionCommittee != prevExecutionCommittee) { - self.executionCommittee = executionCommittee; - emit EmergencyExecutionCommitteeSet(executionCommittee); - } + // --- + // Main functionality + // --- - Timestamp prevProtectedTill = self.protectedTill; - Timestamp newProtectedTill = protectionDuration.addTo(Timestamps.now()); + /// @dev Activates the emergency mode. + /// @param self The storage reference to the Context struct. + function activateEmergencyMode(Context storage self) internal { + Timestamp now_ = Timestamps.now(); - if (newProtectedTill != prevProtectedTill) { - self.protectedTill = newProtectedTill; - emit EmergencyCommitteeProtectedTillSet(newProtectedTill); + if (now_ > self.emergencyProtectionEndsAfter) { + revert EmergencyProtectionExpired(self.emergencyProtectionEndsAfter); } - Duration prevEmergencyModeDuration = self.emergencyModeDuration; - if (emergencyModeDuration != prevEmergencyModeDuration) { - self.emergencyModeDuration = emergencyModeDuration; - emit EmergencyModeDurationSet(emergencyModeDuration); - } - } + self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(now_); - function activate(State storage self) internal { - Timestamp timestamp = Timestamps.now(); - if (timestamp > self.protectedTill) { - revert EmergencyCommitteeExpired(timestamp, self.protectedTill); - } - self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(timestamp); - emit EmergencyModeActivated(timestamp); + emit EmergencyModeActivated(); } - function deactivate(State storage self) internal { - self.activationCommittee = address(0); - self.executionCommittee = address(0); - self.protectedTill = Timestamps.ZERO; + /// @dev Deactivates the emergency mode. + /// @param self The storage reference to the Context struct. + function deactivateEmergencyMode(Context storage self) internal { + self.emergencyActivationCommittee = address(0); + self.emergencyExecutionCommittee = address(0); + self.emergencyProtectionEndsAfter = Timestamps.ZERO; self.emergencyModeEndsAfter = Timestamps.ZERO; self.emergencyModeDuration = Durations.ZERO; - emit EmergencyModeDeactivated(Timestamps.now()); + emit EmergencyModeDeactivated(); } - function getEmergencyState(State storage self) internal view returns (EmergencyState memory res) { - res.executionCommittee = self.executionCommittee; - res.activationCommittee = self.activationCommittee; - res.protectedTill = self.protectedTill; - res.emergencyModeDuration = self.emergencyModeDuration; - res.emergencyModeEndsAfter = self.emergencyModeEndsAfter; - res.isEmergencyModeActivated = isEmergencyModeActivated(self); + // --- + // Setup functionality + // --- + + function setEmergencyGovernance(Context storage self, address newEmergencyGovernance) internal { + if (newEmergencyGovernance == self.emergencyGovernance) { + return; + } + self.emergencyGovernance = newEmergencyGovernance; + emit EmergencyGovernanceSet(newEmergencyGovernance); } - function isEmergencyModeActivated(State storage self) internal view returns (bool) { - return self.emergencyModeEndsAfter.isNotZero(); + function setEmergencyProtectionEndDate( + Context storage self, + Timestamp newEmergencyProtectionEndDate, + Duration maxEmergencyProtectionDuration + ) internal { + if (newEmergencyProtectionEndDate > maxEmergencyProtectionDuration.addTo(Timestamps.now())) { + revert InvalidEmergencyProtectionEndDate(newEmergencyProtectionEndDate); + } + + if (newEmergencyProtectionEndDate == self.emergencyProtectionEndsAfter) { + return; + } + self.emergencyProtectionEndsAfter = newEmergencyProtectionEndDate; + emit EmergencyProtectionEndDateSet(newEmergencyProtectionEndDate); } - function isEmergencyModePassed(State storage self) internal view returns (bool) { - Timestamp endsAfter = self.emergencyModeEndsAfter; - return endsAfter.isNotZero() && Timestamps.now() > endsAfter; + function setEmergencyModeDuration( + Context storage self, + Duration newEmergencyModeDuration, + Duration maxEmergencyModeDuration + ) internal { + if (newEmergencyModeDuration > maxEmergencyModeDuration) { + revert InvalidEmergencyModeDuration(newEmergencyModeDuration); + } + if (newEmergencyModeDuration == self.emergencyModeDuration) { + return; + } + + self.emergencyModeDuration = newEmergencyModeDuration; + emit EmergencyModeDurationSet(newEmergencyModeDuration); } - function isEmergencyProtectionEnabled(State storage self) internal view returns (bool) { - return Timestamps.now() <= self.protectedTill || self.emergencyModeEndsAfter.isNotZero(); + function setEmergencyActivationCommittee(Context storage self, address newActivationCommittee) internal { + if (newActivationCommittee == self.emergencyActivationCommittee) { + return; + } + self.emergencyActivationCommittee = newActivationCommittee; + emit EmergencyActivationCommitteeSet(newActivationCommittee); } - function checkActivationCommittee(State storage self, address account) internal view { - if (self.activationCommittee != account) { - revert NotEmergencyActivator(account); + function setEmergencyExecutionCommittee(Context storage self, address newExecutionCommittee) internal { + if (newExecutionCommittee == self.emergencyExecutionCommittee) { + return; } + self.emergencyExecutionCommittee = newExecutionCommittee; + emit EmergencyExecutionCommitteeSet(newExecutionCommittee); } - function checkExecutionCommittee(State storage self, address account) internal view { - if (self.executionCommittee != account) { - revert NotEmergencyEnactor(account); + // --- + // Checks + // --- + + /// @dev Checks if the caller is the emergency activator and reverts if not. + /// @param self The storage reference to the Context struct. + function checkCallerIsEmergencyActivationCommittee(Context storage self) internal view { + if (self.emergencyActivationCommittee != msg.sender) { + revert CallerIsNotEmergencyActivationCommittee(msg.sender); } } - function checkEmergencyModeActive(State storage self, bool expected) internal view { - bool actual = isEmergencyModeActivated(self); - if (actual != expected) { - revert InvalidEmergencyModeActiveValue(actual, expected); + /// @dev Checks if the caller is the emergency enactor and reverts if not. + /// @param self The storage reference to the Context struct. + function checkCallerIsEmergencyExecutionCommittee(Context storage self) internal view { + if (self.emergencyExecutionCommittee != msg.sender) { + revert CallerIsNotEmergencyExecutionCommittee(msg.sender); } } + + /// @dev Checks if the emergency mode matches with expected passed value and reverts if not. + /// @param self The storage reference to the Context struct. + /// @param isActive The expected value of the emergency mode. + function checkEmergencyMode(Context storage self, bool isActive) internal view { + if (isEmergencyModeActive(self) != isActive) { + revert UnexpectedEmergencyModeState(isActive); + } + } + + // --- + // Getters + // --- + + /// @dev Checks if the emergency mode is activated + /// @param self The storage reference to the Context struct. + /// @return Whether the emergency mode is activated or not. + function isEmergencyModeActive(Context storage self) internal view returns (bool) { + return self.emergencyModeEndsAfter.isNotZero(); + } + + /// @dev Checks if the emergency mode has passed. + /// @param self The storage reference to the Context struct. + /// @return Whether the emergency mode has passed or not. + function isEmergencyModeDurationPassed(Context storage self) internal view returns (bool) { + Timestamp endsAfter = self.emergencyModeEndsAfter; + return endsAfter.isNotZero() && Timestamps.now() > endsAfter; + } + + /// @dev Checks if the emergency protection is enabled. + /// @param self The storage reference to the Context struct. + /// @return Whether the emergency protection is enabled or not. + function isEmergencyProtectionEnabled(Context storage self) internal view returns (bool) { + return Timestamps.now() <= self.emergencyProtectionEndsAfter || self.emergencyModeEndsAfter.isNotZero(); + } } diff --git a/contracts/libraries/EnumerableProposals.sol b/contracts/libraries/EnumerableProposals.sol index 31610365..031b2033 100644 --- a/contracts/libraries/EnumerableProposals.sol +++ b/contracts/libraries/EnumerableProposals.sol @@ -9,6 +9,9 @@ struct Proposal { bytes data; } +/// @title Enumerable Proposals Library +/// @notice Library to manage a set of proposals with enumerable functionality +/// @dev Uses EnumerableSet for managing the proposal keys library EnumerableProposals { using EnumerableSet for EnumerableSet.Bytes32Set; @@ -21,6 +24,13 @@ library EnumerableProposals { mapping(bytes32 key => Proposal) _proposals; } + /// @notice Adds a new proposal to the map + /// @dev Adds the proposal if it does not already exist in the map + /// @param map The map to add the proposal to + /// @param key The key of the proposal + /// @param proposalType The type of the proposal + /// @param data The data associated with the proposal + /// @return success A boolean indicating if the proposal was added successfully function push( Bytes32ToProposalMap storage map, bytes32 key, @@ -37,19 +47,38 @@ library EnumerableProposals { return false; } + /// @notice Checks if a proposal exists in the map + /// @dev Checks if the key is present in the set of keys + /// @param map The map to check + /// @param key The key of the proposal + /// @return exists A boolean indicating if the proposal exists function contains(Bytes32ToProposalMap storage map, bytes32 key) internal view returns (bool) { return map._keys.contains(key); } + /// @notice Gets the number of proposals in the map + /// @dev Returns the length of the ordered keys array + /// @param map The map to check + /// @return length The number of proposals in the map function length(Bytes32ToProposalMap storage map) internal view returns (uint256) { return map._orderedKeys.length; } + /// @notice Gets a proposal at a specific index + /// @dev Returns the proposal at the specified index in the ordered keys array + /// @param map The map to check + /// @param index The index to retrieve + /// @return proposal The proposal at the specified index function at(Bytes32ToProposalMap storage map, uint256 index) internal view returns (Proposal memory) { bytes32 key = map._orderedKeys[index]; return map._proposals[key]; } + /// @notice Gets a proposal by its key + /// @dev Returns the proposal associated with the given key, reverts if the proposal does not exist + /// @param map The map to check + /// @param key The key of the proposal + /// @return value The proposal associated with the given key function get(Bytes32ToProposalMap storage map, bytes32 key) internal view returns (Proposal memory value) { if (!contains(map, key)) { revert ProposalDoesNotExist(key); @@ -57,10 +86,20 @@ library EnumerableProposals { value = map._proposals[key]; } + /// @notice Gets the ordered keys of the proposals + /// @dev Returns the array of ordered keys + /// @param map The map to check + /// @return keys The ordered keys of the proposals function getOrderedKeys(Bytes32ToProposalMap storage map) internal view returns (bytes32[] memory) { return map._orderedKeys; } + /// @notice Gets a subset of ordered keys with pagination + /// @dev Returns a subset of the ordered keys based on the provided offset and limit + /// @param map The map to check + /// @param offset The starting index for the subset + /// @param limit The maximum number of keys to return + /// @return keys The subset of ordered keys function getOrderedKeys( Bytes32ToProposalMap storage map, uint256 offset, diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol new file mode 100644 index 00000000..9b6fd842 --- /dev/null +++ b/contracts/libraries/EscrowState.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +/// @notice The state of Escrow representing the current set of actions allowed to be called +/// on the Escrow instance. +/// @param NotInitialized The default (uninitialized) state of the Escrow contract. Only the master +/// copy of the Escrow contract is expected to be in this state. +/// @param SignallingEscrow In this state, the Escrow contract functions as an on-chain oracle for measuring stakers' disagreement +/// with DAO decisions. Users are allowed to lock and unlock funds in the Escrow contract in this state. +/// @param RageQuitEscrow The final state of the Escrow contract. In this state, the Escrow instance acts as an accumulator +/// for withdrawn funds locked during the VetoSignalling phase. +enum State { + NotInitialized, + SignallingEscrow, + RageQuitEscrow +} + +/// @notice Represents the logic to manipulate the state of the Escrow +library EscrowState { + // --- + // Errors + // --- + + error ClaimingIsFinished(); + error UnexpectedState(State value); + error RageQuitExtraTimelockNotStarted(); + error WithdrawalsTimelockNotPassed(); + error BatchesCreationNotInProgress(); + + // --- + // Events + // --- + + event RageQuitTimelockStarted(); + event EscrowStateChanged(State from, State to); + event RageQuitStarted(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock); + event MinAssetsLockDurationSet(Duration newAssetsLockDuration); + + /// @notice Stores the context of the state of the Escrow instance + /// @param state The current state of the Escrow instance + /// @param minAssetsLockDuration The minimum time required to pass before tokens can be unlocked from the Escrow + /// contract instance + /// @param rageQuitExtensionDelay The period of time that starts after all withdrawal batches are formed, which delays + /// the exit from the RageQuit state of the DualGovernance. The main purpose of the rage quit extension delay is to provide + /// enough time for users who locked their unstETH to claim it. + struct Context { + /// @dev slot0: [0..7] + State state; + /// @dev slot0: [8..39] + Duration minAssetsLockDuration; + /// @dev slot0: [40..71] + Duration rageQuitExtensionDelay; + /// @dev slot0: [72..111] + Timestamp rageQuitExtensionDelayStartedAt; + /// @dev slot0: [112..143] + Duration rageQuitWithdrawalsTimelock; + } + + function initialize(Context storage self, Duration minAssetsLockDuration) internal { + _checkState(self, State.NotInitialized); + _setState(self, State.SignallingEscrow); + _setMinAssetsLockDuration(self, minAssetsLockDuration); + } + + function startRageQuit( + Context storage self, + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock + ) internal { + _checkState(self, State.SignallingEscrow); + _setState(self, State.RageQuitEscrow); + self.rageQuitExtensionDelay = rageQuitExtensionDelay; + self.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + emit RageQuitStarted(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + } + + function startRageQuitExtensionDelay(Context storage self) internal { + self.rageQuitExtensionDelayStartedAt = Timestamps.now(); + emit RageQuitTimelockStarted(); + } + + function setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) internal { + if (self.minAssetsLockDuration == newMinAssetsLockDuration) { + return; + } + _setMinAssetsLockDuration(self, newMinAssetsLockDuration); + } + + // --- + // Checks + // --- + + function checkSignallingEscrow(Context storage self) internal view { + _checkState(self, State.SignallingEscrow); + } + + function checkRageQuitEscrow(Context storage self) internal view { + _checkState(self, State.RageQuitEscrow); + } + + function checkBatchesClaimingInProgress(Context storage self) internal view { + if (!self.rageQuitExtensionDelayStartedAt.isZero()) { + revert ClaimingIsFinished(); + } + } + + function checkWithdrawalsTimelockPassed(Context storage self) internal view { + if (self.rageQuitExtensionDelayStartedAt.isZero()) { + revert RageQuitExtraTimelockNotStarted(); + } + Duration withdrawalsTimelock = self.rageQuitExtensionDelay + self.rageQuitWithdrawalsTimelock; + if (Timestamps.now() <= withdrawalsTimelock.addTo(self.rageQuitExtensionDelayStartedAt)) { + revert WithdrawalsTimelockNotPassed(); + } + } + + // --- + // Getters + // --- + function isRageQuitExtensionDelayStarted(Context storage self) internal view returns (bool) { + return self.rageQuitExtensionDelayStartedAt.isNotZero(); + } + + function isRageQuitExtensionDelayPassed(Context storage self) internal view returns (bool) { + Timestamp rageQuitExtensionDelayStartedAt = self.rageQuitExtensionDelayStartedAt; + return rageQuitExtensionDelayStartedAt.isNotZero() + && Timestamps.now() > self.rageQuitExtensionDelay.addTo(rageQuitExtensionDelayStartedAt); + } + + function isRageQuitEscrow(Context storage self) internal view returns (bool) { + return self.state == State.RageQuitEscrow; + } + + // --- + // Private Methods + // --- + + function _checkState(Context storage self, State state) private view { + if (self.state != state) { + revert UnexpectedState(state); + } + } + + function _setState(Context storage self, State newState) private { + State prevState = self.state; + self.state = newState; + emit EscrowStateChanged(prevState, newState); + } + + function _setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) private { + self.minAssetsLockDuration = newMinAssetsLockDuration; + emit MinAssetsLockDurationSet(newMinAssetsLockDuration); + } +} diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol new file mode 100644 index 00000000..bd65908e --- /dev/null +++ b/contracts/libraries/ExecutableProposals.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + revert ProposalNotSubmitted(proposalId); + } + + if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + revert ProposalNotScheduled(proposalId); + } + + if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} diff --git a/contracts/libraries/ExternalCalls.sol b/contracts/libraries/ExternalCalls.sol new file mode 100644 index 00000000..dfa9838f --- /dev/null +++ b/contracts/libraries/ExternalCalls.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IExternalExecutor} from "../interfaces/IExternalExecutor.sol"; + +struct ExternalCall { + address target; + uint96 value; // ~ 7.9 billion ETH + bytes payload; +} + +library ExternalCalls { + function execute( + ExternalCall[] memory calls, + IExternalExecutor executor + ) internal returns (bytes[] memory results) { + uint256 callsCount = calls.length; + results = new bytes[](callsCount); + for (uint256 i = 0; i < callsCount; ++i) { + results[i] = executor.execute(calls[i].target, calls[i].value, calls[i].payload); + } + } +} diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol deleted file mode 100644 index cf0b0b06..00000000 --- a/contracts/libraries/Proposals.sol +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "../types/Duration.sol"; -import {Timestamp, Timestamps} from "../types/Timestamp.sol"; - -import {IExecutor, ExecutorCall} from "../interfaces/IExecutor.sol"; - -enum Status { - NotExist, - Submitted, - Scheduled, - Executed, - Cancelled -} - -struct Proposal { - uint256 id; - Status status; - address executor; - Timestamp submittedAt; - Timestamp scheduledAt; - Timestamp executedAt; - ExecutorCall[] calls; -} - -library Proposals { - struct ProposalPacked { - address executor; - Timestamp submittedAt; - Timestamp scheduledAt; - Timestamp executedAt; - ExecutorCall[] calls; - } - - struct State { - // any proposals with ids less or equal to the given one cannot be executed - uint256 lastCancelledProposalId; - ProposalPacked[] proposals; - } - - error EmptyCalls(); - error ProposalCancelled(uint256 proposalId); - error ProposalNotFound(uint256 proposalId); - error ProposalNotScheduled(uint256 proposalId); - error ProposalNotSubmitted(uint256 proposalId); - error AfterSubmitDelayNotPassed(uint256 proposalId); - error AfterScheduleDelayNotPassed(uint256 proposalId); - - event ProposalScheduled(uint256 indexed id); - event ProposalSubmitted(uint256 indexed id, address indexed executor, ExecutorCall[] calls); - event ProposalExecuted(uint256 indexed id, bytes[] callResults); - event ProposalsCancelledTill(uint256 proposalId); - - // The id of the first proposal - uint256 private constant PROPOSAL_ID_OFFSET = 1; - - function submit( - State storage self, - address executor, - ExecutorCall[] memory calls - ) internal returns (uint256 newProposalId) { - if (calls.length == 0) { - revert EmptyCalls(); - } - - uint256 newProposalIndex = self.proposals.length; - - self.proposals.push(); - ProposalPacked storage newProposal = self.proposals[newProposalIndex]; - - newProposal.executor = executor; - newProposal.submittedAt = Timestamps.now(); - - // copying of arrays of custom types from calldata to storage has not been supported by the - // Solidity compiler yet, so insert item by item - for (uint256 i = 0; i < calls.length; ++i) { - newProposal.calls.push(calls[i]); - } - - newProposalId = newProposalIndex + PROPOSAL_ID_OFFSET; - emit ProposalSubmitted(newProposalId, executor, calls); - } - - function schedule(State storage self, uint256 proposalId, Duration afterSubmitDelay) internal { - _checkProposalSubmitted(self, proposalId); - _checkAfterSubmitDelayPassed(self, proposalId, afterSubmitDelay); - - ProposalPacked storage proposal = _packed(self, proposalId); - proposal.scheduledAt = Timestamps.now(); - - emit ProposalScheduled(proposalId); - } - - function execute(State storage self, uint256 proposalId, Duration afterScheduleDelay) internal { - _checkProposalScheduled(self, proposalId); - _checkAfterScheduleDelayPassed(self, proposalId, afterScheduleDelay); - _executeProposal(self, proposalId); - } - - function cancelAll(State storage self) internal { - uint256 lastProposalId = self.proposals.length; - self.lastCancelledProposalId = lastProposalId; - emit ProposalsCancelledTill(lastProposalId); - } - - function get(State storage self, uint256 proposalId) internal view returns (Proposal memory proposal) { - _checkProposalExists(self, proposalId); - ProposalPacked storage packed = _packed(self, proposalId); - - proposal.id = proposalId; - proposal.status = _getProposalStatus(self, proposalId); - proposal.executor = packed.executor; - proposal.submittedAt = packed.submittedAt; - proposal.scheduledAt = packed.scheduledAt; - proposal.executedAt = packed.executedAt; - proposal.calls = packed.calls; - } - - function getProposalSubmissionTime( - State storage self, - uint256 proposalId - ) internal view returns (Timestamp submittedAt) { - _checkProposalExists(self, proposalId); - submittedAt = _packed(self, proposalId).submittedAt; - } - - function count(State storage self) internal view returns (uint256 count_) { - count_ = self.proposals.length; - } - - function canExecute( - State storage self, - uint256 proposalId, - Duration afterScheduleDelay - ) internal view returns (bool) { - return _getProposalStatus(self, proposalId) == Status.Scheduled - && Timestamps.now() >= afterScheduleDelay.addTo(_packed(self, proposalId).scheduledAt); - } - - function canSchedule( - State storage self, - uint256 proposalId, - Duration afterSubmitDelay - ) internal view returns (bool) { - return _getProposalStatus(self, proposalId) == Status.Submitted - && Timestamps.now() >= afterSubmitDelay.addTo(_packed(self, proposalId).submittedAt); - } - - function _executeProposal(State storage self, uint256 proposalId) private { - ProposalPacked storage packed = _packed(self, proposalId); - packed.executedAt = Timestamps.now(); - - ExecutorCall[] memory calls = packed.calls; - uint256 callsCount = calls.length; - - assert(callsCount > 0); - - address executor = packed.executor; - bytes[] memory results = new bytes[](callsCount); - for (uint256 i = 0; i < callsCount; ++i) { - results[i] = IExecutor(payable(executor)).execute(calls[i].target, calls[i].value, calls[i].payload); - } - emit ProposalExecuted(proposalId, results); - } - - function _packed(State storage self, uint256 proposalId) private view returns (ProposalPacked storage packed) { - packed = self.proposals[proposalId - PROPOSAL_ID_OFFSET]; - } - - function _checkProposalExists(State storage self, uint256 proposalId) private view { - if (proposalId < PROPOSAL_ID_OFFSET || proposalId > self.proposals.length) { - revert ProposalNotFound(proposalId); - } - } - - function _checkProposalSubmitted(State storage self, uint256 proposalId) private view { - Status status = _getProposalStatus(self, proposalId); - if (status != Status.Submitted) { - revert ProposalNotSubmitted(proposalId); - } - } - - function _checkProposalScheduled(State storage self, uint256 proposalId) private view { - Status status = _getProposalStatus(self, proposalId); - if (status != Status.Scheduled) { - revert ProposalNotScheduled(proposalId); - } - } - - function _checkAfterSubmitDelayPassed( - State storage self, - uint256 proposalId, - Duration afterSubmitDelay - ) private view { - if (Timestamps.now() < afterSubmitDelay.addTo(_packed(self, proposalId).submittedAt)) { - revert AfterSubmitDelayNotPassed(proposalId); - } - } - - function _checkAfterScheduleDelayPassed( - State storage self, - uint256 proposalId, - Duration afterScheduleDelay - ) private view { - if (Timestamps.now() < afterScheduleDelay.addTo(_packed(self, proposalId).scheduledAt)) { - revert AfterScheduleDelayNotPassed(proposalId); - } - } - - function _getProposalStatus(State storage self, uint256 proposalId) private view returns (Status status) { - if (proposalId < PROPOSAL_ID_OFFSET || proposalId > self.proposals.length) return Status.NotExist; - - ProposalPacked storage packed = _packed(self, proposalId); - - if (packed.executedAt.isNotZero()) return Status.Executed; - if (proposalId <= self.lastCancelledProposalId) return Status.Cancelled; - if (packed.scheduledAt.isNotZero()) return Status.Scheduled; - if (packed.submittedAt.isNotZero()) return Status.Submitted; - assert(false); - } -} diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 746dac07..fce7f2e0 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -1,132 +1,167 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {IAdminExecutorConfiguration as IConfiguration} from "../interfaces/IConfiguration.sol"; - -struct Proposer { - bool isAdmin; - address account; - address executor; -} - -struct ProposerData { - address proposer; - address executor; - bool isAdmin; -} +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; +/// @title Proposers Library +/// @dev This library manages proposers and their assigned executors in a governance system, providing functions to register, +/// unregister, and verify proposers and their roles. It ensures proper assignment and validation of proposers and executors. library Proposers { - using SafeCast for uint256; - - error NotProposer(address account); - error NotAssignedExecutor(address account, address actualExecutor, address expectedExecutor); - error NotAdminProposer(address account); + // --- + // Errors + // --- + error InvalidExecutor(address executor); + error InvalidProposerAccount(address account); error ProposerNotRegistered(address proposer); error ProposerAlreadyRegistered(address proposer); - error InvalidAdminExecutor(address executor); - error ExecutorNotRegistered(address account); - error LastAdminProposerRemoval(); + + // --- + // Events + // --- event AdminExecutorSet(address indexed adminExecutor); event ProposerRegistered(address indexed proposer, address indexed executor); event ProposerUnregistered(address indexed proposer, address indexed executor); + // --- + // Data Types + // --- + + /// @notice The info about the registered proposer and associated executor + /// @param account Address of the proposer + /// @param executor Address of the executor associated with proposer. When proposer submits proposals, they execution + /// will be done with this address. + struct Proposer { + address account; + address executor; + } + + /// @notice The internal info about the proposer's executor data + /// @param proposerIndex The one-based index of the proposer associated with the `executor` from + /// the `Context.proposers` array + /// @param executor The address of the executor associated with the proposer struct ExecutorData { - uint8 proposerIndexOneBased; // indexed from 1. The count of executors is limited + /// @dev slot0: [0..31] + IndexOneBased proposerIndex; + /// @dev slot0: [32..191] address executor; } - struct State { + /// @notice The context of the Proposers library + /// @param proposers The list of the registered proposers + /// @param executors The mapping with the executor info of the registered proposers + /// @param executorRefsCounts The mapping with the count of how many proposers is associated + /// with given executor address + struct Context { address[] proposers; mapping(address proposer => ExecutorData) executors; mapping(address executor => uint256 usagesCount) executorRefsCounts; } - function register(State storage self, address proposer, address executor) internal { - if (self.executors[proposer].proposerIndexOneBased != 0) { - revert ProposerAlreadyRegistered(proposer); + // --- + // Main Functionality + // --- + + /// @dev Registers a proposer with an assigned executor. + /// @param self The storage state of the Proposers library. + /// @param proposerAccount The address of the proposer to register. + /// @param executor The address of the assigned executor. + function register(Context storage self, address proposerAccount, address executor) internal { + if (proposerAccount == address(0)) { + revert InvalidProposerAccount(proposerAccount); } - self.proposers.push(proposer); - self.executors[proposer] = ExecutorData(self.proposers.length.toUint8(), executor); - self.executorRefsCounts[executor] += 1; - emit ProposerRegistered(proposer, executor); - } - function unregister(State storage self, IConfiguration config, address proposer) internal { - uint256 proposerIndexToDelete; - ExecutorData memory executorData = self.executors[proposer]; - unchecked { - proposerIndexToDelete = executorData.proposerIndexOneBased - 1; - } - if (proposerIndexToDelete == type(uint256).max) { - revert ProposerNotRegistered(proposer); + if (executor == address(0)) { + revert InvalidExecutor(executor); } - uint256 lastProposerIndex = self.proposers.length - 1; - if (proposerIndexToDelete != lastProposerIndex) { - self.proposers[proposerIndexToDelete] = self.proposers[lastProposerIndex]; + if (_isRegisteredProposer(self.executors[proposerAccount])) { + revert ProposerAlreadyRegistered(proposerAccount); } - self.proposers.pop(); - delete self.executors[proposer]; - address executor = executorData.executor; - if (executor == config.ADMIN_EXECUTOR() && self.executorRefsCounts[executor] == 1) { - revert LastAdminProposerRemoval(); - } + self.proposers.push(proposerAccount); + self.executors[proposerAccount] = + ExecutorData({proposerIndex: IndicesOneBased.fromOneBasedValue(self.proposers.length), executor: executor}); + self.executorRefsCounts[executor] += 1; - self.executorRefsCounts[executor] -= 1; - emit ProposerUnregistered(proposer, executor); + emit ProposerRegistered(proposerAccount, executor); } - function all(State storage self) internal view returns (Proposer[] memory proposers) { - proposers = new Proposer[](self.proposers.length); - for (uint256 i = 0; i < proposers.length; ++i) { - proposers[i] = get(self, self.proposers[i]); + /// @dev Unregisters a proposer. + /// @param self The storage state of the Proposers library. + /// @param proposerAccount The address of the proposer to unregister. + function unregister(Context storage self, address proposerAccount) internal { + ExecutorData memory executorData = self.executors[proposerAccount]; + + _checkRegisteredProposer(proposerAccount, executorData); + + IndexOneBased lastProposerIndex = IndicesOneBased.fromOneBasedValue(self.proposers.length); + if (executorData.proposerIndex != lastProposerIndex) { + self.proposers[executorData.proposerIndex.toZeroBasedValue()] = + self.proposers[lastProposerIndex.toZeroBasedValue()]; } + + self.proposers.pop(); + delete self.executors[proposerAccount]; + self.executorRefsCounts[executorData.executor] -= 1; + + emit ProposerUnregistered(proposerAccount, executorData.executor); } - function get(State storage self, address account) internal view returns (Proposer memory proposer) { - ExecutorData memory executorData = self.executors[account]; - if (executorData.proposerIndexOneBased == 0) { - revert ProposerNotRegistered(account); - } - proposer.account = account; + // --- + // Getters + // --- + + /// @dev Retrieves the details of a specific proposer. + /// @param self The storage state of the Proposers library. + /// @param proposerAccount The address of the proposer. + /// @return proposer The struct representing the details of the proposer. + function getProposer( + Context storage self, + address proposerAccount + ) internal view returns (Proposer memory proposer) { + ExecutorData memory executorData = self.executors[proposerAccount]; + _checkRegisteredProposer(proposerAccount, executorData); + + proposer.account = proposerAccount; proposer.executor = executorData.executor; } - function isProposer(State storage self, address account) internal view returns (bool) { - return self.executors[account].proposerIndexOneBased != 0; + /// @dev Retrieves all registered proposers. + /// @param self The storage state of the Proposers library. + /// @return proposers An array of structs representing all registered proposers. + function getAllProposers(Context storage self) internal view returns (Proposer[] memory proposers) { + proposers = new Proposer[](self.proposers.length); + for (uint256 i = 0; i < proposers.length; ++i) { + proposers[i] = getProposer(self, self.proposers[i]); + } } - function isAdminProposer(State storage self, IConfiguration config, address account) internal view returns (bool) { - ExecutorData memory executorData = self.executors[account]; - return executorData.proposerIndexOneBased != 0 && executorData.executor == config.ADMIN_EXECUTOR(); + /// @dev Checks if an account is a registered proposer. + /// @param self The storage state of the Proposers library. + /// @param account The address to check. + /// @return A boolean indicating whether the account is a registered proposer. + function isProposer(Context storage self, address account) internal view returns (bool) { + return _isRegisteredProposer(self.executors[account]); } - function isExecutor(State storage self, address account) internal view returns (bool) { + /// @dev Checks if an account is an executor. + /// @param self The storage state of the Proposers library. + /// @param account The address to check. + /// @return A boolean indicating whether the account is an executor. + function isExecutor(Context storage self, address account) internal view returns (bool) { return self.executorRefsCounts[account] > 0; } - function checkProposer(State storage self, address account) internal view { - if (!isProposer(self, account)) { - revert NotProposer(account); + /// @dev Checks that proposer with given executorData is registered proposer + function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { + if (!_isRegisteredProposer(executorData)) { + revert ProposerNotRegistered(proposerAccount); } } - function checkExecutor(State storage self, address account, address executor) internal view { - checkProposer(self, account); - ExecutorData memory executorData = self.executors[account]; - if (executor != executorData.executor) { - revert NotAssignedExecutor(account, executor, executorData.executor); - } - } - - function checkAdminProposer(State storage self, IConfiguration config, address account) internal view { - checkProposer(self, account); - if (!isAdminProposer(self, config, account)) { - revert NotAdminProposer(account); - } + /// @dev Returns if the executorData belongs to registered proposer + function _isRegisteredProposer(ExecutorData memory executorData) internal pure returns (bool) { + return executorData.proposerIndex.isNotEmpty(); } } diff --git a/contracts/libraries/SealableCalls.sol b/contracts/libraries/SealableCalls.sol index e472faad..bff9cc2b 100644 --- a/contracts/libraries/SealableCalls.sol +++ b/contracts/libraries/SealableCalls.sol @@ -3,7 +3,18 @@ pragma solidity 0.8.26; import {ISealable} from "../interfaces/ISealable.sol"; +/// @title SealableCalls Library +/// @dev A library for making calls to a contract implementing the ISealable interface. library SealableCalls { + /// @dev Calls the `pauseFor` function on a `Sealable` contract with the specified `sealDuration`. + /// If the call is successful and the contract is paused, it returns `true` and low-level error message, if any. + /// If the call fails, it returns `false` and the low-level error message. + /// + /// @param sealable The `Sealable` contract to call the `pauseFor` function on. + /// @param sealDuration The duration for which the contract should be paused. + /// + /// @return success A boolean indicating whether the call to `pauseFor` was successful and the contract is paused. + /// @return lowLevelError The low-level error message, if any, encountered during the call to `pauseFor`. function callPauseFor( ISealable sealable, uint256 sealDuration @@ -18,6 +29,15 @@ library SealableCalls { } } + /// @dev Calls the `isPaused` function on a `Sealable` contract to check if the contract is currently paused. + /// If the call is successful, it returns `true` indicating that the contract is paused, along with a low-level error message if any. + /// If the call fails, it returns `false` and the low-level error message encountered during the call. + /// + /// @param sealable The `Sealable` contract to call the `isPaused` function on. + /// + /// @return success A boolean indicating whether the call to `isPaused` was successful. + /// @return lowLevelError The low-level error message, if any, encountered during the call to `isPaused`. + /// @return isPaused A boolean indicating whether the contract is currently paused. function callIsPaused(ISealable sealable) internal view @@ -32,6 +52,14 @@ library SealableCalls { } } + /// @dev Calls the `resume` function on a `Sealable` contract to resume the contract's functionality. + /// If the call is successful and the contract is resumed, it returns `true` and a low-level error message, if any. + /// If the call fails, it returns `false` and the low-level error message encountered during the call. + /// + /// @param sealable The `Sealable` contract to call the `resume` function on. + /// + /// @return success A boolean indicating whether the call to `resume` was successful and the contract is resumed. + /// @return lowLevelError The low-level error message, if any, encountered during the call to `resume`. function callResume(ISealable sealable) internal returns (bool success, bytes memory lowLevelError) { try sealable.resume() { (bool isPausedCallSuccess, bytes memory isPausedLowLevelError, bool isPaused) = callIsPaused(sealable); diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol new file mode 100644 index 00000000..137a089c --- /dev/null +++ b/contracts/libraries/Tiebreaker.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Duration.sol"; + +import {ISealable} from "../interfaces/ISealable.sol"; + +import {SealableCalls} from "./SealableCalls.sol"; +import {State as DualGovernanceState} from "./DualGovernanceStateMachine.sol"; + +library Tiebreaker { + using SealableCalls for ISealable; + using EnumerableSet for EnumerableSet.AddressSet; + + error TiebreakDisallowed(); + error InvalidSealable(address value); + error InvalidTiebreakerCommittee(address value); + error InvalidTiebreakerActivationTimeout(Duration value); + error SealableWithdrawalBlockersLimitReached(); + + event SealableWithdrawalBlockerAdded(address sealable); + event SealableWithdrawalBlockerRemoved(address sealable); + event TiebreakerCommitteeSet(address newTiebreakerCommittee); + event TiebreakerActivationTimeoutSet(Duration newTiebreakerActivationTimeout); + + struct Context { + /// @dev slot0 [0..159] + address tiebreakerCommittee; + /// @dev slot0 [160..191] + Duration tiebreakerActivationTimeout; + /// @dev slot1 [0..255] + EnumerableSet.AddressSet sealableWithdrawalBlockers; + } + + // --- + // Setup functionality + // --- + + function addSealableWithdrawalBlocker( + Context storage self, + address sealableWithdrawalBlocker, + uint256 maxSealableWithdrawalBlockersCount + ) internal { + uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); + if (sealableWithdrawalBlockersCount == maxSealableWithdrawalBlockersCount) { + revert SealableWithdrawalBlockersLimitReached(); + } + + (bool isCallSucceed, /* lowLevelError */, /* isPaused */ ) = ISealable(sealableWithdrawalBlocker).callIsPaused(); + if (!isCallSucceed) { + revert InvalidSealable(sealableWithdrawalBlocker); + } + + bool isSuccessfullyAdded = self.sealableWithdrawalBlockers.add(sealableWithdrawalBlocker); + if (isSuccessfullyAdded) { + emit SealableWithdrawalBlockerAdded(sealableWithdrawalBlocker); + } + } + + function removeSealableWithdrawalBlocker(Context storage self, address sealableWithdrawalBlocker) internal { + bool isSuccessfullyRemoved = self.sealableWithdrawalBlockers.remove(sealableWithdrawalBlocker); + if (isSuccessfullyRemoved) { + emit SealableWithdrawalBlockerRemoved(sealableWithdrawalBlocker); + } + } + + function setTiebreakerCommittee(Context storage self, address newTiebreakerCommittee) internal { + if (newTiebreakerCommittee == address(0)) { + revert InvalidTiebreakerCommittee(newTiebreakerCommittee); + } + if (self.tiebreakerCommittee == newTiebreakerCommittee) { + return; + } + self.tiebreakerCommittee = newTiebreakerCommittee; + emit TiebreakerCommitteeSet(newTiebreakerCommittee); + } + + function setTiebreakerActivationTimeout( + Context storage self, + Duration minTiebreakerActivationTimeout, + Duration newTiebreakerActivationTimeout, + Duration maxTiebreakerActivationTimeout + ) internal { + if ( + newTiebreakerActivationTimeout < minTiebreakerActivationTimeout + || newTiebreakerActivationTimeout > maxTiebreakerActivationTimeout + ) { + revert InvalidTiebreakerActivationTimeout(newTiebreakerActivationTimeout); + } + + if (self.tiebreakerActivationTimeout == newTiebreakerActivationTimeout) { + return; + } + self.tiebreakerActivationTimeout = newTiebreakerActivationTimeout; + emit TiebreakerActivationTimeoutSet(newTiebreakerActivationTimeout); + } + + // --- + // Checks + // --- + + function checkCallerIsTiebreakerCommittee(Context storage self) internal view { + if (msg.sender != self.tiebreakerCommittee) { + revert InvalidTiebreakerCommittee(msg.sender); + } + } + + function checkTie( + Context storage self, + DualGovernanceState state, + Timestamp normalOrVetoCooldownExitedAt + ) internal view { + if (!isTie(self, state, normalOrVetoCooldownExitedAt)) { + revert TiebreakDisallowed(); + } + } + + // --- + // Getters + // --- + + function isTie( + Context storage self, + DualGovernanceState state, + Timestamp normalOrVetoCooldownExitedAt + ) internal view returns (bool) { + if (state == DualGovernanceState.Normal || state == DualGovernanceState.VetoCooldown) return false; + + // when the governance is locked for long period of time + if (Timestamps.now() >= self.tiebreakerActivationTimeout.addTo(normalOrVetoCooldownExitedAt)) { + return true; + } + + return state == DualGovernanceState.RageQuit && isSomeSealableWithdrawalBlockerPaused(self); + } + + function isSomeSealableWithdrawalBlockerPaused(Context storage self) internal view returns (bool) { + uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); + for (uint256 i = 0; i < sealableWithdrawalBlockersCount; ++i) { + (bool isCallSucceed, /* lowLevelError */, bool isPaused) = + ISealable(self.sealableWithdrawalBlockers.at(i)).callIsPaused(); + + // in normal condition this call must never fail, so if some sealable withdrawal blocker + // started behave unexpectedly tiebreaker action may be the last hope for the protocol saving + if (isPaused || !isCallSucceed) return true; + } + return false; + } + + function getTiebreakerInfo(Context storage self) + internal + view + returns ( + address tiebreakerCommittee, + Duration tiebreakerActivationTimeout, + address[] memory sealableWithdrawalBlockers + ) + { + tiebreakerCommittee = self.tiebreakerCommittee; + tiebreakerActivationTimeout = self.tiebreakerActivationTimeout; + + uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); + sealableWithdrawalBlockers = new address[](sealableWithdrawalBlockersCount); + + for (uint256 i = 0; i < sealableWithdrawalBlockersCount; ++i) { + sealableWithdrawalBlockers[i] = self.sealableWithdrawalBlockers.at(i); + } + } +} diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol deleted file mode 100644 index ffdb5665..00000000 --- a/contracts/libraries/TiebreakerProtection.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -interface IResealManger { - function resume(address sealable) external; -} - -library TiebreakerProtection { - struct Tiebreaker { - address tiebreaker; - IResealManger resealManager; - } - - event TiebreakerSet(address tiebreakCommittee, address resealManager); - event SealableResumed(address sealable); - - error ProposalNotExecutable(uint256 proposalId); - error NotTiebreaker(address account, address tiebreakCommittee); - error TieBreakerAddressIsSame(); - - function resumeSealable(Tiebreaker storage self, address sealable) internal { - self.resealManager.resume(sealable); - emit SealableResumed(sealable); - } - - function setTiebreaker(Tiebreaker storage self, address tiebreaker, address resealManager) internal { - if (self.tiebreaker == tiebreaker) { - revert TieBreakerAddressIsSame(); - } - - self.tiebreaker = tiebreaker; - self.resealManager = IResealManger(resealManager); - emit TiebreakerSet(tiebreaker, resealManager); - } - - function checkTiebreakerCommittee(Tiebreaker storage self, address account) internal view { - if (account != self.tiebreaker) { - revert NotTiebreaker(account, self.tiebreaker); - } - } -} diff --git a/contracts/libraries/TimelockState.sol b/contracts/libraries/TimelockState.sol new file mode 100644 index 00000000..eda71827 --- /dev/null +++ b/contracts/libraries/TimelockState.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; + +library TimelockState { + error CallerIsNotGovernance(address caller); + error InvalidGovernance(address value); + error InvalidAfterSubmitDelay(Duration value); + error InvalidAfterScheduleDelay(Duration value); + + event GovernanceSet(address newGovernance); + event AdminExecutorSet(address newAdminExecutor); + event AfterSubmitDelaySet(Duration newAfterSubmitDelay); + event AfterScheduleDelaySet(Duration newAfterScheduleDelay); + + struct Context { + /// @dev slot0 [0..159] + address governance; + /// @dev slot0 [160..191] + Duration afterSubmitDelay; + /// @dev slot0 [192..224] + Duration afterScheduleDelay; + } + + function setGovernance(Context storage self, address newGovernance) internal { + if (newGovernance == address(0)) { + revert InvalidGovernance(newGovernance); + } + if (self.governance == newGovernance) { + return; + } + self.governance = newGovernance; + emit GovernanceSet(newGovernance); + } + + function getAfterSubmitDelay(Context storage self) internal view returns (Duration) { + return self.afterSubmitDelay; + } + + function getAfterScheduleDelay(Context storage self) internal view returns (Duration) { + return self.afterScheduleDelay; + } + + function setAfterSubmitDelay( + Context storage self, + Duration newAfterSubmitDelay, + Duration maxAfterSubmitDelay + ) internal { + if (newAfterSubmitDelay > maxAfterSubmitDelay) { + revert InvalidAfterScheduleDelay(newAfterSubmitDelay); + } + if (self.afterSubmitDelay == newAfterSubmitDelay) { + return; + } + self.afterSubmitDelay = newAfterSubmitDelay; + emit AfterSubmitDelaySet(newAfterSubmitDelay); + } + + function setAfterScheduleDelay( + Context storage self, + Duration newAfterScheduleDelay, + Duration maxAfterScheduleDelay + ) internal { + if (newAfterScheduleDelay > maxAfterScheduleDelay) { + revert InvalidAfterScheduleDelay(newAfterScheduleDelay); + } + if (self.afterScheduleDelay == newAfterScheduleDelay) { + return; + } + self.afterScheduleDelay = newAfterScheduleDelay; + emit AfterScheduleDelaySet(newAfterScheduleDelay); + } + + function checkCallerIsGovernance(Context storage self) internal view { + if (self.governance != msg.sender) { + revert CallerIsNotGovernance(msg.sender); + } + } +} diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol index 9f882b01..59c2c6ab 100644 --- a/contracts/libraries/WithdrawalBatchesQueue.sol +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -4,87 +4,116 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {ArrayUtils} from "../utils/arrays.sol"; -import {SequentialBatch, SequentialBatches} from "../types/SequentialBatches.sol"; - -enum Status { - // The default status of the WithdrawalsBatchesQueue. In the closed state the only action allowed - // to be called is open(), which transfers it into Opened state. - Empty, - // In the Opened state WithdrawalsBatchesQueue allows to add batches into the queue +/// @notice The state of the WithdrawalBatchesQueue +/// @param Empty The initial (uninitialized) state of the WithdrawalBatchesQueue +/// @param Opened In this state, the WithdrawalBatchesQueue allows the addition of new batches of unstETH ids +/// @param Closed The terminal state of the queue. In this state, the addition of new batches is forbidden +enum State { + Absent, Opened, - // When the WithdrawalsBatchesQueue enters Filled queue - it's not allowed to add batches and - // only allowed to mark batches claimed Closed } -struct QueueIndex { - uint32 batchIndex; - uint16 valueIndex; -} - +/// @title WithdrawalsBatchesQueue +/// @dev A library for managing a queue of withdrawal batches. library WithdrawalsBatchesQueue { - using SafeCast for uint256; + // --- + // Errors + // --- - struct State { - Status status; - QueueIndex lastClaimedUnstETHIdIndex; - uint48 totalUnstETHCount; - uint48 totalUnstETHClaimed; - SequentialBatch[] batches; - } + error EmptyBatch(); + error InvalidUnstETHIdsSequence(); + error NotAllBatchesClaimed(uint256 total, uint256 claimed); + error InvalidWithdrawalsBatchesQueueState(State actual); + error WithdrawalBatchesQueueIsInAbsentState(); + error WithdrawalBatchesQueueIsNotInOpenedState(); + error WithdrawalBatchesQueueIsNotInAbsentState(); + // --- + // Events + // --- + + event WithdrawalBatchesQueueClosed(); event UnstETHIdsAdded(uint256[] unstETHIds); event UnstETHIdsClaimed(uint256[] unstETHIds); + event WithdrawalBatchesQueueOpened(uint256 boundaryUnstETHId); - error InvalidWithdrawalsBatchesQueueStatus(Status status); + // --- + // Data types + // --- - function calcRequestAmounts( - uint256 minRequestAmount, - uint256 requestAmount, - uint256 amount - ) internal pure returns (uint256[] memory requestAmounts) { - uint256 requestsCount = amount / requestAmount; - // last request amount will be equal to zero when it's multiple requestAmount - // when it's in the range [0, minRequestAmount) - it will not be included in the result - uint256 lastRequestAmount = amount - requestsCount * requestAmount; - if (lastRequestAmount >= minRequestAmount) { - requestsCount += 1; - } - requestAmounts = ArrayUtils.seed(requestsCount, requestAmount); - if (lastRequestAmount >= minRequestAmount) { - requestAmounts[requestsCount - 1] = lastRequestAmount; - } + /// @notice Represents a sequential batch of unstETH ids + /// @param firstUnstETHId The id of the first unstETH in the batch + /// @param lastUnstETHId The id of the last unstETH in the batch + /// @dev If the batch contains only one item, firstUnstETHId == lastUnstETHId + struct SequentialBatch { + /// @dev slot0: [0..255] + uint256 firstUnstETHId; + /// @dev slot1: [0..255] + uint256 lastUnstETHId; } - function open(State storage self) internal { - _checkStatus(self, Status.Empty); - // insert empty batch as a stub for first item - self.batches.push(SequentialBatches.create({seed: 0, count: 1})); - self.status = Status.Opened; + /// @notice Holds the meta-information about the queue and the claiming process + /// @param state The current state of the WithdrawalQueue + /// @param lastClaimedBatchIndex The index of the batch containing the id of the last claimed unstETH NFT + /// @param lastClaimedUnstETHIdIndex The index of the last claimed unstETH id in the batch with index `lastClaimedBatchIndex` + /// @param totalUnstETHCount The total number of unstETH ids in the batches + /// @param totalUnstETHClaimed The total number of unstETH ids that have been marked as claimed + struct QueueInfo { + /// @dev slot0: [0..7] + State state; + /// @dev slot0: [8..63] + uint56 lastClaimedBatchIndex; + /// @dev slot0: [64..127] + uint64 lastClaimedUnstETHIdIndex; + /// @dev slot0: [128..191] + uint64 totalUnstETHIdsCount; + /// @dev slot0: [192..255] + uint64 totalUnstETHIdsClaimed; } - function close(State storage self) internal { - _checkStatus(self, Status.Opened); - self.status = Status.Closed; + /// @notice The context of the WithdrawalsBatchesQueue library + /// @param info The meta info of the queue + /// @param batches The list of the withdrawal batches + struct Context { + /// @dev slot0: [0..255] + QueueInfo info; + /// @dev slot1: [0..255] - array length + 2 slots for each item + SequentialBatch[] batches; } - function isClosed(State storage self) internal view returns (bool) { - return self.status == Status.Closed; - } + // --- + // Main Functionality + // --- - function isAllUnstETHClaimed(State storage self) internal view returns (bool) { - return self.totalUnstETHClaimed == self.totalUnstETHCount; - } + /// @notice Opens the WithdrawalsBatchesQueue, allowing batches to be added. Adds an empty batch as a stub. + /// @param self The context of the WithdrawalsBatchesQueue + /// @param boundaryUnstETHId The id of the unstETH NFT which is used as the boundary value for the withdrawal queue. + /// `boundaryUnstETHId` value is used as a lower bound for the adding unstETH ids + function open(Context storage self, uint256 boundaryUnstETHId) internal { + if (self.info.state != State.Absent) { + revert WithdrawalBatchesQueueIsNotInAbsentState(); + } + + self.info.state = State.Opened; - function checkOpened(State storage self) internal view { - _checkStatus(self, Status.Opened); + /// @dev add the boundary UnstETH element into the queue, which will be used as the last unstETH id + /// when the queue is empty. This element doesn't used during the claiming of the batches created + /// via addUnstETHIds() method and always allocates single batch + self.batches.push(SequentialBatch({firstUnstETHId: boundaryUnstETHId, lastUnstETHId: boundaryUnstETHId})); + emit WithdrawalBatchesQueueOpened(boundaryUnstETHId); } - function add(State storage self, uint256[] memory unstETHIds) internal { + /// @dev Adds new unstETHIds to the WithdrawalsBatchesQueue. + /// @param self The WithdrawalsBatchesQueue context. + /// @param unstETHIds The array of unstETH that have been added. + function addUnstETHIds(Context storage self, uint256[] memory unstETHIds) internal { + _checkInOpenedState(self); + uint256 unstETHIdsCount = unstETHIds.length; + if (unstETHIdsCount == 0) { - return; + revert EmptyBatch(); } // before creating the batch, assert that the unstETHIds is sequential @@ -92,60 +121,186 @@ library WithdrawalsBatchesQueue { assert(unstETHIds[i + 1] == unstETHIds[i] + 1); } + uint256 firstAddingUnstETHId = unstETHIds[0]; + uint256 lastAddingUnstETHId = unstETHIds[unstETHIdsCount - 1]; + uint256 lastBatchIndex = self.batches.length - 1; - SequentialBatch lastWithdrawalsBatch = self.batches[lastBatchIndex]; - SequentialBatch newWithdrawalsBatch = SequentialBatches.create({seed: unstETHIds[0], count: unstETHIdsCount}); + SequentialBatch memory lastWithdrawalsBatch = self.batches[lastBatchIndex]; - if (SequentialBatches.canMerge(lastWithdrawalsBatch, newWithdrawalsBatch)) { - self.batches[lastBatchIndex] = SequentialBatches.merge(lastWithdrawalsBatch, newWithdrawalsBatch); + if (firstAddingUnstETHId <= lastWithdrawalsBatch.lastUnstETHId) { + revert InvalidUnstETHIdsSequence(); + } else if (firstAddingUnstETHId == lastWithdrawalsBatch.lastUnstETHId + 1 && lastBatchIndex != 0) { + /// @dev this option is allowed only when used not the seed batch id + self.batches[lastBatchIndex].lastUnstETHId = lastAddingUnstETHId; } else { - self.batches.push(newWithdrawalsBatch); + self.batches.push( + SequentialBatch({firstUnstETHId: firstAddingUnstETHId, lastUnstETHId: lastAddingUnstETHId}) + ); } - self.totalUnstETHCount += newWithdrawalsBatch.size().toUint48(); + /// @dev theoretically here may happen math overflow, when the total unstETH count exceeds the capacity of + /// the uint64 type, BUT in reality it's not possible if the system works properly + self.info.totalUnstETHIdsCount += SafeCast.toUint64(unstETHIdsCount); emit UnstETHIdsAdded(unstETHIds); } + /// @dev Forms the next batch of unstETHs for claiming. + /// @param self The WithdrawalsBatchesQueue context. + /// @param maxUnstETHIdsCount The maximum number of unstETHIds to be claimed. + /// @return unstETHIds The array of claimed unstETHIds. function claimNextBatch( - State storage self, + Context storage self, uint256 maxUnstETHIdsCount ) internal returns (uint256[] memory unstETHIds) { - (unstETHIds, self.lastClaimedUnstETHIdIndex) = _getNextClaimableUnstETHIds(self, maxUnstETHIdsCount); - self.totalUnstETHClaimed += unstETHIds.length.toUint48(); + if (self.info.totalUnstETHIdsClaimed == self.info.totalUnstETHIdsCount) { + revert EmptyBatch(); + } + (unstETHIds, self.info) = _getNextClaimableUnstETHIds(self, maxUnstETHIdsCount); emit UnstETHIdsClaimed(unstETHIds); } + /// @notice Closes the WithdrawalsBatchesQueue, preventing further batch additions + /// @param self The context of the WithdrawalsBatchesQueue + function close(Context storage self) internal { + _checkInOpenedState(self); + self.info.state = State.Closed; + emit WithdrawalBatchesQueueClosed(); + } + + // --- + // Getters + // --- + + /// @dev Calculates the request amounts based on the given parameters. + /// @param minRequestAmount The minimum request amount. + /// @param maxRequestAmount The maximum request amount. + /// @param remainingAmount The remaining amount to be requested. + /// @return requestAmounts An array of request amounts. + function calcRequestAmounts( + uint256 minRequestAmount, + uint256 maxRequestAmount, + uint256 remainingAmount + ) internal pure returns (uint256[] memory requestAmounts) { + uint256 requestsCount = remainingAmount / maxRequestAmount; + // last request amount will be equal to zero when it's multiple requestAmount + // when it's in the range [0, minRequestAmount) - it will not be included in the result + uint256 lastRequestAmount = remainingAmount - requestsCount * maxRequestAmount; + if (lastRequestAmount >= minRequestAmount) { + requestsCount += 1; + } + + requestAmounts = new uint256[](requestsCount); + for (uint256 i = 0; i < requestsCount; ++i) { + requestAmounts[i] = maxRequestAmount; + } + + if (lastRequestAmount >= minRequestAmount) { + requestAmounts[requestsCount - 1] = lastRequestAmount; + } + } + + /// @dev Retrieves the next batch of unstETHIds that can be claimed from the WithdrawalsBatchesQueue. + /// @param self The WithdrawalsBatchesQueue context. + /// @param limit The maximum number of unstETHIds to be retrieved. + /// @return unstETHIds The array of next claimable unstETHIds. function getNextWithdrawalsBatches( - State storage self, + Context storage self, uint256 limit ) internal view returns (uint256[] memory unstETHIds) { (unstETHIds,) = _getNextClaimableUnstETHIds(self, limit); } + /// @dev Retrieves the id of the boundary unstETH id. Reverts when the queue is in Absent context. + /// @param self The WithdrawalsBatchesQueue context. + /// @return boundaryUnstETHId The id of the boundary unstETH. + function getBoundaryUnstETHId(Context storage self) internal view returns (uint256) { + _checkNotInAbsentState(self); + return self.batches[0].firstUnstETHId; + } + + /// @dev Retrieves the total count of the unstETH ids added in the queue. + /// @param self The WithdrawalsBatchesQueue context. + /// @return totalUnstETHIdsCount The total count of the unstETH ids. + function getTotalUnstETHIdsCount(Context storage self) internal view returns (uint256) { + return self.info.totalUnstETHIdsCount; + } + + /// @dev Retrieves the total unclaimed unstETH ids count. + /// @param self The WithdrawalsBatchesQueue context. + /// @return totalUnclaimedUnstETHIdsCount The total count of unclaimed unstETH ids + function getTotalUnclaimedUnstETHIdsCount(Context storage self) internal view returns (uint256) { + return self.info.totalUnstETHIdsCount - self.info.totalUnstETHIdsClaimed; + } + + /// @dev Returns the id of the last claimed UnstETH. When the queue is empty, returns 0 + /// @param self The WithdrawalsBatchesQueue context. + /// @return lastClaimedUnstETHId The id of the lastClaimedUnstETHId or 0 when the queue is empty + function getLastClaimedOrBoundaryUnstETHId(Context storage self) internal view returns (uint256) { + _checkNotInAbsentState(self); + QueueInfo memory info = self.info; + return self.batches[info.lastClaimedBatchIndex].firstUnstETHId + info.lastClaimedUnstETHIdIndex; + } + + /// @dev Returns if all unstETH ids in the queue have been claimed + /// @param self The WithdrawalsBatchesQueue context. + /// @return isAllBatchesClaimed Equals true if all unstETHs have been claimed, false otherwise. + function isAllBatchesClaimed(Context storage self) internal view returns (bool) { + QueueInfo memory info = self.info; + return info.totalUnstETHIdsClaimed == info.totalUnstETHIdsCount; + } + + /// @dev Checks if the WithdrawalsBatchesQueue is closed. + /// @param self The WithdrawalsBatchesQueue context. + /// @return isClosed_ True if the WithdrawalsBatchesQueue is closed, false otherwise. + function isClosed(Context storage self) internal view returns (bool isClosed_) { + isClosed_ = self.info.state == State.Closed; + } + + // --- + // Helper Methods + // --- + + /// @dev Retrieves the next claimable unstETHIds from the WithdrawalsBatchesQueue. + /// @param self The WithdrawalsBatchesQueue context. + /// @param maxUnstETHIdsCount The maximum number of unstETHIds to be retrieved. + /// @return unstETHIds The array of next claimable unstETHIds. + /// @return info The updated QueueIndex of the last claimed unstETHId. function _getNextClaimableUnstETHIds( - State storage self, + Context storage self, uint256 maxUnstETHIdsCount - ) private view returns (uint256[] memory unstETHIds, QueueIndex memory lastClaimedUnstETHIdIndex) { - uint256 unstETHIdsCount = Math.min(self.totalUnstETHCount - self.totalUnstETHClaimed, maxUnstETHIdsCount); + ) private view returns (uint256[] memory unstETHIds, QueueInfo memory info) { + info = self.info; + uint256 unstETHIdsCount = Math.min(info.totalUnstETHIdsCount - info.totalUnstETHIdsClaimed, maxUnstETHIdsCount); unstETHIds = new uint256[](unstETHIdsCount); - lastClaimedUnstETHIdIndex = self.lastClaimedUnstETHIdIndex; - SequentialBatch currentBatch = self.batches[lastClaimedUnstETHIdIndex.batchIndex]; + SequentialBatch memory currentBatch = self.batches[info.lastClaimedBatchIndex]; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - lastClaimedUnstETHIdIndex.valueIndex += 1; - if (currentBatch.size() == lastClaimedUnstETHIdIndex.valueIndex) { - lastClaimedUnstETHIdIndex.batchIndex += 1; - lastClaimedUnstETHIdIndex.valueIndex = 0; - currentBatch = self.batches[lastClaimedUnstETHIdIndex.batchIndex]; + info.lastClaimedUnstETHIdIndex += 1; + uint256 unstETHIdsCountInTheBatch = currentBatch.lastUnstETHId - currentBatch.firstUnstETHId + 1; + if (unstETHIdsCountInTheBatch == info.lastClaimedUnstETHIdIndex) { + info.lastClaimedBatchIndex += 1; + info.lastClaimedUnstETHIdIndex = 0; + currentBatch = self.batches[info.lastClaimedBatchIndex]; } - unstETHIds[i] = currentBatch.valueAt(lastClaimedUnstETHIdIndex.valueIndex); + unstETHIds[i] = currentBatch.firstUnstETHId + info.lastClaimedUnstETHIdIndex; + } + info.totalUnstETHIdsClaimed += SafeCast.toUint64(unstETHIdsCount); + } + + /// @dev Checks the queue not in the Absent state. + /// @param self The WithdrawalsBatchesQueue context. + function _checkNotInAbsentState(Context storage self) private view { + if (self.info.state == State.Absent) { + revert WithdrawalBatchesQueueIsInAbsentState(); } } - function _checkStatus(State storage self, Status expectedStatus) private view { - if (self.status != expectedStatus) { - revert InvalidWithdrawalsBatchesQueueStatus(self.status); + /// @dev Checks the queue in the Opened state. + /// @param self The WithdrawalsBatchesQueue context. + function _checkInOpenedState(Context storage self) private view { + if (self.info.state != State.Opened) { + revert WithdrawalBatchesQueueIsNotInOpenedState(); } } } diff --git a/contracts/types/ETHValue.sol b/contracts/types/ETHValue.sol index 44160dfd..9a85a1b9 100644 --- a/contracts/types/ETHValue.sol +++ b/contracts/types/ETHValue.sol @@ -24,7 +24,7 @@ function minus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { if (v1 < v2) { revert ETHValueUnderflow(); } - return ETHValues.from(ETHValue.unwrap(v1) + ETHValue.unwrap(v2)); + return ETHValues.from(ETHValue.unwrap(v1) - ETHValue.unwrap(v2)); } function lt(ETHValue v1, ETHValue v2) pure returns (bool) { @@ -56,4 +56,8 @@ library ETHValues { } return ETHValue.wrap(uint128(value)); } + + function fromAddressBalance(address account) internal view returns (ETHValue) { + return from(account.balance); + } } diff --git a/contracts/types/IndexOneBased.sol b/contracts/types/IndexOneBased.sol index 1bde6542..b080af75 100644 --- a/contracts/types/IndexOneBased.sol +++ b/contracts/types/IndexOneBased.sol @@ -6,14 +6,21 @@ type IndexOneBased is uint32; error IndexOneBasedOverflow(); error IndexOneBasedUnderflow(); -using {neq as !=} for IndexOneBased global; -using {value} for IndexOneBased global; +using {neq as !=, isEmpty, isNotEmpty, toZeroBasedValue} for IndexOneBased global; function neq(IndexOneBased i1, IndexOneBased i2) pure returns (bool) { return IndexOneBased.unwrap(i1) != IndexOneBased.unwrap(i2); } -function value(IndexOneBased index) pure returns (uint256) { +function isEmpty(IndexOneBased index) pure returns (bool) { + return IndexOneBased.unwrap(index) == 0; +} + +function isNotEmpty(IndexOneBased index) pure returns (bool) { + return IndexOneBased.unwrap(index) != 0; +} + +function toZeroBasedValue(IndexOneBased index) pure returns (uint256) { if (IndexOneBased.unwrap(index) == 0) { revert IndexOneBasedUnderflow(); } @@ -23,10 +30,10 @@ function value(IndexOneBased index) pure returns (uint256) { } library IndicesOneBased { - function from(uint256 value) internal pure returns (IndexOneBased) { - if (value > type(uint32).max) { + function fromOneBasedValue(uint256 oneBasedIndexValue) internal pure returns (IndexOneBased) { + if (oneBasedIndexValue > type(uint32).max) { revert IndexOneBasedOverflow(); } - return IndexOneBased.wrap(uint32(value)); + return IndexOneBased.wrap(uint32(oneBasedIndexValue)); } } diff --git a/contracts/types/PercentD16.sol b/contracts/types/PercentD16.sol new file mode 100644 index 00000000..c865cb96 --- /dev/null +++ b/contracts/types/PercentD16.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +type PercentD16 is uint256; + +uint256 constant HUNDRED_PERCENTS_UINT256 = 100 * 10 ** 16; + +error Overflow(); + +using {lt as <, lte as <=, gte as >=, gt as >, minus as -, plus as +} for PercentD16 global; + +function lt(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) < PercentD16.unwrap(b); +} + +function lte(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) <= PercentD16.unwrap(b); +} + +function gt(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) > PercentD16.unwrap(b); +} + +function gte(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) >= PercentD16.unwrap(b); +} + +function minus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { + if (b > a) { + revert Overflow(); + } + return PercentD16.wrap(PercentD16.unwrap(a) - PercentD16.unwrap(b)); +} + +function plus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { + return PercentD16.wrap(PercentD16.unwrap(a) + PercentD16.unwrap(b)); +} + +library PercentsD16 { + function fromBasisPoints(uint256 bpValue) internal pure returns (PercentD16) { + return PercentD16.wrap(HUNDRED_PERCENTS_UINT256 * bpValue / 100_00); + } + + function fromFraction(uint256 numerator, uint256 denominator) internal pure returns (PercentD16) { + return PercentD16.wrap(HUNDRED_PERCENTS_UINT256 * numerator / denominator); + } +} diff --git a/contracts/types/SequentialBatches.sol b/contracts/types/SequentialBatches.sol deleted file mode 100644 index 35edef11..00000000 --- a/contracts/types/SequentialBatches.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -uint256 constant BATCH_SIZE_LENGTH = 16; -uint256 constant BATCH_SIZE_MASK = 2 ** BATCH_SIZE_LENGTH - 1; - -uint256 constant MAX_BATCH_SIZE = BATCH_SIZE_MASK; -uint256 constant MAX_BATCH_VALUE = 2 ** (256 - BATCH_SIZE_LENGTH) - 1; - -// Stores the info about the withdrawals batch encoded as single uint256 -// The 230 MST bits stores the id of the UnstETH id -// the 16 LST bits stores the size of the batch (max size is 2 ^ 16 - 1= 65535) -type SequentialBatch is uint256; - -error BatchValueOverflow(); -error InvalidBatchSize(uint256 size); -error IndexOutOfBounds(uint256 index); - -using {size} for SequentialBatch global; -using {last} for SequentialBatch global; -using {first} for SequentialBatch global; -using {valueAt} for SequentialBatch global; -using {capacity} for SequentialBatch global; - -function capacity(SequentialBatch) pure returns (uint256) { - return MAX_BATCH_SIZE; -} - -function size(SequentialBatch batch) pure returns (uint256) { - unchecked { - return SequentialBatch.unwrap(batch) & BATCH_SIZE_MASK; - } -} - -function first(SequentialBatch batch) pure returns (uint256) { - unchecked { - return SequentialBatch.unwrap(batch) >> BATCH_SIZE_LENGTH; - } -} - -function last(SequentialBatch batch) pure returns (uint256) { - unchecked { - return batch.first() + batch.size() - 1; - } -} - -function valueAt(SequentialBatch batch, uint256 index) pure returns (uint256) { - if (index >= batch.size()) { - revert IndexOutOfBounds(index); - } - unchecked { - return batch.first() + index; - } -} - -library SequentialBatches { - function create(uint256 seed, uint256 count) internal pure returns (SequentialBatch) { - if (seed > MAX_BATCH_VALUE) { - revert BatchValueOverflow(); - } - if (count == 0 || count > MAX_BATCH_SIZE) { - revert InvalidBatchSize(count); - } - unchecked { - return SequentialBatch.wrap(seed << BATCH_SIZE_LENGTH | count); - } - } - - function canMerge(SequentialBatch b1, SequentialBatch b2) internal pure returns (bool) { - unchecked { - return b1.last() == b2.first() && b1.capacity() - b1.size() > 0; - } - } - - function merge(SequentialBatch b1, SequentialBatch b2) internal pure returns (SequentialBatch b3) { - return create(b1.first(), b1.size() + b2.size()); - } -} diff --git a/contracts/types/Timestamp.sol b/contracts/types/Timestamp.sol index 87d280a2..f153f71b 100644 --- a/contracts/types/Timestamp.sol +++ b/contracts/types/Timestamp.sol @@ -71,6 +71,10 @@ library Timestamps { Timestamp internal constant MIN = ZERO; Timestamp internal constant MAX = Timestamp.wrap(uint40(MAX_TIMESTAMP_VALUE)); + function max(Timestamp t1, Timestamp t2) internal pure returns (Timestamp) { + return t1 > t2 ? t1 : t2; + } + function now() internal view returns (Timestamp res) { res = Timestamp.wrap(uint40(block.timestamp)); } diff --git a/docs/plan-b.md b/docs/plan-b.md new file mode 100644 index 00000000..1cd93be9 --- /dev/null +++ b/docs/plan-b.md @@ -0,0 +1,434 @@ +# Timelocked Governance specification + +Timelocked Governance (TG) is a governance subsystem positioned between the Lido DAO, represented by the admin voting system (defaulting to Aragon's Voting), and the protocol contracts it manages. The TG subsystem helps protect users from malicious DAO proposals by allowing the **Emergency Activation Committee** to activate a long-lasting timelock on these proposals. + +> Motivation: the upcoming Ethereum upgrade *Pectra* will introduce a new [withdrawal mechanism](https://eips.ethereum.org/EIPS/eip-7002) (EIP-7002), significantly affecting the operation of the Lido protocol. This enhancement will allow withdrawal queue contract to trigger withdrawals, introducing a new attack vector for the whole protocol. This poses a threat to stETH users, as governance capture (or malicious actions) could enable an upgrade to the withdrawal queue contract, resulting in the theft of user funds. Timelocked Governance in its turn provides security assurances through the implementation of guardians (emergency committees) that can halt malicious proposals and the implementation of the timelock to ensure users and committees have sufficient time to react to potential threats. + +## Navigation +* [System overview](#system-overview) +* [Proposal flow](#proposal-flow) +* [Proposal execution](#proposal-execution) +* [Common types](#common-types) +* [Contract: `TimelockedGovernance`](#contract-timelockedgovernance) +* [Contract: `EmergencyProtectedTimelock`](#contract-emergencyprotectedtimelock) +* [Contract: `Executor`](#contract-executor) +* [Contract: `Configuration`](#contract-configuration) +* [Contract: `ProposalsList`](#contract-proposalslist) +* [Contract: `HashConsensus`](#contract-hashconsensus) +* [Contract: `EmergencyActivationCommittee`](#contract-emergencyactivationcommittee) +* [Contract: `EmergencyExecutionCommittee`](#contract-emergencyexecutioncommittee) + +## System Overview + +image + +The system comprises the following primary contracts: +- **`TimelockedGovernance.sol`**: A singleton contract that serves as the interface for submitting and scheduling the execution of governance proposals. +- **[`EmergencyProtectedTimelock.sol`]**: A singleton contract responsible for storing submitted proposals and providing an interface for their execution. It offers protection against malicious proposals submitted by the DAO, implemented as a timelock on proposal execution. This protection is enforced through the cooperation of two emergency committees that can suspend proposal execution. +- [`EmergencyProtectedTimelock.sol`]() A singleton contract that stores submitted proposals and provides an execution interface. In addition, it implements an optional protection from a malicious proposals submitted by the DAO. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and suspend the execution of the proposals. +- **[`Executor.sol`]**: A contract instance responsible for executing calls resulting from governance proposals. All protocol permissions or roles protected by TG, as well as the authority to manage these roles/permissions, should be assigned exclusively to instance of this contract, rather than being assigned directly to the DAO voting system. +- **[`EmergencyActivationCommittee`]**: A contract with the authority to activate Emergency Mode. Activation requires a quorum from committee members. +- **[`EmergencyExecutionCommittee`]**: A contract that enables the execution of proposals during Emergency Mode by obtaining a quorum of committee members. + +## Proposal flow +image + +The general proposal flow is as follows: +1. **Proposal Submission**: The Lido DAO submits a proposal via the admin voting system. This involves a set of external calls (represented by an array of [`ExecutorCall`] structs) to be executed by the [Admin Executor], by calling the [`TimelockedGovernance.submitProposal()`] function. +2. **After Submit Delay**: This initiates a preconfigured `AfterSubmitDelay` timelock period. Depending on the configuration, this period may be set to 0. If set to 0, the submitted proposal can be scheduled for execution immediately by anyone using the `TimelockGovernance.scheduleProposal()` method. +3. **Optional Proposal Cancellation**: At any moment before the proposal is executed, the Lido DAO may cancel all pending proposals using the `TimelockedGovernance.cancelAllPendingProposals` method. +4. **Proposal Execution**: After the configured timelock has passed, the proposal may be executed, resulting in the proposal's calls being issued by the admin executor contract. + +## Proposal execution +image + +The proposal execution flow begins after the proposal is scheduled for execution and the `AfterScheduleDelay` has passed. + +If emergency protection is enabled on the `EmergencyProtectedTimelock` instance, an **emergency activation committee** has a one-off, time-limited right to activate an adversarial **emergency mode** if they detect a malicious proposal submitted by the Lido DAO. + +- Once the emergency mode is activated, the emergency activation committee is disabled, meaning it loses the ability to activate the emergency mode again. If the emergency activation committee doesn't activate the emergency mode within the **emergency protection duration** since the committee was configured, it gets automatically disabled as well. +- The emergency mode lasts up to the **emergency mode max duration** from the moment of its activation. While it's active, only the **emergency execution committee** has the right to execute scheduled proposals. This committee also has a one-off right to **disable the emergency mode**. +- If the emergency execution committee doesn't disable the emergency mode before the emergency mode max duration elapses, anyone can deactivate the emergency mode, allowing proposals to proceed and disabling the emergency committee. Once the emergency mode is disabled, all pending proposals will be marked as cancelled and cannot be executed. + +## Common types + +### Struct: ExecutorCall + +```solidity +struct ExecutorCall { + address target; + uint96 value; + bytes payload; +} +``` + +Encodes an external call from an executor contract to the `target` address with the specified `value` and the calldata being set to `payload`. + +## Contract: `TimelockedGovernance` + +The main entry point to the timelocked governance system, which provides an interface for submitting and canceling governance proposals in the `EmergencyProtectedTimelock` contract. This contract is a singleton, meaning that any TG deployment includes exactly one instance of this contract. + +### Function: `TimelockedGovernance.submitProposal` +```solidity +function submitProposal(ExecutorCall[] calls) + returns (uint256 proposalId) +``` + +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to register a new governance proposal composed of one or more external `calls` to be made by an admin executor contract. Initiates a timelock on scheduling the proposal for execution. + +See: [`EmergencyProtectedTimelock.submit`](#) +#### Returns +The id of the successfully registered proposal. +#### Preconditions +* The `msg.sender` MUST be the address of the admin voting system + +### Function: `TimelockedGovernance.scheduleProposal` +```solidity +function scheduleProposal(uint256 proposalId) external +``` +Instructs the [`EmergencyProtectedTimelock`](#) singleton instance to schedule the proposal with id `proposalId` for execution. + +See: [`EmergencyProtectedTimelock.schedule`](#) +#### Preconditions +- The proposal with the given id MUST be in the `Submitted` state. + +### Function: `TimelockedGovernance.executeProposal` +```solidity +function executeProposal(uint256 proposalId) external +``` +Instructs the [`EmergencyProtectedTimelock`](#) singleton instance to execute the proposal with id `proposalId`. + +See: [`EmergencyProtectedTimelock.execute`](#) +#### Preconditions +- The proposal with the given id MUST be in the `Scheduled` state. +### Function: `TimelockedGovernance.cancelAllPendingProposals` + +```solidity +function cancelAllPendingProposals() +``` + +Cancels all currently submitted and non-executed proposals. If a proposal was submitted but not scheduled, it becomes unschedulable. If a proposal was scheduled, it becomes unexecutable. + +See: [`EmergencyProtectedTimelock.cancelAllNonExecutedProposals`](#) +#### Preconditions +* MUST be called by an [admin voting system](#) + +## Contract: `EmergencyProtectedTimelock` +`EmergencyProtectedTimelock` is a singleton instance that stores and manages the lifecycle of proposals submitted by the DAO via the `TimelockedGovernance` contract. It can be configured with time-bound **Emergency Activation Committee** and **Emergency Execution Committee**, which act as safeguards against the execution of malicious proposals. + +For a proposal to be executed, the following steps have to be performed in order: +1. The proposal must be submitted using the `EmergencyProtectedTimelock.submit()` function. +2. The configured post-submit timelock (`Configuration.AFTER_SUBMIT_DELAY()`) must elapse. +3. The proposal must be scheduled using the `EmergencyProtectedTimelock.schedule()` function. +4. The configured emergency protection delay (`Configuration.AFTER_SCHEDULE_DELAY()`) must elapse (can be zero, see below). +5. The proposal must be executed using the `EmergencyProtectedTimelock.execute()` function. + +The contract only allows proposal submission and scheduling by the `governance` address. Normally, this address points to the [`TimelockedGovernance`](#Contract-TimelockedGovernancesol) singleton instance. Proposal execution is permissionless, unless Emergency Mode is activated. + +If the Emergency Committees are set up and active, the governance proposal undergoes a separate emergency protection delay between submission and scheduling. This additional timelock is implemented to protect against the execution of malicious proposals submitted by the DAO. If the Emergency Committees aren't set, the proposal flow remains the same, but the timelock duration is zero. + +If the Emergency Committees are set up and active, the governance proposal undergoes a separate emergency protection delay between submission and scheduling. This additional timelock is implemented to safeguard against the execution of malicious proposals submitted by the DAO. If the Emergency Committees aren't set, the proposal flow remains the same, but the timelock duration is zero. + +While active, the Emergency Activation Committee can enable Emergency Mode. This mode prohibits anyone but the Emergency Execution Committee from executing proposals. Once the **Emergency Duration** has ended, the Emergency Execution Committee or anyone else may disable the emergency mode, canceling all pending proposals. After the emergency mode is deactivated or the Emergency Period has elapsed, the Emergency Committees lose their power. +### Function: `EmergencyProtectedTimelock.submit` +```solidity +function submit(address executor, ExecutorCall[] calls) + returns (uint256 proposalId) +``` +Registers a new governance proposal composed of one or more external `calls` to be made by the `executor` contract. Initiates the `AfterSubmitDelay`. +#### Returns +The ID of the successfully registered proposal. +#### Preconditions +* MUST be called by the `governance` address. +### Function: `EmergencyProtectedTimelock.schedule` +```solidity +function schedule(uint256 proposalId) +``` +Schedules the submitted proposal for execution. Initiates the `AfterScheduleDelay`. +#### Preconditions + +* MUST be called by the `governance` address. +* The proposal MUST be in the `Submitted` state. +* The `AfterSubmitDelay` MUST already elapse since the moment the proposal was submitted. + +### Function: `EmergencyProtectedTimelock.execute` +```solidity +function execute(uint256 proposalId) +``` +Instructs the executor contract associated with the proposal to issue the proposal's calls. +#### Preconditions +* Emergency mode MUST NOT be active. +* The proposal MUST be in the `Scheduled` state. +* The `AfterScheduleDelay` MUST already elapse since the moment the proposal was scheduled. + +### Function: `EmergencyProtectedTimelock.cancelAllNonExecutedProposals` +```solidity +function cancelAllNonExecutedProposals() +``` +Cancels all non-executed proposals, making them permanently non-executable. +#### Preconditions +* MUST be called by the `governance` address. +### Function: `EmergencyProtectedTimelock.activateEmergencyMode` +```solidity +function activateEmergencyMode() +``` +Activates the Emergency Mode. +#### Preconditions +* MUST be called by the active Emergency Activation Committee address. +* The Emergency Mode MUST NOT be active. +### Function: `EmergencyProtectedTimelock.emergencyExecute` + +```solidity +function emergencyExecute(uint256 proposalId) +``` + +Executes the scheduled proposal, bypassing the post-schedule delay. +#### Preconditions +* MUST be called by the Emergency Execution Committee address. +* The Emergency Mode MUST be active. +### Function: `EmergencyProtectedTimelock.deactivateEmergencyMode` +```solidity +function deactivateEmergencyMode() +``` +Deactivates Emergency Mode, resets the Emergency Activation and Emergency Execution committees (setting their addresses to `0x00`), and cancels all unexecuted proposals. +#### Preconditions +* The Emergency Mode MUST be active. +* If the Emergency Mode was activated less than the `emergency mode max duration` ago, MUST be called by the [Admin Executor](#) address. +### Function: `EmergencyProtectedTimelock.emergencyReset` +```solidity +function emergencyReset() +``` +Resets the `governance` address to the `EMERGENCY_GOVERNANCE` value defined in the configuration, deactivates the Emergency Mode, resets the Emergency Activation and Emergency Execution Committees (setting their addresses to `0x00`), and cancels all unexecuted proposals. +#### Preconditions +* The Emergency Mode MUST be active. +* MUST be called by the Emergency Execution Committee address. + +### Admin functions +The contract includes functions for managing emergency protection configuration (`setEmergencyProtection`) and general system wiring (`transferExecutorOwnership`, `setGovernance`). These functions MUST be called by the [Admin Executor](#) address. + +## Contract: `Executor` +Executes calls resulting from governance proposals' execution. Every protocol permission or role protected by the TG, as well as the permission to manage these roles/permissions, should be assigned exclusively to instances of this contract. + +The timelocked governance setup is designed to use a single admin instance of the `Executor`, which is owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. + +### Function: `Executor.execute` +```solidity +function execute(address target, uint256 value, bytes payload) + payable returns (bytes result) +``` +Issues a external call to the `target` address with the `payload` calldata, optionally sending `value` wei ETH. + +Reverts if the call was unsuccessful. +#### Returns +The result of the call. +#### Preconditions +* MUST be called by the contract owner (which SHOULD be the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance). + +## Contract: `Configuration` +`Configuration` is the smart contract encompassing all the constants in the Timelocked Governance design & providing the interfaces for getting access to them. It implements interfaces `IAdminExecutorConfiguration`, `ITimelockConfiguration` covering for relevant "parameters domains". + +## Contract: `ProposalsList` +`ProposalsList` implements storage for list of `Proposal`s with public interface to access. + +### Function: `ProposalsList.getProposals` +```solidity +function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) +``` +Returns a list of `Proposal` structs, starting from the specified `offset` and bounded to the specified `limit`. + +### Function: `ProposalsList.getProposalAt` +```solidity +function getProposalAt(uint256 index) public view returns (Proposal memory) +``` +Returns the `Proposal` at the specified index. + +### Function: `ProposalsList.getProposal` +```solidity +function getProposal(bytes32 key) public view returns (Proposal memory) +``` +Returns the `Proposal` with the given key. + +### Function: `ProposalsList.getProposalsLength` +```solidity +function getProposalsLength() public view returns (uint256) +``` +Returns the total number of created `Proposal`s. + +### Function: `ProposalsList.getOrderedKeys` +```solidity +function getOrderedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) +``` +Returns a list of `Proposal` keys, starting from the specified `offset` and bounded by the specified `limit`. + + +## Contract: `HashConsensus` +`HashConsensus` is an abstract contract that facilitates consensus-based decision-making among a set of members. Consensus is achieved through members voting on a specific hash, with decisions executed only if a quorum is reached and a timelock period has elapsed. + +### Function: `HashConsensus.addMember` +```solidity +function addMember(address newMember, uint256 newQuorum) public onlyOwner +``` +Adds a new member and updates the quorum. + +#### Preconditions +- Only the `owner` can call this function. +- `newQuorum` MUST be greater than 0 and less than or equal to the number of members. + +### Function: `HashConsensus.removeMember` +```solidity +function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner +``` +Removes a member and updates the quorum. + +#### Preconditions +- Only the `owner` can call this function. +- `memberToRemove` MUST be an added member. +- `newQuorum` MUST be greater than 0 and less than or equal to the number of remaining members. + +### Function: `HashConsensus.getMembers` +```solidity +function getMembers() public view returns (address[] memory) +``` +Returns the list of current members. + +### Function: `HashConsensus.isMember` +```solidity +function isMember(address member) public view returns (bool) +``` +Returns whether an account is listed as a member. + +### Function: `HashConsensus.setTimelockDuration` +```solidity +function setTimelockDuration(uint256 timelock) public onlyOwner +``` +Sets the duration of the timelock. + +#### Preconditions +- Only the `owner` can call this function. + +### Function: `HashConsensus.setQuorum` +```solidity +function setQuorum(uint256 newQuorum) public onlyOwner +``` +Sets the quorum required for decision execution. + +#### Preconditions +- Only the `owner` can call this function. +- `newQuorum` MUST be greater than 0 and less than or equal to the number of members. + +## Contract: `EmergencyActivationCommittee` +`EmergencyActivationCommittee` is a smart contract that extends the functionality of the `HashConsensus` contract to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the specified contract. + +### Constructor +```solidity +constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock +) +``` +Initializes the contract with an owner, committee members, a quorum, and the address of the `EmergencyProtectedTimelock` contract. + +#### Preconditions +- `executionQuorum` MUST be greater than 0. + +### Function: `EmergencyActivationCommittee.approveActivateEmergencyMode` +```solidity +function approveActivateEmergencyMode() public onlyMember +``` +Approves the emergency activation by voting on the `EMERGENCY_ACTIVATION_HASH`. + +#### Preconditions +- MUST be called by a committee member. + +### Function: `EmergencyActivationCommittee.getActivateEmergencyModeState` +```solidity +function getActivateEmergencyModeState() + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` +Returns the state of the emergency activation proposal, including the support count, quorum, and execution status. + +### Function: `EmergencyActivationCommittee.executeActivateEmergencyMode` +```solidity +function executeActivateEmergencyMode() external +``` +Executes the emergency activation by calling the `emergencyActivate` function on the `EmergencyProtectedTimelock` contract. + +#### Preconditions +- The emergency activation proposal MUST have reached quorum and passed the timelock duration. + +## Contract: `EmergencyExecutionCommittee` +`EmergencyExecutionCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage emergency execution and governance reset proposals through a consensus mechanism. It interacts with the `EmergencyProtectedTimelock` contract to execute critical emergency proposals. + +### Constructor +```solidity +constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock +) +``` +Initializes the contract with an owner, committee members, a quorum, and the address of the `EmergencyProtectedTimelock` contract. + +#### Preconditions +- `executionQuorum` MUST be greater than 0. + +### Function: `EmergencyExecutionCommittee.voteEmergencyExecute` +```solidity +function voteEmergencyExecute(uint256 proposalId, bool _supports) public +``` +Allows committee members to vote on an emergency execution proposal. + +#### Preconditions +- MUST be called by a committee member. + +### Function: `EmergencyExecutionCommittee.getEmergencyExecuteState` +```solidity +function getEmergencyExecuteState(uint256 proposalId) + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` +Returns the state of an emergency execution proposal, including the support count, quorum, and execution status. + +### Function: `EmergencyExecutionCommittee.executeEmergencyExecute` +```solidity +function executeEmergencyExecute(uint256 proposalId) public +``` +Executes an emergency execution proposal by calling the `emergencyExecute` function on the `EmergencyProtectedTimelock` contract. + +#### Preconditions +- The emergency execution proposal MUST have reached quorum and passed the timelock duration. + +### Function: `EmergencyExecutionCommittee.approveEmergencyReset` +```solidity +function approveEmergencyReset() public +``` +Approves the governance reset by voting on the reset proposal. + +#### Preconditions +- MUST be called by a committee member. + +### Function: `EmergencyExecutionCommittee.getEmergencyResetState` +```solidity +function getEmergencyResetState() + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` +Returns the state of the governance reset proposal, including the support count, quorum, and execution status. + +### Function: `EmergencyExecutionCommittee.executeEmergencyReset` +```solidity +function executeEmergencyReset() external +``` +Executes the governance reset by calling the `emergencyReset` function on the `EmergencyProtectedTimelock` contract. + +#### Preconditions +- The governance reset proposal MUST have reached quorum and passed the timelock duration. + + diff --git a/docs/specification.md b/docs/specification.md index 7d01471f..6eb412a1 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -109,7 +109,11 @@ The protected deployment mode is a temporary mode designed to be active during a In this mode, an **emergency activation committee** has the one-off and time-limited right to activate an adversarial **emergency mode** if they see a scheduled proposal that was created or altered due to a vulnerability in the DG contracts or if governance execution is prevented by such a vulnerability. Once the emergency mode is activated, the emergency activation committee is disabled, i.e. loses the ability to activate the emergency mode again. If the emergency activation committee doesn't activate the emergency mode within the duration of the **emergency protection duration** since the committee was configured by the DAO, it gets automatically disabled as well. -The emergency mode lasts up to the **emergency mode max duration** counting from the moment of its activation. While it's active, 1) only the **emergency execution committee** has the right to execute scheduled proposals, and 2) the same committee has the one-off right to **disable the DG subsystem**, i.e. disconnect the `EmergencyProtectedTimelock` contract and its associated executor contracts from the DG contracts and reconnect it to the Lido DAO Voting/Agent contract. The latter also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors. +The emergency mode lasts up to the **emergency mode max duration** counting from the moment of its activation. While it's active, the following conditions apply: +1) Only the **emergency execution committee** has the right to execute scheduled proposals +2) The same committee has the one-off right to **disable the DG subsystem**. After this action, the system should start behaving according to [this specification](plan-b.md)). This involves disconnecting the `EmergencyProtectedTimelock` contract and its associated executor contracts from the DG contracts and reconnect them to the `TimelockedGovernance` contract instance. + +Disabling the DG subsystem also disables also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors. If the emergency execution committee doesn't disable the DG until the emergency mode max duration elapses, anyone gets the right to deactivate the emergency mode, switching the system back to the protected mode and disabling the emergency committee. @@ -394,42 +398,44 @@ In the Lido protocol, specific critical components (`WithdrawalQueue` and `Valid However, the effectiveness of this approach is contingent upon the predictability of the DAO's solution adoption timeframe. With the dual governance system, proposal execution may experience significant delays based on the current state of the `DualGovernance` contract. There's a risk that `GateSeal`'s pause period may expire before the Lido DAO can implement the necessary fixes. -To address the compatibility challenge between gate seals and dual governance, the `ResealManager` contract is introduced. This smart contract is designed to manage the resealing and resuming of sealable contracts during emergencies. The `ResealManager` can extend the pause of temporarily paused contracts to a permanent pause or resume them if the following conditions are met: - -- The `ResealManager` holds the `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. -- Contracts are paused until a specific timestamp that is in the future and not indefinitely. -- DAO governance is blocked by `DualGovernance`. - -### Constructor +To address this compatibility challenge between gate seals and dual governance, the `ResealManager` contract is introduced. The `ResealManager` allows to extend pause of temporarily paused contracts to permanent pause or resume it, if conditions are met: +- `ResealManager` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. +- Contracts are paused until timestamp after current timestamp and not for infinite time. +- The DAO governance is blocked by `DualGovernance` +- Only governance address obtained from `EmergencyProtectedTimelock` can trigger these actions. ```solidity constructor(address emergencyProtectedTimelock) ``` -Initializes the contract with the address of the `EmergencyProtectedTimelock` contract. +Initializes the contract with the address of the EmergencyProtectedTimelock contract. #### Preconditions -* `emergencyProtectedTimelock` MUST be a valid address. +* emergencyProtectedTimelock MUST be a valid address. -### Function: ResealManager.reseal +### Function ResealManager.reseal ```solidity -function reseal(address[] memory sealables) external onlyGovernance +function reseal(address sealable) public ``` -Extends the pause of the specified `sealables` contracts. This function can be called by the governance address defined in the `EmergencyProtectedTimelock`. +Extends the pause of the specified `sealable` contract. This function can be called by the governance address defined in the `EmergencyProtectedTimelock`. #### Preconditions -- The `ResealManager` MUST have `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. -- The target contracts MUST be paused until a future timestamp and not indefinitely. +- The `ResealManager` MUST have `PAUSE_ROLE` and `RESUME_ROLE` for the target contract. +- The target contract MUST be paused until a future timestamp and not indefinitely. - The function MUST be called by the governance address defined in `EmergencyProtectedTimelock`. +#### Errors +- `SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. +- `SenderIsNotGovernance`: Thrown if the sender is not the governance address. + ### Function: ResealManager.resume ```solidity -function resume(address sealable) external onlyGovernance +function resume(address sealable) external ``` Resumes the specified `sealable` contract if it is scheduled to resume in the future. @@ -440,24 +446,14 @@ Resumes the specified `sealable` contract if it is scheduled to resume in the fu - The target contract MUST be paused. - The function MUST be called by the governance address defined in `EmergencyProtectedTimelock`. -### Modifier: ResealManager.onlyGovernance - -```solidity -modifier onlyGovernance() -``` - -Ensures that the function can only be called by the governance address. +#### Errors +- `SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. +- `SenderIsNotGovernance`: Thrown if the sender is not the governance address. #### Preconditions - The sender MUST be the governance address obtained from the `EmergencyProtectedTimelock` contract. -### Errors - -- `SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. -- `SenderIsNotGovernance`: Thrown if the sender is not the governance address. - - ## Contract: Escrow.sol The `Escrow` contract serves as an accumulator of users' (w)stETH, withdrawal NFTs (unstETH), and ETH. It has two internal states and serves a different purpose depending on its state: @@ -476,7 +472,7 @@ At any point in time, there can be only one instance of the contract in the `Sig After the `Escrow` instance transitions into the `RageQuitEscrow` state, all locked stETH and wstETH tokens are meant to be converted into withdrawal NFTs using the permissionless `Escrow.requestNextWithdrawalsBatch()` function. -Once all funds locked in the `Escrow` instance are converted into withdrawal NFTs, finalized, and claimed, the main rage quit phase concludes, and the `RageQuitExtensionDelay` period begins. +Once all funds locked in the `Escrow` instance are converted into withdrawal NFTs, finalized, and claimed, the main rage quit phase concludes, and the `Escrow.startRageQuitExtensionDelay()` method may be used to start the `RageQuitExtensionDelay` period. The purpose of the `RageQuitExtensionDelay` phase is to provide sufficient time to participants who locked withdrawal NFTs to claim them before Lido DAO's proposal execution is unblocked. As soon as a withdrawal NFT is claimed, the user's ETH is no longer affected by any code controlled by the DAO. @@ -503,7 +499,7 @@ stETHTotals.lockedShares += amountInShares; The rage quit support will be dynamically updated to reflect changes in the stETH balance due to protocol rewards or validators slashing. -Finally, the function calls `DualGovernance.activateNextState()`, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. +The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. #### Returns @@ -561,7 +557,7 @@ assets[msg.sender].stETHLockedShares += stETHShares; stETHTotals.lockedShares += stETHShares; ``` -Finally, the function calls the `DualGovernance.activateNextState()`. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. +The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. #### Returns @@ -619,7 +615,7 @@ assets[msg.sender].unstETHLockedShares += amountOfShares; unstETHTotals.unfinalizedShares += amountOfShares; ``` -Finally, calls the `DualGovernance.activateNextState()` function. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. +The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. #### Preconditions @@ -702,6 +698,24 @@ Withdrawal NFTs belonging to any of the following categories are excluded from t - The `Escrow` instance MUST be in the `SignallingEscrow` state. +### Function Escrow.requestWithdrawals + +```solidity +function requestWithdrawals(uint256[] calldata stETHAmounts) returns (uint256[] memory unstETHIds) +``` + +Allows users who have locked their stETH and wstETH to convert it into unstETH NFTs by requesting withdrawals on the Lido's `WithdrawalQueue` contract. + +Internally, this function marks the total amount specified in `stETHAmounts` as unlocked from the `Escrow` and accounts for it in the form of a list of unstETH NFTs, with amounts corresponding to `stETHAmounts`. + +#### Preconditions +- The total amount specified in `stETHAmounts` MUST NOT exceed the user's currently locked stETH and wstETH. +- The `stETHAmounts` values MUST be in range [`WithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()`, `WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()`]. + +#### Returns + +An array of ids for the generated unstETH NFTs. + ### Function Escrow.getRageQuitSupport() ```solidity @@ -752,7 +766,7 @@ Upon execution, the function tracks the ids of the withdrawal requests generated #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The `maxBatchSize` MUST be greater than or equal to `CONFIG.MIN_WITHDRAWALS_BATCH_SIZE()` and less than or equal to `CONFIG.MAX_WITHDRAWALS_BATCH_SIZE()`. +- The `maxBatchSize` MUST be greater than or equal to `Escrow.MIN_WITHDRAWALS_BATCH_SIZE()`. - The generation of withdrawal request batches MUST not be concluded ### Function Escrow.claimNextWithdrawalsBatch(uint256, uint256[]) @@ -762,12 +776,13 @@ function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] hints) ``` Allows users to claim finalized withdrawal NFTs generated by the `Escrow.requestNextWithdrawalsBatch()` function. -Tracks the total amount of claimed ETH updating the `stETHTotals.claimedETH` variable. Upon claiming the last batch, the `RageQuitExtensionDelay` period commences. +This function updates the `stETHTotals.claimedETH` variable to track the total amount of claimed ETH. #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The `fromUnstETHId` MUST be equal to the id of the first unclaimed withdrawal NFT locked in the `Escrow`. The ids of the unclaimed withdrawal NFTs can be retrieved via the `getNextWithdrawalBatch()` method. +- There MUST be at least one unclaimed withdrawal NFT. ### Function Escrow.claimNextWithdrawalsBatch(uint256) @@ -792,6 +807,19 @@ To safeguard the ETH associated with withdrawal NFTs, this function should be in - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The provided `unstETHIds` MUST only contain finalized but unclaimed withdrawal requests with the owner set to `msg.sender`. +### Function Escrow.startRageQuitExtensionDelay + +```solidity +function startRageQuitExtensionDelay() +``` + +Initiates the `RageQuitExtensionDelay` once all withdrawal batches have been claimed. In cases where the `Escrow` instance only has locked unstETH NFTs, it verifies that the last unstETH NFT registered in the `WithdrawalQueue` at the time of the `Escrow.startRageQuit()` call is finalized. This ensures that every unstETH NFT locked in the Escrow can be claimed by the user during the `RageQuitExtensionDelay`. + +#### Preconditions +- All withdrawal batches MUST be formed using the `Escrow.requestNextWithdrawalsBatch()`. +- The last unstETH NFT in the `WithdrawalQueue` at the time of the `Escrow.startRageQuit()` call MUST be finalized. +- All withdrawal batches generated during `Escrow.requestNextWithdrawalsBatch()` MUST be claimed. + ### Function Escrow.isRageQuitFinalized ```solidity @@ -1034,31 +1062,32 @@ Returns total list of `Proposal`s keys with offset and limit. `HashConsensus` is an abstract contract that allows for consensus-based decision-making among a set of members. The consensus is achieved by members voting on a specific hash, and decisions can only be executed if a quorum is reached and a timelock period has elapsed. -### Function: HashConsensus.addMember +### Function: HashConsensus.addMembers ```solidity -function addMember(address newMember, uint256 newQuorum) public onlyOwner +function addMembers(address[] memory newMembers, uint256 newQuorum) public onlyOwner ``` -Adds a new member and updates the quorum. +Adds new members and updates the quorum. #### Preconditions * Only the owner can call this function. +* Members MUST NOT be part of the set. * `newQuorum` MUST be greater than 0 and less than or equal to the number of members. ### Function: HashConsensus.removeMember ```solidity -function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner +function removeMembers(address[] memory membersToRemove, uint256 newQuorum) public onlyOwner ``` -Removes a member and updates the quorum. +Removes members and updates the quorum. #### Preconditions * Only the owner can call this function. -* Member MUST be part of the set. +* Members MUST be part of the set. * `newQuorum` MUST be greater than 0 and less than or equal to the number of remaining members. ### Function: HashConsensus.getMembers @@ -1135,7 +1164,7 @@ Initializes the contract with an owner, committee members, a quorum, the address ### Function: TiebreakerCore.scheduleProposal ```solidity -function scheduleProposal(uint256 proposalId) public onlyMember +function scheduleProposal(uint256 proposalId) public ``` Schedules a proposal for execution by voting on it and adding it to the proposal list. @@ -1178,7 +1207,7 @@ Returns the current nonce for resuming operations of a sealable contract. ### Function: TiebreakerCore.sealableResume ```solidity -function sealableResume(address sealable, uint256 nonce) public onlyMember +function sealableResume(address sealable, uint256 nonce) public ``` Submits a request to resume operations of a sealable contract by voting on it and adding it to the proposal list. @@ -1235,7 +1264,7 @@ Initializes the contract with an owner, committee members, a quorum, and the add ### Function: TiebreakerSubCommittee.scheduleProposal ```solidity -function scheduleProposal(uint256 proposalId) public onlyMember +function scheduleProposal(uint256 proposalId) public ``` Schedules a proposal for execution by voting on it and adding it to the proposal list. @@ -1322,10 +1351,10 @@ executionQuorum MUST be greater than 0. emergencyProtectedTimelock MUST be a valid address. -### Function: EmergencyActivationCommittee.approveEmergencyActivate +### Function: EmergencyActivationCommittee.approveActivateEmergencyMode ```solidity -function approveEmergencyActivate() public onlyMember +function approveActivateEmergencyMode() public ``` Approves the emergency activation by voting on the EMERGENCY_ACTIVATION_HASH. @@ -1334,10 +1363,10 @@ Approves the emergency activation by voting on the EMERGENCY_ACTIVATION_HASH. * MUST be called by a member. -### Function: EmergencyActivationCommittee.getEmergencyActivateState +### Function: EmergencyActivationCommittee.getActivateEmergencyModeState ```solidity -function getEmergencyActivateState() +function getActivateEmergencyModeState() public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) @@ -1345,10 +1374,10 @@ function getEmergencyActivateState() Returns the state of the emergency activation proposal including support count, quorum, and execution status. -### Function: EmergencyActivationCommittee.executeEmergencyActivate +### Function: EmergencyActivationCommittee.executeActivateEmergencyMode ```solidity -function executeEmergencyActivate() external +function executeActivateEmergencyMode() external ``` Executes the emergency activation by calling the emergencyActivate function on the EmergencyProtectedTimelock contract. @@ -1383,7 +1412,7 @@ Initializes the contract with an owner, committee members, a quorum, and the add ### Function: EmergencyExecutionCommittee.voteEmergencyExecute ```solidity -function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember +function voteEmergencyExecute(uint256 proposalId, bool _supports) public ``` Allows committee members to vote on an emergency execution proposal. @@ -1418,7 +1447,7 @@ Emergency execution proposal MUST have reached quorum and passed the timelock du ### Function: EmergencyExecutionCommittee.approveEmergencyReset ```solidity -function approveEmergencyReset() public onlyMember +function approveEmergencyReset() public ``` Approves the governance reset by voting on the reset proposal. diff --git a/foundry.toml b/foundry.toml index f609c46e..b40e7e68 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = 'out' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' -solc-version = "0.8.26" +# solc-version = "0.8.26" no-match-path = 'test/kontrol/*' [profile.kprove] diff --git a/hardhat.config.js b/hardhat.config.js deleted file mode 100644 index 35d7b2e7..00000000 --- a/hardhat.config.js +++ /dev/null @@ -1,20 +0,0 @@ -require("@nomicfoundation/hardhat-chai-matchers") -require("@nomicfoundation/hardhat-foundry") -require("@nomicfoundation/hardhat-toolbox") -require("solidity-coverage") - -module.exports = { - defaultNetwork: "hardhat", - solidity: { - compilers: [ - { - version: "0.8.23", - settings: { - optimizer: { - enabled: false, - } - }, - }, - ], - }, -} diff --git a/package.json b/package.json index 13453cac..7e2b49f4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "scripts": { "test": "forge test", "prepare": "husky", - "lint": "solhint \"contracts/**/*.sol\" --ignore-path .solhintignore" + "lint": "solhint \"contracts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", + "coverage": "forge coverage", + "precov-report": "mkdir -p ./coverage-report && forge coverage --report lcov --report-file ./coverage-report/lcov.info", + "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --exclude test --ignore-errors inconsistent --ignore-errors category" }, "lint-staged": { "*.sol": [ diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 00000000..e2bc9ded --- /dev/null +++ b/slither.config.json @@ -0,0 +1,7 @@ +{ + "exclude_informational": true, + "exclude_low": true, + "exclude_medium": false, + "exclude_high": false, + "filter_paths": "(.test/|.template/|node_modules/|.lib/|.contracts/model)" +} diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol index bb0094c0..277ed143 100644 --- a/test/kontrol/VetoSignalling.t.sol +++ b/test/kontrol/VetoSignalling.t.sol @@ -7,9 +7,9 @@ import "kontrol-cheatcodes/KontrolCheats.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; -import "contracts/model/DualGovernance.sol"; -import "contracts/model/EmergencyProtectedTimelock.sol"; -import "contracts/model/Escrow.sol"; +import "./model/DualGovernance.sol"; +import "./model/EmergencyProtectedTimelock.sol"; +import "./model/Escrow.sol"; contract FakeETH is ERC20("fakeETH", "fETH") {} diff --git a/contracts/model/DualGovernance.sol b/test/kontrol/model/DualGovernance.sol similarity index 99% rename from contracts/model/DualGovernance.sol rename to test/kontrol/model/DualGovernance.sol index c5a192f0..a00bde09 100644 --- a/contracts/model/DualGovernance.sol +++ b/test/kontrol/model/DualGovernance.sol @@ -61,7 +61,7 @@ contract DualGovernance { * Submits a proposal for consideration within the governance model. * Proposals can be submitted when in the Normal state or during Veto Signalling; however they cannot be executed in Veto Signalling. */ - function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { activateNextState(); require(proposers[msg.sender], "Caller is not authorized to submit proposals."); diff --git a/contracts/model/EmergencyProtectedTimelock.sol b/test/kontrol/model/EmergencyProtectedTimelock.sol similarity index 98% rename from contracts/model/EmergencyProtectedTimelock.sol rename to test/kontrol/model/EmergencyProtectedTimelock.sol index f63bd6c9..44cb5b43 100644 --- a/contracts/model/EmergencyProtectedTimelock.sol +++ b/test/kontrol/model/EmergencyProtectedTimelock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; // Struct to represent executor calls -struct ExecutorCall { +struct ExternalCall { address target; uint96 value; bytes payload; @@ -18,7 +18,7 @@ enum ProposalStatus { struct Proposal { uint256 id; address proposer; - ExecutorCall[] calls; + ExternalCall[] calls; uint256 submissionTime; uint256 schedulingTime; ProposalStatus status; @@ -52,7 +52,7 @@ contract EmergencyProtectedTimelock { } // Submits a new proposal, initializing its timelock and storing its calls. - function submit(address executor, ExecutorCall[] memory calls) external returns (uint256 proposalId) { + function submit(address executor, ExternalCall[] memory calls) external returns (uint256 proposalId) { // Ensure that only the governance can submit new proposals. require(msg.sender == governance, "Only governance can submit proposal."); diff --git a/contracts/model/Escrow.sol b/test/kontrol/model/Escrow.sol similarity index 100% rename from contracts/model/Escrow.sol rename to test/kontrol/model/Escrow.sol diff --git a/test/mocks/SealableMock.sol b/test/mocks/SealableMock.sol new file mode 100644 index 00000000..73da6af8 --- /dev/null +++ b/test/mocks/SealableMock.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ISealable} from "contracts/interfaces/ISealable.sol"; + +contract SealableMock is ISealable { + bool private paused; + bool private shouldRevertPauseFor; + bool private shouldRevertIsPaused; + bool private shouldRevertResume; + + function getResumeSinceTimestamp() external view override returns (uint256) { + revert("Not implemented"); + } + + function setShouldRevertPauseFor(bool _shouldRevert) external { + shouldRevertPauseFor = _shouldRevert; + } + + function setShouldRevertIsPaused(bool _shouldRevert) external { + shouldRevertIsPaused = _shouldRevert; + } + + function setShouldRevertResume(bool _shouldRevert) external { + shouldRevertResume = _shouldRevert; + } + + function pauseFor(uint256) external override { + if (shouldRevertPauseFor) { + revert("pauseFor failed"); + } + paused = true; + } + + function isPaused() external view override returns (bool) { + if (shouldRevertIsPaused) { + revert("isPaused failed"); + } + return paused; + } + + function resume() external override { + if (shouldRevertResume) { + revert("resume failed"); + } + paused = false; + } +} diff --git a/test/unit/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol similarity index 60% rename from test/unit/mocks/TimelockMock.sol rename to test/mocks/TimelockMock.sol index a513ecef..314caed3 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -2,12 +2,18 @@ pragma solidity 0.8.26; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {ITimelock} from "contracts/interfaces/ITimelock.sol"; -import {ExecutorCall} from "contracts/libraries/Proposals.sol"; +import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; contract TimelockMock is ITimelock { uint8 public constant OFFSET = 1; + address internal immutable _ADMIN_EXECUTOR; + + constructor(address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + } + mapping(uint256 => bool) public canScheduleProposal; uint256[] public submittedProposals; @@ -16,7 +22,9 @@ contract TimelockMock is ITimelock { uint256 public lastCancelledProposalId; - function submit(address, ExecutorCall[] calldata) external returns (uint256 newProposalId) { + address internal governance; + + function submit(address, ExternalCall[] calldata) external returns (uint256 newProposalId) { newProposalId = submittedProposals.length + OFFSET; submittedProposals.push(newProposalId); canScheduleProposal[newProposalId] = false; @@ -67,7 +75,39 @@ contract TimelockMock is ITimelock { return lastCancelledProposalId; } - function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt) { + function getProposal(uint256 proposalId) external view returns (Proposal memory) { + revert("Not Implemented"); + } + + function setGovernance(address newGovernance) external { + governance = newGovernance; + } + + function getGovernance() external view returns (address) { + return governance; + } + + function emergencyReset() external { revert("Not Implemented"); } + + function emergencyExecute(uint256 proposalId) external { + revert("Not Implemented"); + } + + function activateEmergencyMode() external { + revert("Not Implemented"); + } + + function getProposalInfo(uint256 proposalId) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + revert("Not Implemented"); + } + + function getAdminExecutor() external view returns (address) { + return _ADMIN_EXECUTOR; + } } diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index b49cf865..5cdebbee 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -1,34 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ExecutorCall, ScenarioTestBlueprint, ExecutorCall} from "../utils/scenario-test-blueprint.sol"; +import {ExternalCall, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract AgentTimelockTest is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); } function testFork_AgentTimelockHappyPath() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; _step("1. THE PROPOSAL IS SUBMITTED"); { - proposalId = _submitProposal( - _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls + proposalId = _submitProposalViaDualGovernance( + "Propose to doSmth on target passing dual governance", regularStaffCalls ); - _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), regularStaffCalls); - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); + _assertCanScheduleViaDualGovernance(proposalId, false); } _step("2. THE PROPOSAL IS SCHEDULED"); { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _assertCanExecute(proposalId, false); @@ -48,27 +46,27 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _assertProposalExecuted(proposalId); _assertCanExecute(proposalId, false); - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); - _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertTargetMockCalls(_getAdminExecutor(), regularStaffCalls); } } function testFork_TimelockEmergencyReset() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); // --- // 1. THE PROPOSAL IS SUBMITTED // --- uint256 proposalId; { - proposalId = _submitProposal( - _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls + proposalId = _submitProposalViaDualGovernance( + "Propose to doSmth on target passing dual governance", regularStaffCalls ); - _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); // proposal can't be scheduled until the AFTER_SUBMIT_DELAY has passed - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); } // --- @@ -76,13 +74,13 @@ contract AgentTimelockTest is ScenarioTestBlueprint { // --- { // wait until the delay has passed - _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); + _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); // when the first delay is passed and the is no opposition from the stETH holders // the proposal can be scheduled - _assertCanSchedule(_dualGovernance, proposalId, true); + _assertCanScheduleViaDualGovernance(proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); // proposal can't be executed until the second delay has ended _assertProposalScheduled(proposalId); @@ -95,7 +93,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { { // some time passes and emergency committee activates emergency mode // and resets the controller - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); // committee resets governance vm.prank(address(_emergencyActivationCommittee)); @@ -105,11 +103,11 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _timelock.emergencyReset(); // proposal is canceled now - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); // remove canceled call from the timelock _assertCanExecute(proposalId, false); - _assertProposalCanceled(proposalId); + _assertProposalCancelled(proposalId); } } } diff --git a/test/scenario/emergency-committee.t.sol b/test/scenario/emergency-committee.t.sol new file mode 100644 index 00000000..c1962a37 --- /dev/null +++ b/test/scenario/emergency-committee.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; +import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; +import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; + +contract EmergencyCommitteeTest is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); + _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(10_00)); + _lockStETH(_VETOER, 1 ether); + } + + function test_emergency_committees_happy_path() external { + uint256 quorum; + uint256 support; + bool isExecuted; + + address[] memory members; + + ExternalCall[] memory proposalCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (0)) + ); + uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); + + _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); + _assertCanSchedule(_dualGovernance, proposalIdToExecute, true); + _scheduleProposal(_dualGovernance, proposalIdToExecute); + + // Emergency Activation + members = _emergencyActivationCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyActivationCommittee.quorum() - 1; i++) { + vm.prank(members[i]); + _emergencyActivationCommittee.approveActivateEmergencyMode(); + (support, quorum, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _emergencyActivationCommittee.approveActivateEmergencyMode(); + (support, quorum, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); + assert(support == quorum); + assert(isExecuted == false); + + _emergencyActivationCommittee.executeActivateEmergencyMode(); + (support, quorum, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); + assert(isExecuted == true); + + // Emergency Execute + members = _emergencyExecutionCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum() - 1; i++) { + vm.prank(members[i]); + _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); + (support, quorum, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); + (support, quorum, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); + assert(support == quorum); + assert(isExecuted == false); + + _emergencyExecutionCommittee.executeEmergencyExecute(proposalIdToExecute); + (support, quorum, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); + assert(isExecuted == true); + } +} diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index e5b1414d..badee1ef 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -1,95 +1,61 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {WithdrawalRequestStatus} from "../utils/interfaces.sol"; -import {Duration as DurationType} from "contracts/types/Duration.sol"; -import { - Escrow, - Balances, - WITHDRAWAL_QUEUE, - ScenarioTestBlueprint, - VetoerState, - LockedAssetsTotals, - Durations -} from "../utils/scenario-test-blueprint.sol"; - -contract TestHelpers is ScenarioTestBlueprint { - function rebase(int256 deltaBP) public { - bytes32 CL_BALANCE_POSITION = 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483; // keccak256("lido.Lido.beaconBalance"); - - uint256 totalSupply = _ST_ETH.totalSupply(); - uint256 clBalance = uint256(vm.load(address(_ST_ETH), CL_BALANCE_POSITION)); - - int256 delta = (deltaBP * int256(totalSupply) / 10000); - vm.store(address(_ST_ETH), CL_BALANCE_POSITION, bytes32(uint256(int256(clBalance) + delta))); +import {console} from "forge-std/Test.sol"; - assertEq( - uint256(int256(totalSupply) * deltaBP / 10000 + int256(totalSupply)), _ST_ETH.totalSupply(), "total supply" - ); - } +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; - function finalizeWQ() public { - uint256 lastRequestId = _WITHDRAWAL_QUEUE.getLastRequestId(); - finalizeWQ(lastRequestId); - } +import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; - function finalizeWQ(uint256 id) public { - uint256 finalizationShareRate = _ST_ETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate - address lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; - vm.prank(lido); - _WITHDRAWAL_QUEUE.finalize(id, finalizationShareRate); +import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; - bytes32 LOCKED_ETHER_AMOUNT_POSITION = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); +import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; - vm.store(WITHDRAWAL_QUEUE, LOCKED_ETHER_AMOUNT_POSITION, bytes32(address(WITHDRAWAL_QUEUE).balance)); - } -} +import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; + +contract EscrowHappyPath is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; -contract EscrowHappyPath is TestHelpers { Escrow internal escrow; - DurationType internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = Durations.from(14 days); - DurationType internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = Durations.from(7 days); + Duration internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = Durations.from(14 days); + Duration internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = Durations.from(7 days); address internal immutable _VETOER_1 = makeAddr("VETOER_1"); address internal immutable _VETOER_2 = makeAddr("VETOER_2"); - Balances internal _firstVetoerBalances; - Balances internal _secondVetoerBalances; - function setUp() external { - _selectFork(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); escrow = _getVetoSignallingEscrow(); - _setupStETHWhale(_VETOER_1); + _setupStETHBalance(_VETOER_1, PercentsD16.fromBasisPoints(10_00)); + vm.startPrank(_VETOER_1); - _ST_ETH.approve(address(_WST_ETH), type(uint256).max); - _ST_ETH.approve(address(escrow), type(uint256).max); - _ST_ETH.approve(address(_WITHDRAWAL_QUEUE), type(uint256).max); - _WST_ETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.wstETH), type(uint256).max); + _lido.stETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.withdrawalQueue), type(uint256).max); + _lido.wstETH.approve(address(escrow), type(uint256).max); - _WST_ETH.wrap(100_000 * 10 ** 18); + _lido.wstETH.wrap(100_000 * 10 ** 18); vm.stopPrank(); - _setupStETHWhale(_VETOER_2); + _setupStETHBalance(_VETOER_2, PercentsD16.fromBasisPoints(10_00)); + vm.startPrank(_VETOER_2); - _ST_ETH.approve(address(_WST_ETH), type(uint256).max); - _ST_ETH.approve(address(escrow), type(uint256).max); - _ST_ETH.approve(address(_WITHDRAWAL_QUEUE), type(uint256).max); - _WST_ETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.wstETH), type(uint256).max); + _lido.stETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.withdrawalQueue), type(uint256).max); + _lido.wstETH.approve(address(escrow), type(uint256).max); - _WST_ETH.wrap(100_000 * 10 ** 18); + _lido.wstETH.wrap(100_000 * 10 ** 18); vm.stopPrank(); - - _firstVetoerBalances = _getBalances(_VETOER_1); - _secondVetoerBalances = _getBalances(_VETOER_2); } function test_lock_unlock() public { - uint256 firstVetoerStETHBalanceBefore = _ST_ETH.balanceOf(_VETOER_1); - uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); + uint256 firstVetoerStETHBalanceBefore = _lido.stETH.balanceOf(_VETOER_1); + uint256 secondVetoerWstETHBalanceBefore = _lido.wstETH.balanceOf(_VETOER_2); uint256 firstVetoerLockStETHAmount = 1 ether; uint256 firstVetoerLockWstETHAmount = 2 ether; @@ -103,34 +69,33 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, secondVetoerLockStETHAmount); _lockWstETH(_VETOER_2, secondVetoerLockWstETHAmount); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _unlockStETH(_VETOER_1); assertApproxEqAbs( - _ST_ETH.balanceOf(_VETOER_1), - firstVetoerStETHBalanceBefore + _ST_ETH.getPooledEthByShares(firstVetoerLockWstETHAmount), + _lido.stETH.balanceOf(_VETOER_1), + firstVetoerStETHBalanceBefore + _lido.stETH.getPooledEthByShares(firstVetoerLockWstETHAmount), 1 ); _unlockWstETH(_VETOER_2); assertApproxEqAbs( secondVetoerWstETHBalanceBefore, - _WST_ETH.balanceOf(_VETOER_2), - secondVetoerWstETHBalanceBefore + _ST_ETH.getSharesByPooledEth(secondVetoerLockWstETHAmount) + _lido.wstETH.balanceOf(_VETOER_2), + secondVetoerWstETHBalanceBefore + _lido.stETH.getSharesByPooledEth(secondVetoerLockWstETHAmount) ); } function test_lock_unlock_w_rebase() public { uint256 firstVetoerStETHAmount = 10 * 10 ** 18; - uint256 firstVetoerStETHShares = _ST_ETH.getSharesByPooledEth(firstVetoerStETHAmount); + uint256 firstVetoerStETHShares = _lido.stETH.getSharesByPooledEth(firstVetoerStETHAmount); uint256 firstVetoerWstETHAmount = 11 * 10 ** 18; uint256 secondVetoerStETHAmount = 13 * 10 ** 18; - uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; - uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); - uint256 secondVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_2); + uint256 firstVetoerWstETHBalanceBefore = _lido.wstETH.balanceOf(_VETOER_1); + uint256 secondVetoerStETHSharesBefore = _lido.stETH.sharesOf(_VETOER_2); _lockStETH(_VETOER_1, firstVetoerStETHAmount); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); @@ -138,23 +103,17 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, secondVetoerStETHAmount); _lockWstETH(_VETOER_2, secondVetoerWstETHAmount); - rebase(100); + _simulateRebase(PercentsD16.fromBasisPoints(101_00)); // +1% - uint256 firstVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_1); - uint256 firstVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_1); - - uint256 secondVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_2); - uint256 secondVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_2); - - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _unlockWstETH(_VETOER_1); assertApproxEqAbs( firstVetoerWstETHBalanceBefore + firstVetoerStETHShares, - _WST_ETH.balanceOf(_VETOER_1), + _lido.wstETH.balanceOf(_VETOER_1), // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount - // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + // into shares via _lido.stETH.getSharesByPooledEth(secondVetoerStETHAmount) 2 ); @@ -162,9 +121,10 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs( // all locked stETH and wstETH was withdrawn as stETH - _ST_ETH.getPooledEthByShares(secondVetoerStETHSharesBefore + secondVetoerWstETHAmount), - _ST_ETH.balanceOf(_VETOER_2), - 1 + _lido.stETH.getPooledEthByShares(secondVetoerStETHSharesBefore + secondVetoerWstETHAmount), + _lido.stETH.balanceOf(_VETOER_2), + // Considering that during the previous operation 2 wei may be lost, total rounding error may be 3 wei + 3 ); } @@ -174,10 +134,10 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHAmount = 13 * 10 ** 18; uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; - uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); + uint256 secondVetoerStETHShares = _lido.stETH.getSharesByPooledEth(secondVetoerStETHAmount); - uint256 firstVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_1); - uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); + uint256 firstVetoerStETHSharesBefore = _lido.stETH.sharesOf(_VETOER_1); + uint256 secondVetoerWstETHBalanceBefore = _lido.wstETH.balanceOf(_VETOER_2); _lockStETH(_VETOER_1, firstVetoerStETHAmount); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); @@ -185,15 +145,15 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, secondVetoerStETHAmount); _lockWstETH(_VETOER_2, secondVetoerWstETHAmount); - rebase(-100); + _simulateRebase(PercentsD16.fromBasisPoints(99_00)); // -1% - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _unlockStETH(_VETOER_1); assertApproxEqAbs( // all locked stETH and wstETH was withdrawn as stETH - _ST_ETH.getPooledEthByShares(firstVetoerStETHSharesBefore + firstVetoerWstETHAmount), - _ST_ETH.balanceOf(_VETOER_1), + _lido.stETH.getPooledEthByShares(firstVetoerStETHSharesBefore + firstVetoerWstETHAmount), + _lido.stETH.balanceOf(_VETOER_1), 1 ); @@ -201,10 +161,10 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs( secondVetoerWstETHBalanceBefore + secondVetoerStETHShares, - _WST_ETH.balanceOf(_VETOER_2), + _lido.wstETH.balanceOf(_VETOER_2), // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount - // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + // into shares via _lido.stETH.getSharesByPooledEth(secondVetoerStETHAmount) 2 ); } @@ -216,11 +176,11 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); _lockUnstETH(_VETOER_1, unstETHIds); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _unlockUnstETH(_VETOER_1, unstETHIds); } @@ -232,9 +192,9 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); - finalizeWQ(); + _finalizeWithdrawalQueue(); vm.expectRevert(); this.externalLockUnstETH(_VETOER_1, unstETHIds); @@ -248,10 +208,10 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); uint256 totalSharesLocked; - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { totalSharesLocked += statuses[i].amountOfShares; } @@ -265,14 +225,14 @@ contract EscrowHappyPath is TestHelpers { assertEq(totals.unstETHFinalizedETH, 0); assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); - finalizeWQ(unstETHIds[0]); + _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); totals = escrow.getLockedAssetsTotals(); assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); - uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; + uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); } @@ -282,38 +242,40 @@ contract EscrowHappyPath is TestHelpers { amounts[i] = 1e18; } - uint256 totalSupply = _ST_ETH.totalSupply(); - vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); uint256 amountToLock = 1e18; - uint256 sharesToLock = _ST_ETH.getSharesByPooledEth(amountToLock); + uint256 sharesToLock = _lido.stETH.getSharesByPooledEth(amountToLock); _lockStETH(_VETOER_1, amountToLock); _lockWstETH(_VETOER_1, sharesToLock); _lockUnstETH(_VETOER_1, unstETHIds); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 1); + uint256 totalSupply = _lido.stETH.totalSupply(); + + // epsilon is 2 here, because the wstETH unwrap may produce 1 wei error and stETH transfer 1 wei + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 2); assertEq(escrow.getVetoerState(_VETOER_1).unstETHIdsCount, 2); - uint256 rageQuitSupport = escrow.getRageQuitSupport(); - assertEq(rageQuitSupport, 4 * 1e18 * 1e18 / totalSupply); + assertEq(escrow.getRageQuitSupport(), PercentsD16.fromFraction({numerator: 4 ether, denominator: totalSupply})); - finalizeWQ(unstETHIds[0]); + _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); - uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; + + uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); - rageQuitSupport = escrow.getRageQuitSupport(); assertEq( - rageQuitSupport, - 10 ** 18 * (_ST_ETH.getPooledEthByShares(3 * sharesToLock) + ethAmountFinalized) - / (_ST_ETH.totalSupply() + ethAmountFinalized) + escrow.getRageQuitSupport(), + PercentsD16.fromFraction({ + numerator: _lido.stETH.getPooledEthByShares(3 * sharesToLock) + ethAmountFinalized, + denominator: _lido.stETH.totalSupply() + ethAmountFinalized + }) ); } @@ -325,15 +287,15 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); - uint256 requestShares = _ST_ETH.getSharesByPooledEth(30 * requestAmount); + uint256 requestShares = _lido.stETH.getSharesByPooledEth(30 * requestAmount); _lockStETH(_VETOER_1, 20 * requestAmount); _lockWstETH(_VETOER_1, requestShares); _lockUnstETH(_VETOER_1, unstETHIds); - rebase(100); + _simulateRebase(PercentsD16.fromBasisPoints(101_00)); // +1% vm.expectRevert(); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); @@ -341,41 +303,42 @@ contract EscrowHappyPath is TestHelpers { vm.prank(address(_dualGovernance)); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); - uint256 escrowStETHBalance = _ST_ETH.balanceOf(address(escrow)); + uint256 escrowStETHBalance = _lido.stETH.balanceOf(address(escrow)); uint256 expectedWithdrawalBatchesCount = escrowStETHBalance / requestAmount + 1; - assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10); + assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 10); escrow.requestNextWithdrawalsBatch(10); - assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 20); + assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 20); while (!escrow.isWithdrawalsBatchesFinalized()) { escrow.requestNextWithdrawalsBatch(96); } - assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); + assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); assertEq(escrow.isRageQuitFinalized(), false); - vm.deal(WITHDRAWAL_QUEUE, 1000 * requestAmount); - finalizeWQ(); + _finalizeWithdrawalQueue(); uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatch(expectedWithdrawalBatchesCount); // assertEq(total, expectedWithdrawalBatchesCount); - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIdsToClaim); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIdsToClaim); for (uint256 i = 0; i < statuses.length; ++i) { assertTrue(statuses[i].isFinalized); assertFalse(statuses[i].isClaimed); } - uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIdsToClaim, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); - while (!escrow.isWithdrawalsClaimed()) { - escrow.claimNextWithdrawalsBatch(128); + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + escrow.claimNextWithdrawalsBatch(32); } + escrow.startRageQuitExtensionDelay(); assertEq(escrow.isRageQuitFinalized(), false); // --- @@ -383,7 +346,7 @@ contract EscrowHappyPath is TestHelpers { // --- { uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.claimUnstETH(unstETHIds, hints); // but it can't be withdrawn before withdrawal timelock has passed @@ -416,24 +379,74 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); _lockUnstETH(_VETOER_1, unstETHIds); vm.prank(address(_dualGovernance)); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); - vm.deal(WITHDRAWAL_QUEUE, 100 * requestAmount); - finalizeWQ(); + _finalizeWithdrawalQueue(); escrow.requestNextWithdrawalsBatch(96); + vm.expectRevert(); escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); + escrow.startRageQuitExtensionDelay(); + assertEq(escrow.isRageQuitFinalized(), false); uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); + + escrow.claimUnstETH(unstETHIds, hints); + + assertEq(escrow.isRageQuitFinalized(), false); + + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); + assertEq(escrow.isRageQuitFinalized(), true); + + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); + + vm.startPrank(_VETOER_1); + escrow.withdrawETH(unstETHIds); + vm.stopPrank(); + } + + function test_wq_requests_onlyUnstETHWithUnfinalizedRequestsFails() external { + uint256 requestAmount = 10 * 1e18; + uint256 requestsCount = 10; + uint256[] memory amounts = new uint256[](requestsCount); + for (uint256 i = 0; i < requestsCount; ++i) { + amounts[i] = requestAmount; + } + + vm.prank(_VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); + + _lockUnstETH(_VETOER_1, unstETHIds); + + vm.prank(address(_dualGovernance)); + escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); + + vm.expectRevert(Escrow.BatchesQueueIsNotClosed.selector); + escrow.startRageQuitExtensionDelay(); + + escrow.requestNextWithdrawalsBatch(96); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + escrow.claimNextWithdrawalsBatch(0); + + vm.expectRevert(Escrow.UnfinalizedUnstETHIds.selector); + escrow.startRageQuitExtensionDelay(); + + _finalizeWithdrawalQueue(); + + escrow.startRageQuitExtensionDelay(); + + uint256[] memory hints = + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.claimUnstETH(unstETHIds, hints); @@ -453,7 +466,7 @@ contract EscrowHappyPath is TestHelpers { uint256 firstVetoerStETHAmount = 10 ether; uint256 firstVetoerWstETHAmount = 11 ether; - uint256 firstVetoerStETHShares = _ST_ETH.getSharesByPooledEth(firstVetoerStETHAmount); + uint256 firstVetoerStETHShares = _lido.stETH.getSharesByPooledEth(firstVetoerStETHAmount); uint256 totalSharesLocked = firstVetoerWstETHAmount + firstVetoerStETHShares; _lockStETH(_VETOER_1, firstVetoerStETHAmount); @@ -466,7 +479,7 @@ contract EscrowHappyPath is TestHelpers { ); assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, totalSharesLocked, 2); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); uint256[] memory stETHWithdrawalRequestAmounts = new uint256[](1); stETHWithdrawalRequestAmounts[0] = firstVetoerStETHAmount; @@ -478,7 +491,7 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, firstVetoerStETHShares, 2); uint256[] memory wstETHWithdrawalRequestAmounts = new uint256[](1); - wstETHWithdrawalRequestAmounts[0] = _ST_ETH.getPooledEthByShares(firstVetoerWstETHAmount); + wstETHWithdrawalRequestAmounts[0] = _lido.stETH.getPooledEthByShares(firstVetoerWstETHAmount); vm.prank(_VETOER_1); uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawals(wstETHWithdrawalRequestAmounts); @@ -486,12 +499,12 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, totalSharesLocked, 2); - finalizeWQ(wstETHWithdrawalRequestIds[0]); + _finalizeWithdrawalQueue(wstETHWithdrawalRequestIds[0]); escrow.markUnstETHFinalized( stETHWithdrawalRequestIds, - _WITHDRAWAL_QUEUE.findCheckpointHints( - stETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() + _lido.withdrawalQueue.findCheckpointHints( + stETHWithdrawalRequestIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ) ); assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); @@ -500,14 +513,14 @@ contract EscrowHappyPath is TestHelpers { escrow.markUnstETHFinalized( wstETHWithdrawalRequestIds, - _WITHDRAWAL_QUEUE.findCheckpointHints( - wstETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() + _lido.withdrawalQueue.findCheckpointHints( + wstETHWithdrawalRequestIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ) ); assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, 0, 2); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(_VETOER_1); escrow.unlockUnstETH(stETHWithdrawalRequestIds); @@ -519,7 +532,70 @@ contract EscrowHappyPath is TestHelpers { escrow.unlockUnstETH(wstETHWithdrawalRequestIds); } + function test_lock_unlock_funds_in_the_rage_quit_state_forbidden() external { + uint256[] memory nftAmounts = new uint256[](1); + nftAmounts[0] = 1 ether; + + vm.startPrank(_VETOER_1); + uint256[] memory lockedWithdrawalNfts = _lido.withdrawalQueue.requestWithdrawals(nftAmounts, _VETOER_1); + uint256[] memory notLockedWithdrawalNfts = _lido.withdrawalQueue.requestWithdrawals(nftAmounts, _VETOER_1); + vm.stopPrank(); + + _lockStETH(_VETOER_1, 1 ether); + _lockWstETH(_VETOER_1, 1 ether); + _lockUnstETH(_VETOER_1, lockedWithdrawalNfts); + + vm.prank(address(_dualGovernance)); + escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); + + // --- + // After the Escrow enters RageQuitEscrow state, lock/unlock of tokens is forbidden + // --- + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalLockStETH(_VETOER_1, 1 ether); + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalLockWstETH(_VETOER_1, 1 ether); + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalLockUnstETH(_VETOER_1, notLockedWithdrawalNfts); + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalUnlockStETH(_VETOER_1); + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalUnlockWstETH(_VETOER_1); + + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalUnlockUnstETH(_VETOER_1, lockedWithdrawalNfts); + } + + // --- + // Helper external methods to test reverts + // --- + function externalLockUnstETH(address vetoer, uint256[] memory unstETHIds) external { _lockUnstETH(vetoer, unstETHIds); } + + function externalLockStETH(address vetoer, uint256 stEthAmount) external { + _lockStETH(vetoer, stEthAmount); + } + + function externalLockWstETH(address vetoer, uint256 wstEthAmount) external { + _lockWstETH(vetoer, wstEthAmount); + } + + function externalUnlockStETH(address vetoer) external { + _unlockStETH(vetoer); + } + + function externalUnlockWstETH(address vetoer) external { + _unlockWstETH(vetoer); + } + + function externalUnlockUnstETH(address vetoer, uint256[] memory nftIds) external { + _unlockUnstETH(vetoer, nftIds); + } } diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index acaaa1cd..f41f9bd3 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -1,32 +1,35 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ScenarioTestBlueprint, percents, Durations} from "../utils/scenario-test-blueprint.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract GovernanceStateTransitions is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); function setUp() external { - _selectFork(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - _depositStETH(_VETOER, 1 ether); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _setupStETHBalance( + _VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); } function test_signalling_state_min_duration() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2).plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -35,16 +38,16 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_state_max_duration() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); @@ -60,19 +63,19 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_normal() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -81,7 +84,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); - _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); @@ -90,23 +93,23 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_non_stop() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); @@ -115,10 +118,10 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_rage_quit() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION()); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION()); _activateNextState(); _assertVetoSignalingState(); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index ed3afd4b..3c3170fd 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -1,47 +1,45 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + import { - EmergencyState, - EmergencyProtection, - IDangerousContract, ScenarioTestBlueprint, - ExecutorCall, - ExecutorCallHelpers, - DualGovernance, + ExternalCall, + ExternalCallHelpers, Timestamp, Timestamps, Durations } from "../utils/scenario-test-blueprint.sol"; -import {Proposals} from "contracts/libraries/Proposals.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; contract PlanBSetup is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deploySingleGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployTimelockedGovernanceSetup({isEmergencyProtectionEnabled: true}); } function testFork_PlanB_Scenario() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); // --- // ACT 1. 📈 DAO OPERATES AS USUALLY // --- { uint256 proposalId = _submitProposal( - _singleGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls + _timelockedGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls ); _assertProposalSubmitted(proposalId); _assertSubmittedProposalData(proposalId, regularStaffCalls); - _assertCanSchedule(_singleGovernance, proposalId, false); + _assertCanScheduleViaTimelockedGovernance(proposalId, false); _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_singleGovernance, proposalId, true); - _scheduleProposal(_singleGovernance, proposalId); + _assertCanScheduleViaTimelockedGovernance(proposalId, true); + _scheduleProposalViaTimelockedGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -49,30 +47,31 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanExecute(proposalId, true); _executeProposal(proposalId); - _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); } // --- // ACT 2. 😱 DAO IS UNDER ATTACK // --- uint256 maliciousProposalId; - EmergencyState memory emergencyState; + EmergencyProtection.Context memory emergencyState; { // Malicious vote was proposed by the attacker with huge LDO wad (but still not the majority) - ExecutorCall[] memory maliciousCalls = - ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())); + ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ); - maliciousProposalId = _submitProposal(_singleGovernance, "Rug Pool attempt", maliciousCalls); + maliciousProposalId = _submitProposalViaTimelockedGovernance("Rug Pool attempt", maliciousCalls); // the call isn't executable until the delay has passed _assertProposalSubmitted(maliciousProposalId); - _assertCanSchedule(_singleGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); // some time required to assemble the emergency committee and activate emergency mode - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); // malicious call still can't be scheduled - _assertCanSchedule(_singleGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); // emergency committee activates emergency mode vm.prank(address(_emergencyActivationCommittee)); @@ -80,25 +79,24 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency mode was successfully activated Timestamp expectedEmergencyModeEndTimestamp = _EMERGENCY_MODE_DURATION.addTo(Timestamps.now()); - emergencyState = _timelock.getEmergencyState(); - assertTrue(emergencyState.isEmergencyModeActivated); + emergencyState = _timelock.getEmergencyProtectionContext(); + + assertTrue(_timelock.isEmergencyModeActive()); assertEq(emergencyState.emergencyModeEndsAfter, expectedEmergencyModeEndTimestamp); // after the submit delay has passed, the call still may be scheduled, but executed // only the emergency committee - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_singleGovernance, maliciousProposalId, true); - _scheduleProposal(_singleGovernance, maliciousProposalId); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); _waitAfterScheduleDelayPassed(); // but the call still not executable _assertCanExecute(maliciousProposalId, false); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _executeProposal(maliciousProposalId); } @@ -114,23 +112,31 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanExecute(maliciousProposalId, false); // Dual Governance is deployed into mainnet - _deployDualGovernance(); - - ExecutorCall[] memory dualGovernanceLaunchCalls = ExecutorCallHelpers.create( - address(_timelock), + _resealManager = _deployResealManager(_timelock); + _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); + _dualGovernance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + + ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( + [address(_dualGovernance), address(_timelock), address(_timelock), address(_timelock)], [ + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), // Only Dual Governance contract can call the Timelock contract abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))), // Now the emergency mode may be deactivated (all scheduled calls will be canceled) abi.encodeCall(_timelock.deactivateEmergencyMode, ()), // Setup emergency committee for some period of time until the Dual Governance is battle tested abi.encodeCall( - _timelock.setEmergencyProtection, + _timelock.setupEmergencyProtection, ( + address(_emergencyGovernance), address(_emergencyActivationCommittee), address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION, - Durations.from(30 days) + _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + _EMERGENCY_MODE_DURATION ) ) ] @@ -138,13 +144,13 @@ contract PlanBSetup is ScenarioTestBlueprint { // The vote to launch Dual Governance is launched and reached the quorum (the major part of LDO holder still have power) uint256 dualGovernanceLunchProposalId = - _submitProposal(_singleGovernance, "Launch the Dual Governance", dualGovernanceLaunchCalls); + _submitProposalViaTimelockedGovernance("Launch the Dual Governance", dualGovernanceLaunchCalls); // wait until the after submit delay has passed _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_singleGovernance, dualGovernanceLunchProposalId, true); - _scheduleProposal(_singleGovernance, dualGovernanceLunchProposalId); + _assertCanScheduleViaTimelockedGovernance(dualGovernanceLunchProposalId, true); + _scheduleProposalViaTimelockedGovernance(dualGovernanceLunchProposalId); _assertProposalScheduled(dualGovernanceLunchProposalId); _waitAfterScheduleDelayPassed(); @@ -156,7 +162,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // TODO: check emergency protection also was applied // malicious proposal now cancelled - _assertProposalCanceled(maliciousProposalId); + _assertProposalCancelled(maliciousProposalId); } // --- @@ -174,8 +180,8 @@ contract PlanBSetup is ScenarioTestBlueprint { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _waitAfterScheduleDelayPassed(); @@ -183,7 +189,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanExecute(proposalId, true); _executeProposal(proposalId); - _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); } // --- @@ -192,21 +198,26 @@ contract PlanBSetup is ScenarioTestBlueprint { { // some time later, the major Dual Governance update release is ready to be launched _wait(Durations.from(365 days)); - DualGovernance dualGovernanceV2 = - new DualGovernance(address(_config), address(_timelock), address(_escrowMasterCopy), _ADMIN_PROPOSER); - - ExecutorCall[] memory dualGovernanceUpdateCalls = ExecutorCallHelpers.create( - address(_timelock), + DualGovernance dualGovernanceV2 = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + + ExternalCall[] memory dualGovernanceUpdateCalls = ExternalCallHelpers.create( + [address(dualGovernanceV2), address(_timelock), address(_timelock)], [ + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), // Update the controller for timelock abi.encodeCall(_timelock.setGovernance, address(dualGovernanceV2)), // Assembly the emergency committee again, until the new version of Dual Governance is battle tested abi.encodeCall( - _timelock.setEmergencyProtection, + _timelock.setupEmergencyProtection, ( + address(_emergencyGovernance), address(_emergencyActivationCommittee), address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION, + _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), Durations.from(30 days) ) ) @@ -214,12 +225,12 @@ contract PlanBSetup is ScenarioTestBlueprint { ); uint256 updateDualGovernanceProposalId = - _submitProposal(_dualGovernance, "Update Dual Governance to V2", dualGovernanceUpdateCalls); + _submitProposalViaDualGovernance("Update Dual Governance to V2", dualGovernanceUpdateCalls); _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, updateDualGovernanceProposalId, true); - _scheduleProposal(_dualGovernance, updateDualGovernanceProposalId); + _assertCanScheduleViaDualGovernance(updateDualGovernanceProposalId, true); + _scheduleProposalViaDualGovernance(updateDualGovernanceProposalId); _waitAfterScheduleDelayPassed(); @@ -233,10 +244,11 @@ contract PlanBSetup is ScenarioTestBlueprint { // - emergency protection enabled assertTrue(_timelock.isEmergencyProtectionEnabled()); - emergencyState = _timelock.getEmergencyState(); - assertEq(emergencyState.activationCommittee, address(_emergencyActivationCommittee)); - assertEq(emergencyState.executionCommittee, address(_emergencyExecutionCommittee)); - assertFalse(emergencyState.isEmergencyModeActivated); + assertFalse(_timelock.isEmergencyModeActive()); + + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + assertEq(emergencyState.emergencyActivationCommittee, address(_emergencyActivationCommittee)); + assertEq(emergencyState.emergencyExecutionCommittee, address(_emergencyExecutionCommittee)); assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); @@ -256,8 +268,8 @@ contract PlanBSetup is ScenarioTestBlueprint { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); // wait while the after schedule delay has passed @@ -269,50 +281,48 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertProposalExecuted(proposalId); // call successfully executed - _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); } } function testFork_SubmittedCallsCantBeExecutedAfterEmergencyModeDeactivation() external { - ExecutorCall[] memory maliciousCalls = - ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())); + ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ); // schedule some malicious call uint256 maliciousProposalId; { - maliciousProposalId = _submitProposal(_singleGovernance, "Rug Pool attempt", maliciousCalls); + maliciousProposalId = _submitProposalViaTimelockedGovernance("Rug Pool attempt", maliciousCalls); // malicious calls can't be executed until the delays have passed - _assertCanSchedule(_singleGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); } // activate emergency mode - EmergencyState memory emergencyState; + EmergencyProtection.Context memory emergencyState; { - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); - emergencyState = _timelock.getEmergencyState(); - assertTrue(emergencyState.isEmergencyModeActivated); + assertTrue(_timelock.isEmergencyModeActive()); } // delay for malicious proposal has passed, but it can't be executed because of emergency mode was activated { // the after submit delay has passed, and proposal can be scheduled, but not executed - _wait(_config.AFTER_SCHEDULE_DELAY() + Durations.from(1 seconds)); - _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); - _assertCanSchedule(_singleGovernance, maliciousProposalId, true); + _wait(_timelock.getAfterScheduleDelay() + Durations.from(1 seconds)); + _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); - _scheduleProposal(_singleGovernance, maliciousProposalId); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); - _wait(_config.AFTER_SCHEDULE_DELAY().plusSeconds(1)); + _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(maliciousProposalId, false); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _executeProposal(maliciousProposalId); } @@ -322,23 +332,22 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); // emergency mode still active - assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); + assertTrue(_timelock.getEmergencyProtectionContext().emergencyModeEndsAfter > Timestamps.now()); - anotherMaliciousProposalId = _submitProposal(_singleGovernance, "Another Rug Pool attempt", maliciousCalls); + anotherMaliciousProposalId = + _submitProposalViaTimelockedGovernance("Another Rug Pool attempt", maliciousCalls); // malicious calls can't be executed until the delays have passed _assertCanExecute(anotherMaliciousProposalId, false); // the after submit delay has passed, and proposal can not be executed - _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); - _assertCanSchedule(_singleGovernance, anotherMaliciousProposalId, true); + _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); + _assertCanScheduleViaTimelockedGovernance(anotherMaliciousProposalId, true); - _wait(_config.AFTER_SCHEDULE_DELAY().plusSeconds(1)); + _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(anotherMaliciousProposalId, false); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _executeProposal(anotherMaliciousProposalId); } @@ -347,14 +356,10 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); assertTrue(emergencyState.emergencyModeEndsAfter < Timestamps.now()); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _executeProposal(maliciousProposalId); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _executeProposal(anotherMaliciousProposalId); } @@ -362,20 +367,23 @@ contract PlanBSetup is ScenarioTestBlueprint { { _timelock.deactivateEmergencyMode(); - emergencyState = _timelock.getEmergencyState(); - assertFalse(emergencyState.isEmergencyModeActivated); + assertFalse(_timelock.isEmergencyModeActive()); assertFalse(_timelock.isEmergencyProtectionEnabled()); } // all malicious calls is canceled now and can't be executed { - _assertProposalCanceled(maliciousProposalId); - _assertProposalCanceled(anotherMaliciousProposalId); + _assertProposalCancelled(maliciousProposalId); + _assertProposalCancelled(anotherMaliciousProposalId); - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, maliciousProposalId)); + vm.expectRevert( + abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, maliciousProposalId) + ); _executeProposal(maliciousProposalId); - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, anotherMaliciousProposalId)); + vm.expectRevert( + abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, anotherMaliciousProposalId) + ); _executeProposal(anotherMaliciousProposalId); } } @@ -383,44 +391,45 @@ contract PlanBSetup is ScenarioTestBlueprint { function testFork_EmergencyResetGovernance() external { // deploy dual governance full setup { - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ true); - assertNotEq(_timelock.getGovernance(), _config.EMERGENCY_GOVERNANCE()); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); + assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); } // emergency committee activates emergency mode - EmergencyState memory emergencyState; { vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); - emergencyState = _timelock.getEmergencyState(); - assertTrue(emergencyState.isEmergencyModeActivated); + assertTrue(_timelock.isEmergencyModeActive()); } // before the end of the emergency mode emergency committee can reset the controller to // disable dual governance { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); + + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); _executeEmergencyReset(); - assertEq(_timelock.getGovernance(), _config.EMERGENCY_GOVERNANCE()); + assertEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); - emergencyState = _timelock.getEmergencyState(); - assertEq(emergencyState.activationCommittee, address(0)); - assertEq(emergencyState.executionCommittee, address(0)); + emergencyState = _timelock.getEmergencyProtectionContext(); + assertEq(emergencyState.emergencyActivationCommittee, address(0)); + assertEq(emergencyState.emergencyExecutionCommittee, address(0)); assertEq(emergencyState.emergencyModeDuration, Durations.ZERO); assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); - assertFalse(emergencyState.isEmergencyModeActivated); + assertFalse(_timelock.isEmergencyModeActive()); } } function testFork_ExpiredEmergencyCommitteeHasNoPower() external { // deploy dual governance full setup { - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ true); - assertNotEq(_timelock.getGovernance(), _config.EMERGENCY_GOVERNANCE()); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); + assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); } // wait till the protection duration passes @@ -428,15 +437,13 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_PROTECTION_DURATION.plusSeconds(1)); } - EmergencyState memory emergencyState = _timelock.getEmergencyState(); + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); // attempt to activate emergency protection fails { vm.expectRevert( abi.encodeWithSelector( - EmergencyProtection.EmergencyCommitteeExpired.selector, - block.timestamp, - emergencyState.protectedTill + EmergencyProtection.EmergencyProtectionExpired.selector, emergencyState.emergencyProtectionEndsAfter ) ); vm.prank(address(_emergencyActivationCommittee)); diff --git a/test/scenario/happy-path.t.sol b/test/scenario/happy-path.t.sol index 4bc1b336..0bf8ec82 100644 --- a/test/scenario/happy-path.t.sol +++ b/test/scenario/happy-path.t.sol @@ -1,27 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { - Utils, - ExecutorCall, - IDangerousContract, - ExecutorCallHelpers, - ScenarioTestBlueprint -} from "../utils/scenario-test-blueprint.sol"; -import {Proposals} from "contracts/libraries/Proposals.sol"; - -import {IAragonAgent, IAragonForwarder} from "../utils/interfaces.sol"; -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; +import {EvmScriptUtils} from "../utils/evm-script-utils.sol"; +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +import {ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; + +import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; + +import {LidoUtils, EvmScriptUtils} from "../utils/lido-utils.sol"; contract HappyPathTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; + function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } function testFork_HappyPath() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId = _submitProposal( _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls @@ -29,17 +26,17 @@ contract HappyPathTest is ScenarioTestBlueprint { _assertProposalSubmitted(proposalId); _assertSubmittedProposalData(proposalId, regularStaffCalls); - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); // the min execution delay hasn't elapsed yet - vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); + _scheduleProposalViaDualGovernance(proposalId); // wait till the first phase of timelock passes - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -47,40 +44,41 @@ contract HappyPathTest is ScenarioTestBlueprint { _assertCanExecute(proposalId, true); _executeProposal(proposalId); - _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); } function testFork_HappyPathWithMultipleItems() external { // additional phase required here, grant rights to call DAO Agent to the admin executor - Utils.grantPermission(DAO_AGENT, IAragonAgent(DAO_AGENT).RUN_SCRIPT_ROLE(), _config.ADMIN_EXECUTOR()); + _lido.grantPermission(address(_lido.agent), _lido.agent.RUN_SCRIPT_ROLE(), _timelock.getAdminExecutor()); - bytes memory agentDoRegularStaffPayload = abi.encodeCall(IDangerousContract.doRegularStaff, (42)); - bytes memory targetCallEvmScript = Utils.encodeEvmCallScript(address(_target), agentDoRegularStaffPayload); + bytes memory agentDoRegularStaffPayload = abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (42)); + bytes memory targetCallEvmScript = + EvmScriptUtils.encodeEvmCallScript(address(_targetMock), agentDoRegularStaffPayload); - ExecutorCall[] memory multipleCalls = ExecutorCallHelpers.create( - [DAO_AGENT, address(_target)], + ExternalCall[] memory multipleCalls = ExternalCallHelpers.create( + [address(_lido.agent), address(_targetMock)], [ - abi.encodeCall(IAragonForwarder.forward, (targetCallEvmScript)), - abi.encodeCall(IDangerousContract.doRegularStaff, (43)) + abi.encodeCall(_lido.agent.forward, (targetCallEvmScript)), + abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (43)) ] ); uint256 proposalId = _submitProposal(_dualGovernance, "Multiple items", multipleCalls); - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); // proposal can't be scheduled before the after submit delay has passed - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); // the min execution delay hasn't elapsed yet - vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); + _scheduleProposalViaDualGovernance(proposalId); // wait till the DG-enforced timelock elapses - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -89,12 +87,12 @@ contract HappyPathTest is ScenarioTestBlueprint { _executeProposal(proposalId); address[] memory senders = new address[](2); - senders[0] = DAO_AGENT; - senders[1] = _config.ADMIN_EXECUTOR(); + senders[0] = address(_lido.agent); + senders[1] = _timelock.getAdminExecutor(); - ExecutorCall[] memory expectedTargetCalls = ExecutorCallHelpers.create( - [DAO_AGENT, address(_target)], - [agentDoRegularStaffPayload, abi.encodeCall(IDangerousContract.doRegularStaff, (43))] + ExternalCall[] memory expectedTargetCalls = ExternalCallHelpers.create( + [address(_lido.agent), address(_targetMock)], + [agentDoRegularStaffPayload, abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (43))] ); _assertTargetMockCalls(senders, expectedTargetCalls); @@ -104,7 +102,7 @@ contract HappyPathTest is ScenarioTestBlueprint { // function test_escalation_and_one_sided_de_escalation() external { // Target target = new Target(); - // ExecutorCall[] memory calls = new ExecutorCall[](1); + // ExternalCall[] memory calls = new ExternalCall[](1); // calls[0].value = 0; // calls[0].target = address(target); // calls[0].payload = abi.encodeCall(target.doSmth, (42)); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index bf90b73f..04caff99 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -1,30 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +import {DualGovernance} from "contracts/DualGovernance.sol"; + import { - percents, - ScenarioTestBlueprint, - ExecutorCall, - ExecutorCallHelpers, - DualGovernanceState, - Durations + ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers, Durations } from "../utils/scenario-test-blueprint.sol"; -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} - contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } function testFork_LastMomentMaliciousProposal() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; _step("1. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); @@ -38,9 +31,10 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _setupStETHBalance(maliciousActor, PercentsD16.fromBasisPoints(15_00)); _step("2. MALICIOUS ACTOR STARTS ACQUIRE VETO SIGNALLING DURATION"); { - _lockStETH(maliciousActor, percents("12.0")); + _lockStETH(maliciousActor, PercentsD16.fromBasisPoints(12_00)); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -58,7 +52,9 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { maliciousProposalId = _submitProposal( _dualGovernance, "Malicious Proposal", - ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())) + ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ) ); // the both calls aren't executable until the delay has passed @@ -76,14 +72,21 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } address stEthHolders = makeAddr("STETH_WHALE"); + _setupStETHBalance( + stEthHolders, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); _step("5. STETH HOLDERS ACQUIRING QUORUM TO VETO MALICIOUS PROPOSAL"); { - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2)); - _lockStETH(stEthHolders, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT() + 1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2)); + _lockStETH( + stEthHolders, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2).plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); } @@ -91,13 +94,15 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("6. MALICIOUS PROPOSAL CAN'T BE EXECUTED IN THE VETO COOLDOWN STATE"); { // the regular proposal can be executed - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _waitAfterScheduleDelayPassed(); _executeProposal(proposalId); _assertProposalExecuted(proposalId); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, maliciousProposalId) + ); this.scheduleProposalExternal(maliciousProposalId); _assertProposalSubmitted(maliciousProposalId); @@ -105,28 +110,31 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("7. NEW VETO SIGNALLING ROUND FOR MALICIOUS PROPOSAL IS STARTED"); { - _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); // the second seal rage quit support is reached - _lockStETH(stEthHolders, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _setupStETHBalance(stEthHolders, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(stEthHolders, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); _logVetoSignallingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _logVetoSignallingState(); _activateNextState(); _assertRageQuitState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, maliciousProposalId) + ); this.scheduleProposalExternal(maliciousProposalId); } } function testFork_VetoSignallingDeactivationDefaultDuration() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; // --- @@ -146,12 +154,15 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // --- address maliciousActor = makeAddr("MALICIOUS_ACTOR"); { - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); - - _lockStETH(maliciousActor, percents("12.0")); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + _setupStETHBalance( + maliciousActor, + _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + _lockStETH(maliciousActor, PercentsD16.fromBasisPoints(12_00)); _assertVetoSignalingState(); - _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); _assertProposalSubmitted(proposalId); @@ -166,13 +177,13 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "VETO_SIGNALLING_DEACTIVATION_MAX_DURATION" DAYS // --- { - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -180,21 +191,28 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertCanExecute(proposalId, true); _executeProposal(proposalId); - _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); } } function testFork_VetoSignallingToNormalState() external { address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _setupStETHBalance( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); _step("2. MALICIOUS ACTOR LOCKS FIRST SEAL THRESHOLD TO ACTIVATE VETO SIGNALLING BEFORE PROPOSAL SUBMISSION"); { - _lockStETH(maliciousActor, percents("3.50")); + _lockStETH( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingState(); _logVetoSignallingState(); } uint256 proposalId; - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); _step("2. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); { proposalId = _submitProposal( @@ -207,30 +225,30 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); this.scheduleProposalExternal(proposalId); } _step("4. AFTER THE VETO COOLDOWN GOVERNANCE TRANSITIONS INTO NORMAL STATE"); { _unlockStETH(maliciousActor); - _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); } _step("5. PROPOSAL EXECUTABLE IN THE NORMAL STATE"); { - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); _executeProposal(proposalId); @@ -242,13 +260,20 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { address maliciousActor = makeAddr("MALICIOUS_ACTOR"); _step("2. MALICIOUS ACTOR LOCKS FIRST SEAL THRESHOLD TO ACTIVATE VETO SIGNALLING BEFORE PROPOSAL SUBMISSION"); { - _lockStETH(maliciousActor, percents("3.50")); + _setupStETHBalance( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + _lockStETH( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingState(); _logVetoSignallingState(); } uint256 proposalId; - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); _step("2. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); { proposalId = _submitProposal( @@ -261,42 +286,42 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); this.scheduleProposalExternal(proposalId); } _step("4. AFTER THE VETO COOLDOWN NEW VETO SIGNALLING ROUND STARTED"); { - _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); this.scheduleProposalExternal(proposalId); } _step("5. PROPOSAL EXECUTABLE IN THE NEXT VETO COOLDOWN"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().multipliedBy(2)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().multipliedBy(2)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); _executeProposal(proposalId); @@ -305,6 +330,6 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } function scheduleProposalExternal(uint256 proposalId) external { - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); } } diff --git a/test/scenario/proposal-deployment-modes.t.sol b/test/scenario/proposal-deployment-modes.t.sol new file mode 100644 index 00000000..93d95e69 --- /dev/null +++ b/test/scenario/proposal-deployment-modes.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; + +import {ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; + +contract ProposalDeploymentModesTest is ScenarioTestBlueprint { + function test_regular_deployment_mode() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + + (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); + _scheduleProposalViaDualGovernance(proposalId); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); + + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(proposalId, true); + _executeProposal(proposalId); + + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); + } + + function test_protected_deployment_mode_execute_after_timelock() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); + + (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); + _scheduleProposalViaDualGovernance(proposalId); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); + + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(proposalId, true); + _executeProposal(proposalId); + + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); + } + + function test_protected_deployment_mode_execute_in_emergency_mode() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); + + (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); + _scheduleProposalViaDualGovernance(proposalId); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); + + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(proposalId, true); + + vm.prank(address(_emergencyActivationCommittee)); + _timelock.activateEmergencyMode(); + + assertEq(_timelock.isEmergencyModeActive(), true); + + _assertCanExecute(proposalId, false); + + vm.prank(address(_emergencyExecutionCommittee)); + _timelock.emergencyExecute(proposalId); + + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); + } + + function test_protected_deployment_mode_deactivation_in_emergency_mode() external { + _deployDualGovernanceSetup(true); + + (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); + _scheduleProposalViaDualGovernance(proposalId); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); + + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(proposalId, true); + + vm.prank(address(_emergencyActivationCommittee)); + _timelock.activateEmergencyMode(); + + assertEq(_timelock.isEmergencyModeActive(), true); + assertEq(_timelock.isEmergencyProtectionEnabled(), true); + _assertCanExecute(proposalId, false); + + // emergency protection disabled after emergency mode is activated + + _wait(_timelock.getEmergencyProtectionContext().emergencyModeDuration.plusSeconds(1)); + + assertEq(_timelock.isEmergencyModeActive(), true); + assertEq(_timelock.isEmergencyProtectionEnabled(), true); + + _timelock.deactivateEmergencyMode(); + + assertEq(_timelock.isEmergencyModeActive(), false); + assertEq(_timelock.isEmergencyProtectionEnabled(), false); + _assertCanExecute(proposalId, false); + } + + function _createAndAssertProposal() internal returns (uint256, ExternalCall[] memory) { + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); + + uint256 proposalId = _submitProposal( + _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls + ); + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, regularStaffCalls); + + return (proposalId, regularStaffCalls); + } +} diff --git a/test/scenario/reseal-committee.t.sol b/test/scenario/reseal-committee.t.sol new file mode 100644 index 00000000..15414182 --- /dev/null +++ b/test/scenario/reseal-committee.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {DualGovernance} from "contracts/DualGovernance.sol"; +import {ResealManager} from "contracts/ResealManager.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; + +import {ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; +import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; + +contract ResealCommitteeTest is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); + _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(10_00)); + _lockStETH(_VETOER, 1 ether); + } + + function test_reseal_committees_happy_path() external { + uint256 quorum; + uint256 support; + bool isExecuted; + + address[] memory members; + + address sealable = address(_lido.withdrawalQueue); + + vm.prank(DAO_AGENT); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(this) + ); + + // Reseal + members = _resealCommittee.getMembers(); + for (uint256 i = 0; i < _resealCommittee.quorum() - 1; i++) { + vm.prank(members[i]); + _resealCommittee.voteReseal(sealable, true); + (support, quorum, isExecuted) = _resealCommittee.getResealState(sealable); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _resealCommittee.voteReseal(sealable, true); + (support, quorum, isExecuted) = _resealCommittee.getResealState(sealable); + assert(support == quorum); + assert(isExecuted == false); + + _assertNormalState(); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ResealIsNotAllowedInNormalState.selector)); + _resealCommittee.executeReseal(sealable); + + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, 1 gwei); + _assertVetoSignalingState(); + + assertEq(_lido.withdrawalQueue.isPaused(), false); + vm.expectRevert(abi.encodeWithSelector(ResealManager.SealableWrongPauseState.selector)); + _resealCommittee.executeReseal(sealable); + + _lido.withdrawalQueue.pauseFor(3600 * 24 * 6); + assertEq(_lido.withdrawalQueue.isPaused(), true); + + _resealCommittee.executeReseal(sealable); + } +} diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebreaker.t.sol similarity index 50% rename from test/scenario/tiebraker.t.sol rename to test/scenario/tiebreaker.t.sol index 4cae4b0e..54228184 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -1,22 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { - ScenarioTestBlueprint, percents, ExecutorCall, ExecutorCallHelpers -} from "../utils/scenario-test-blueprint.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; - -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; +import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; contract TiebreakerScenarioTest is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); uint256 public constant PAUSE_INFINITELY = type(uint256).max; function setUp() external { - _selectFork(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - _depositStETH(_VETOER, 1 ether); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _setupStETHBalance( + _VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); } function test_proposal_approval() external { @@ -28,15 +26,15 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { // Tiebreak activation _assertNormalState(); - _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _lockStETH(_VETOER, 1 gwei); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); - _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); + _wait(_dualGovernance.getTiebreakerState().tiebreakerActivationTimeout); _activateNextState(); - ExecutorCall[] memory proposalCalls = ExecutorCallHelpers.create(address(0), new bytes(0)); + ExternalCall[] memory proposalCalls = ExternalCallHelpers.create(address(0), new bytes(0)); uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); // Tiebreaker subcommittee 0 @@ -45,19 +43,19 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { vm.prank(members[i]); _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); + assertTrue(support < quorum); + assertFalse(isExecuted); } vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); + assertEq(support, quorum); + assertFalse(isExecuted); _tiebreakerSubCommittees[0].executeScheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); - assert(support < quorum); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); + assertTrue(support < quorum); // Tiebreaker subcommittee 1 members = _tiebreakerSubCommittees[1].getMembers(); @@ -65,25 +63,25 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { vm.prank(members[i]); _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); + assertTrue(support < quorum); + assertEq(isExecuted, false); } vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); + assertEq(support, quorum); + assertFalse(isExecuted); // Approve proposal for scheduling _tiebreakerSubCommittees[1].executeScheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); - assert(support == quorum); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); + assertEq(support, quorum); // Waiting for submit delay pass - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(Durations.from(_tiebreakerCoreCommittee.timelockDuration())); - _tiebreakerCommittee.executeScheduleProposal(proposalIdToExecute); + _tiebreakerCoreCommittee.executeScheduleProposal(proposalIdToExecute); } function test_resume_withdrawals() external { @@ -93,72 +91,79 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { address[] memory members; - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(DAO_AGENT) + vm.prank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_lido.agent) ); - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.pauseFor(type(uint256).max); - assertEq(_WITHDRAWAL_QUEUE.isPaused(), true); + vm.prank(address(_lido.agent)); + _lido.withdrawalQueue.pauseFor(type(uint256).max); + assertEq(_lido.withdrawalQueue.isPaused(), true); // Tiebreak activation _assertNormalState(); - _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _lockStETH(_VETOER, 1 gwei); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); - _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); + _wait(_dualGovernance.getTiebreakerState().tiebreakerActivationTimeout); _activateNextState(); // Tiebreaker subcommittee 0 members = _tiebreakerSubCommittees[0].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].sealableResume(address(_lido.withdrawalQueue)); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support < quorum); - assert(isExecuted == false); + _tiebreakerSubCommittees[0].getSealableResumeState(address(_lido.withdrawalQueue)); + assertTrue(support < quorum); + assertFalse(isExecuted); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support == quorum); - assert(isExecuted == false); - - _tiebreakerSubCommittees[0].executeSealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( - address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + _tiebreakerSubCommittees[0].sealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].getSealableResumeState(address(_lido.withdrawalQueue)); + assertEq(support, quorum); + assertFalse(isExecuted); + + _tiebreakerSubCommittees[0].executeSealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( + address(_lido.withdrawalQueue), + _tiebreakerCoreCommittee.getSealableResumeNonce(address(_lido.withdrawalQueue)) ); - assert(support < quorum); + assertTrue(support < quorum); // Tiebreaker subcommittee 1 members = _tiebreakerSubCommittees[1].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].sealableResume(address(_lido.withdrawalQueue)); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support < quorum); - assert(isExecuted == false); + _tiebreakerSubCommittees[1].getSealableResumeState(address(_lido.withdrawalQueue)); + assertTrue(support < quorum); + assertEq(isExecuted, false); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support == quorum); - assert(isExecuted == false); - - _tiebreakerSubCommittees[1].executeSealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( - address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + _tiebreakerSubCommittees[1].sealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].getSealableResumeState(address(_lido.withdrawalQueue)); + assertEq(support, quorum); + assertFalse(isExecuted); + + _tiebreakerSubCommittees[1].executeSealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( + address(_lido.withdrawalQueue), + _tiebreakerCoreCommittee.getSealableResumeNonce(address(_lido.withdrawalQueue)) ); - assert(support == quorum); + assertEq(support, quorum); + + // Waiting for submit delay pass + _wait(Durations.from(_tiebreakerCoreCommittee.timelockDuration())); - _tiebreakerCommittee.executeSealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerCoreCommittee.executeSealableResume(address(_lido.withdrawalQueue)); - assertEq(_WITHDRAWAL_QUEUE.isPaused(), false); + assertEq(_lido.withdrawalQueue.isPaused(), false); } } diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol new file mode 100644 index 00000000..5008c8ff --- /dev/null +++ b/test/scenario/timelocked-governance.t.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration, Durations} from "contracts/types/Duration.sol"; + +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; +import {ExternalCall} from "contracts/libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; + +import {ScenarioTestBlueprint, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +contract TimelockedGovernanceScenario is ScenarioTestBlueprint { + function setUp() external { + _deployTimelockedGovernanceSetup({isEmergencyProtectionEnabled: true}); + } + + function test_operatesAsDefault() external { + // --- + // Act 1. DAO operates as usually. Emergency protection is enabled. + // --- + { + _daoRegularOperations(_timelockedGovernance); + } + + // --- + // Act 2. Timeskip. Emergency protection is about to be expired. + // --- + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + { + assertEq(_timelock.isEmergencyProtectionEnabled(), true); + Duration emergencyProtectionDuration = + Durations.from(emergencyState.emergencyProtectionEndsAfter.toSeconds() - block.timestamp); + _wait(emergencyProtectionDuration.plusSeconds(1)); + assertEq(_timelock.isEmergencyProtectionEnabled(), false); + } + + // --- + // Act 3. Emergency committee has no more power to stop proposal flow. + // + { + vm.prank(address(emergencyState.emergencyActivationCommittee)); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.EmergencyProtectionExpired.selector, + emergencyState.emergencyProtectionEndsAfter.toSeconds() + ) + ); + _timelock.activateEmergencyMode(); + + assertFalse(_timelock.isEmergencyModeActive()); + assertFalse(_timelock.isEmergencyProtectionEnabled()); + } + + // --- + // Act 4. DAO operates as usually. Emergency protection is disabled. + // + { + _daoRegularOperations(_timelockedGovernance); + } + } + + function test_protectionAgainstCapture_cancelProposal() external { + // --- + // Act 1. DAO operates as usually. Emergency protection is enabled. + // --- + { + _daoRegularOperations(_timelockedGovernance); + } + + // --- + // Act 2. Someone creates a malicious proposal. + // --- + (uint256 maliciousProposalId,) = _submitAndAssertMaliciousProposal(); + { + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); + } + + // --- + // Act 3. Emergency committee activates emergency mode. + // --- + { + vm.prank(address(_emergencyActivationCommittee)); + _timelock.activateEmergencyMode(); + + assertTrue(_timelock.isEmergencyModeActive()); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); + + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(maliciousProposalId, false); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + _executeProposal(maliciousProposalId); + } + + // --- + // Act 4. DAO decides to cancel all pending proposals and deactivate emergency mode. + // --- + { + ExternalCall[] memory deactivateEmergencyModeCall = ExternalCallHelpers.create( + [address(_timelock)], [abi.encodeCall(_timelock.deactivateEmergencyMode, ())] + ); + uint256 deactivateEmergencyModeProposalId = + _submitProposal(_timelockedGovernance, "DAO deactivates emergency mode", deactivateEmergencyModeCall); + + _waitAfterSubmitDelayPassed(); + + _assertCanScheduleViaTimelockedGovernance(deactivateEmergencyModeProposalId, true); + _scheduleProposalViaTimelockedGovernance(deactivateEmergencyModeProposalId); + _assertProposalScheduled(deactivateEmergencyModeProposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(deactivateEmergencyModeProposalId, false); + _executeEmergencyExecute(deactivateEmergencyModeProposalId); + + assertFalse(_timelock.isEmergencyModeActive()); + assertFalse(_timelock.isEmergencyProtectionEnabled()); + + _timelock.getProposal(maliciousProposalId); + _assertProposalCancelled(maliciousProposalId); + } + + // --- + // Act 4. DAO operates as usually. Emergency protection is disabled. + // + { + _daoRegularOperations(_timelockedGovernance); + } + } + + function test_protectionAgainstCapture_stakersQuit() external { + // --- + // Act 1. DAO operates as usually. Emergency protection is enabled. + // --- + { + _daoRegularOperations(_timelockedGovernance); + } + + // --- + // Act 2. Someone creates a malicious proposal. + // --- + (uint256 maliciousProposalId,) = _submitAndAssertMaliciousProposal(); + { + _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); + } + + // --- + // Act 3. Emergency committee activates emergency mode. + // --- + { + vm.prank(address(_emergencyActivationCommittee)); + _timelock.activateEmergencyMode(); + + assertTrue(_timelock.isEmergencyModeActive()); + + _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); + + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(maliciousProposalId, false); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + _executeProposal(maliciousProposalId); + } + + // --- + // Act 4. DAO decides to not deactivate emergency mode and allow stakers to quit. + // --- + { + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + assertTrue(_timelock.isEmergencyModeActive()); + + _wait( + Durations.from(emergencyState.emergencyModeEndsAfter.toSeconds()).minusSeconds(block.timestamp) + .plusSeconds(2) + ); + _timelock.deactivateEmergencyMode(); + + assertFalse(_timelock.isEmergencyModeActive()); + } + + // --- + // Act 5. DAO operates as usually. Emergency protection is disabled. + // + { + _daoRegularOperations(_timelockedGovernance); + } + } + + function test_timelockedGovernance_upgradeTo_dualGovernance_andBack() external { + // --- + // Act 1. DAO operates as usually. Emergency protection is enabled. + // --- + { + _daoRegularOperations(_timelockedGovernance); + } + + // --- + // Act 2. DAO decides to upgrade system to dual governance. + // --- + { + _resealManager = _deployResealManager(_timelock); + _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); + _dualGovernance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + + ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( + [address(_dualGovernance), address(_timelock)], + [ + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), + abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))) + ] + ); + + uint256 dualGovernanceLunchProposalId = + _submitProposal(_timelockedGovernance, "Launch the Dual Governance", dualGovernanceLaunchCalls); + + _waitAfterSubmitDelayPassed(); + + _assertCanScheduleViaTimelockedGovernance(dualGovernanceLunchProposalId, true); + _scheduleProposalViaTimelockedGovernance(dualGovernanceLunchProposalId); + _assertProposalScheduled(dualGovernanceLunchProposalId); + + _waitAfterScheduleDelayPassed(); + + _executeProposal(dualGovernanceLunchProposalId); + + assertEq(_timelock.getGovernance(), address(_dualGovernance)); + } + + // --- + // Act 3. DAO operates as usually. Emergency protection is enabled. + // --- + { + _daoRegularOperations(_dualGovernance); + } + + // --- + // Act 4. Someone finds a bug in dual governance. Emergency committee decides to activate emergency mode and DAO decides to downgrade system to single governance. + // --- + { + vm.prank(address(_emergencyActivationCommittee)); + _timelock.activateEmergencyMode(); + + assertTrue(_timelock.isEmergencyModeActive()); + + ExternalCall[] memory timelockedGovernanceLaunchCalls = ExternalCallHelpers.create( + address(_timelock), + [ + abi.encodeCall(_timelock.setGovernance, (address(_timelockedGovernance))), + abi.encodeCall(_timelock.deactivateEmergencyMode, ()) + ] + ); + + uint256 timelockedGovernanceLunchProposalId = + _submitProposal(_dualGovernance, "Launch the Timelocked Governance", timelockedGovernanceLaunchCalls); + + _waitAfterSubmitDelayPassed(); + + _assertCanScheduleViaDualGovernance(timelockedGovernanceLunchProposalId, true); + _scheduleProposalViaDualGovernance(timelockedGovernanceLunchProposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(timelockedGovernanceLunchProposalId, false); + _executeEmergencyExecute(timelockedGovernanceLunchProposalId); + + assertEq(_timelock.getGovernance(), address(_timelockedGovernance)); + } + + // --- + // Act 5. DAO operates as usually. Emergency protection is enabled. + // --- + { + _daoRegularOperations(_timelockedGovernance); + } + } + + function _submitAndAssertProposal(IGovernance governance) internal returns (uint256, ExternalCall[] memory) { + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); + uint256 proposalId = + _submitProposal(governance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls); + + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, regularStaffCalls); + + return (proposalId, regularStaffCalls); + } + + function _submitAndAssertMaliciousProposal() internal returns (uint256, ExternalCall[] memory) { + ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ); + + uint256 proposalId = _submitProposal( + _timelockedGovernance, "DAO does malicious staff on potentially dangerous contract", maliciousCalls + ); + + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, maliciousCalls); + + return (proposalId, maliciousCalls); + } + + function _daoRegularOperations(IGovernance governance) internal { + (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _submitAndAssertProposal(governance); + + _waitAfterSubmitDelayPassed(); + + _assertCanSchedule(governance, proposalId, true); + _scheduleProposal(governance, proposalId); + _assertProposalScheduled(proposalId); + + _waitAfterScheduleDelayPassed(); + + _assertCanExecute(proposalId, true); + _executeProposal(proposalId); + + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); + } +} diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index b44837e0..9c19318a 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -1,30 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { - Escrow, - percents, - ExecutorCall, - ExecutorCallHelpers, - DualGovernanceState, - ScenarioTestBlueprint -} from "../utils/scenario-test-blueprint.sol"; - -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +import {LidoUtils} from "../utils/lido-utils.sol"; +import {Escrow, ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; + function setUp() external { - _selectFork(); - _deployTarget(); _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } function testFork_ProposalSubmittedInRageQuitNonExecutableInTheNextVetoCooldown() external { - ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; _step("1. THE PROPOSAL IS SUBMITTED"); @@ -33,18 +26,22 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls ); - _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertSubmittedProposalData(proposalId, _timelock.getAdminExecutor(), regularStaffCalls); _assertCanSchedule(_dualGovernance, proposalId, false); } - uint256 vetoedStETHAmount; address vetoer = makeAddr("MALICIOUS_ACTOR"); + _setupStETHBalance( + vetoer, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); _step("2. THE SECOND SEAL RAGE QUIT SUPPORT IS ACQUIRED"); { - vetoedStETHAmount = _lockStETH(vetoer, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT() + 1)); + _lockStETH( + vetoer, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); } @@ -57,7 +54,9 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { anotherProposalId = _submitProposal( _dualGovernance, "Another Proposal", - ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())) + ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ) ); } @@ -65,21 +64,20 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { { // request withdrawals batches Escrow rageQuitEscrow = _getRageQuitEscrow(); - uint256 requestAmount = _WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); - uint256 maxRequestsCount = vetoedStETHAmount / requestAmount + 1; while (!rageQuitEscrow.isWithdrawalsBatchesFinalized()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } - vm.deal(address(_WITHDRAWAL_QUEUE), 2 * vetoedStETHAmount); - _finalizeWQ(); + _lido.finalizeWithdrawalQueue(); - while (!rageQuitEscrow.isWithdrawalsClaimed()) { + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(128); } - _wait(_config.RAGE_QUIT_EXTENSION_DELAY().plusSeconds(1)); + rageQuitEscrow.startRageQuitExtensionDelay(); + + _wait(_dualGovernanceConfigProvider.RAGE_QUIT_EXTENSION_DELAY().plusSeconds(1)); assertTrue(rageQuitEscrow.isRageQuitFinalized()); } @@ -97,7 +95,9 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, anotherProposalId) + ); this.scheduleProposalExternal(anotherProposalId); } } @@ -105,20 +105,4 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { function scheduleProposalExternal(uint256 proposalId) external { _scheduleProposal(_dualGovernance, proposalId); } - - function _finalizeWQ() internal { - uint256 lastRequestId = _WITHDRAWAL_QUEUE.getLastRequestId(); - _finalizeWQ(lastRequestId); - } - - function _finalizeWQ(uint256 id) internal { - uint256 finalizationShareRate = _ST_ETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate - address lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; - vm.prank(lido); - _WITHDRAWAL_QUEUE.finalize(id, finalizationShareRate); - - bytes32 LOCKED_ETHER_AMOUNT_POSITION = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); - - vm.store(address(_WITHDRAWAL_QUEUE), LOCKED_ETHER_AMOUNT_POSITION, bytes32(address(_WITHDRAWAL_QUEUE).balance)); - } } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 3aa063ab..69b87217 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -4,22 +4,22 @@ pragma solidity 0.8.26; import {Vm} from "forge-std/Test.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; + +import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; + +import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; + import {Executor} from "contracts/Executor.sol"; -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -import {IConfiguration, Configuration} from "contracts/Configuration.sol"; -import {ConfigurationProvider} from "contracts/ConfigurationProvider.sol"; -import {Executor} from "contracts/Executor.sol"; -import {Proposal, Proposals, ExecutorCall, Status} from "contracts/libraries/Proposals.sol"; -import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; +import {EmergencyProtectedTimelock, TimelockState} from "contracts/EmergencyProtectedTimelock.sol"; -import {UnitTest, Duration, Timestamp, Timestamps, Durations, console} from "test/utils/unit-test.sol"; -import {TargetMock} from "test/utils/utils.sol"; -import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; -import {IDangerousContract} from "test/utils/interfaces.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; +import {TargetMock} from "test/utils/target-mock.sol"; +import {ExternalCall} from "test/utils/executor-calls.sol"; contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyProtectedTimelock private _timelock; - Configuration private _config; TargetMock private _targetMock; Executor private _executor; @@ -34,17 +34,23 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function setUp() external { _executor = new Executor(address(this)); - _config = new Configuration(address(_executor), _emergencyGovernance, new address[](0)); - _timelock = new EmergencyProtectedTimelock(address(_config)); + _adminExecutor = address(_executor); + + _timelock = _deployEmergencyProtectedTimelock(); + _targetMock = new TargetMock(); _executor.transferOwnership(address(_timelock)); - _adminExecutor = address(_executor); vm.startPrank(_adminExecutor); _timelock.setGovernance(_dualGovernance); - _timelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + _timelock.setDelays({afterSubmitDelay: Durations.from(3 days), afterScheduleDelay: Durations.from(2 days)}); + _timelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); vm.stopPrank(); } @@ -55,21 +61,19 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _dualGovernance); vm.prank(stranger); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) - ); - _timelock.submit(_adminExecutor, new ExecutorCall[](0)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, [stranger])); + _timelock.submit(_adminExecutor, new ExternalCall[](0)); assertEq(_timelock.getProposalsCount(), 0); } function test_governance_can_submit_proposal() external { vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Submitted); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); } // EmergencyProtectedTimelock.schedule() @@ -79,12 +83,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Scheduled); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Scheduled); } function testFuzz_stranger_cannot_schedule_proposal(address stranger) external { @@ -94,13 +98,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _submitProposal(); vm.prank(stranger); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) - ); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, [stranger])); + _timelock.schedule(1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Submitted); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); } // EmergencyProtectedTimelock.execute() @@ -112,17 +115,17 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); vm.prank(stranger); _timelock.execute(1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Executed); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Executed); } function test_cannot_execute_proposal_if_emergency_mode_active() external { @@ -130,20 +133,18 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); _activateEmergencyMode(); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); _timelock.execute(1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Scheduled); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Scheduled); } // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() @@ -154,15 +155,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 2); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); - Proposal memory proposal1 = _timelock.getProposal(1); - Proposal memory proposal2 = _timelock.getProposal(2); + ITimelock.Proposal memory proposal1 = _timelock.getProposal(1); + ITimelock.Proposal memory proposal2 = _timelock.getProposal(2); - assert(proposal1.status == Status.Scheduled); - assert(proposal2.status == Status.Submitted); + assertEq(proposal1.status, ProposalStatus.Scheduled); + assertEq(proposal2.status, ProposalStatus.Submitted); vm.prank(_dualGovernance); _timelock.cancelAllNonExecutedProposals(); @@ -171,8 +172,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { proposal2 = _timelock.getProposal(2); assertEq(_timelock.getProposalsCount(), 2); - assert(proposal1.status == Status.Cancelled); - assert(proposal2.status == Status.Cancelled); + assertEq(proposal1.status, ProposalStatus.Cancelled); + assertEq(proposal2.status, ProposalStatus.Cancelled); } function testFuzz_stranger_cannot_cancel_all_non_executed_proposals(address stranger) external { @@ -180,9 +181,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != address(0)); vm.prank(stranger); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) - ); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, [stranger])); + _timelock.cancelAllNonExecutedProposals(); } @@ -210,7 +210,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); } @@ -221,7 +221,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(newGovernance != address(0)); vm.expectEmit(address(_timelock)); - emit EmergencyProtectedTimelock.GovernanceSet(newGovernance); + emit TimelockState.GovernanceSet(newGovernance); vm.recordLogs(); vm.prank(_adminExecutor); @@ -236,24 +236,25 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_cannot_set_governance_to_zero() external { vm.prank(_adminExecutor); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, address(0))); + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, address(0))); _timelock.setGovernance(address(0)); } - function test_cannot_set_governance_to_the_same_address() external { - address currentGovernance = _timelock.getGovernance(); - vm.prank(_adminExecutor); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, _dualGovernance)); - _timelock.setGovernance(currentGovernance); + // TODO: Update test after the convention about return/revert is resolved + // function test_cannot_set_governance_to_the_same_address() external { + // address currentGovernance = _timelock.getGovernance(); + // vm.prank(_adminExecutor); + // vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, _dualGovernance)); + // _timelock.setGovernance(currentGovernance); - assertEq(_timelock.getGovernance(), currentGovernance); - } + // assertEq(_timelock.getGovernance(), currentGovernance); + // } function testFuzz_stranger_cannot_set_governance(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); _timelock.setGovernance(makeAddr("newGovernance")); } @@ -271,7 +272,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != address(0)); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, stranger)); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyActivationCommittee.selector, stranger) + ); _timelock.activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), false); @@ -283,9 +286,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(_emergencyActivator); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); _timelock.activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); @@ -298,11 +299,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); _activateEmergencyMode(); @@ -311,29 +312,26 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyEnactor); _timelock.emergencyExecute(1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Executed); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Executed); } function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { vm.startPrank(_dualGovernance); - _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _timelock.schedule(1); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); vm.stopPrank(); - EmergencyState memory state = _timelock.getEmergencyState(); - assertEq(state.isEmergencyModeActivated, false); + assertEq(_timelock.isEmergencyModeActive(), false); vm.prank(_emergencyActivator); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); _timelock.emergencyExecute(1); } @@ -345,24 +343,27 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); _activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyExecutionCommittee.selector, stranger) + ); _timelock.emergencyExecute(1); } // EmergencyProtectedTimelock.deactivateEmergencyMode() function test_admin_executor_can_deactivate_emergency_mode_if_delay_not_passed() external { + _submitProposal(); _activateEmergencyMode(); vm.prank(_adminExecutor); @@ -376,15 +377,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Submitted); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); _activateEmergencyMode(); _deactivateEmergencyMode(); proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Cancelled); + assertEq(proposal.status, ProposalStatus.Cancelled); } function testFuzz_stranger_can_deactivate_emergency_mode_if_passed(address stranger) external { @@ -392,7 +393,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _activateEmergencyMode(); - EmergencyState memory state = _timelock.getEmergencyState(); + EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); assertEq(_isEmergencyStateActivated(), true); _wait(state.emergencyModeDuration.plusSeconds(1)); @@ -400,7 +401,6 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); _timelock.deactivateEmergencyMode(); - state = _timelock.getEmergencyState(); assertEq(_isEmergencyStateActivated(), false); } @@ -408,15 +408,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); _timelock.deactivateEmergencyMode(); vm.prank(_adminExecutor); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); _timelock.deactivateEmergencyMode(); } @@ -427,7 +423,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); _timelock.deactivateEmergencyMode(); } @@ -441,15 +437,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyEnactor); _timelock.emergencyReset(); - EmergencyState memory newState = _timelock.getEmergencyState(); + EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); assertEq(_isEmergencyStateActivated(), false); assertEq(_timelock.getGovernance(), _emergencyGovernance); assertEq(_timelock.isEmergencyProtectionEnabled(), false); - assertEq(newState.activationCommittee, address(0)); - assertEq(newState.executionCommittee, address(0)); - assertEq(newState.protectedTill, Timestamps.ZERO); + assertEq(newState.emergencyActivationCommittee, address(0)); + assertEq(newState.emergencyExecutionCommittee, address(0)); + assertEq(newState.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(newState.emergencyModeDuration, Durations.ZERO); assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -458,14 +454,14 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _submitProposal(); _activateEmergencyMode(); - Proposal memory proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Submitted); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); vm.prank(_emergencyEnactor); _timelock.emergencyReset(); proposal = _timelock.getProposal(1); - assert(proposal.status == Status.Cancelled); + assertEq(proposal.status, ProposalStatus.Cancelled); } function testFuzz_stranger_cannot_emergency_reset_governance(address stranger) external { @@ -477,7 +473,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyExecutionCommittee.selector, stranger) + ); _timelock.emergencyReset(); assertEq(_isEmergencyStateActivated(), true); @@ -486,76 +484,86 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_cannot_emergency_reset_if_emergency_mode_not_activated() external { assertEq(_isEmergencyStateActivated(), false); - EmergencyState memory state = _timelock.getEmergencyState(); + EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) - ); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); vm.prank(_emergencyEnactor); _timelock.emergencyReset(); - EmergencyState memory newState = _timelock.getEmergencyState(); + EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); - assertEq(newState.executionCommittee, state.executionCommittee); - assertEq(newState.activationCommittee, state.activationCommittee); - assertEq(newState.protectedTill, state.protectedTill); + assertEq(newState.emergencyExecutionCommittee, state.emergencyExecutionCommittee); + assertEq(newState.emergencyActivationCommittee, state.emergencyActivationCommittee); + assertEq(newState.emergencyProtectionEndsAfter, state.emergencyProtectionEndsAfter); assertEq(newState.emergencyModeEndsAfter, state.emergencyModeEndsAfter); assertEq(newState.emergencyModeDuration, state.emergencyModeDuration); - assertEq(newState.isEmergencyModeActivated, state.isEmergencyModeActivated); + assertFalse(_timelock.isEmergencyModeActive()); } - // EmergencyProtectedTimelock.setEmergencyProtection() + // EmergencyProtectedTimelock.setupEmergencyProtection() function test_admin_executor_can_set_emenrgency_protection() external { - EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.prank(_adminExecutor); - _localTimelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); - EmergencyState memory state = _localTimelock.getEmergencyState(); + EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); - assertEq(state.activationCommittee, _emergencyActivator); - assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); - assertEq(state.isEmergencyModeActivated, false); + assertFalse(_timelock.isEmergencyModeActive()); } function testFuzz_stranger_cannot_set_emergency_protection(address stranger) external { vm.assume(stranger != _adminExecutor); vm.assume(stranger != address(0)); - EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); - _localTimelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); - EmergencyState memory state = _localTimelock.getEmergencyState(); + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); - assertEq(state.activationCommittee, address(0)); - assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); - assertEq(state.isEmergencyModeActivated, false); + assertFalse(_localTimelock.isEmergencyModeActive()); } // EmergencyProtectedTimelock.isEmergencyProtectionEnabled() function test_is_emergency_protection_enabled_deactivate() external { - EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); vm.prank(_adminExecutor); - _localTimelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); @@ -572,13 +580,17 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { } function test_is_emergency_protection_enabled_reset() external { - EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); vm.prank(_adminExecutor); - _localTimelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); @@ -594,65 +606,73 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); } - // EmergencyProtectedTimelock.getEmergencyState() + // EmergencyProtectedTimelock.getEmergencyProtectionContext() function test_get_emergency_state_deactivate() external { - EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - EmergencyState memory state = _localTimelock.getEmergencyState(); + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); - assertEq(state.isEmergencyModeActivated, false); - assertEq(state.activationCommittee, address(0)); - assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, Timestamps.ZERO); + assertFalse(_localTimelock.isEmergencyModeActive()); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); vm.prank(_adminExecutor); - _localTimelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); - state = _localTimelock.getEmergencyState(); + state = _localTimelock.getEmergencyProtectionContext(); - assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, false); - assertEq(state.activationCommittee, _emergencyActivator); - assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(_localTimelock.isEmergencyModeActive(), false); + assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); vm.prank(_emergencyActivator); _localTimelock.activateEmergencyMode(); - state = _localTimelock.getEmergencyState(); + state = _localTimelock.getEmergencyProtectionContext(); - assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, true); - assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.activationCommittee, _emergencyActivator); + assertEq(_localTimelock.isEmergencyModeActive(), true); + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(state.emergencyActivationCommittee, _emergencyActivator); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeEndsAfter, _emergencyModeDuration.addTo(Timestamps.now())); vm.prank(_adminExecutor); _localTimelock.deactivateEmergencyMode(); - state = _localTimelock.getEmergencyState(); + state = _localTimelock.getEmergencyProtectionContext(); - assertEq(state.isEmergencyModeActivated, false); - assertEq(state.activationCommittee, address(0)); - assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, Timestamps.ZERO); + assertFalse(_timelock.isEmergencyModeActive()); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); } function test_get_emergency_state_reset() external { - EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.prank(_adminExecutor); - _localTimelock.setEmergencyProtection( - _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration ); vm.prank(_emergencyActivator); @@ -661,12 +681,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyEnactor); _localTimelock.emergencyReset(); - EmergencyState memory state = _localTimelock.getEmergencyState(); + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); - assertEq(state.isEmergencyModeActivated, false); - assertEq(state.activationCommittee, address(0)); - assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, Timestamps.ZERO); + assertFalse(_timelock.isEmergencyModeActive()); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -686,11 +706,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 0); vm.startPrank(_dualGovernance); - ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(address(_targetMock)); + ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); _timelock.submit(_adminExecutor, executorCalls); _timelock.submit(_adminExecutor, executorCalls); - Proposal memory submittedProposal = _timelock.getProposal(1); + ITimelock.Proposal memory submittedProposal = _timelock.getProposal(1); Timestamp submitTimestamp = Timestamps.now(); @@ -698,47 +718,43 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(submittedProposal.executor, _adminExecutor); assertEq(submittedProposal.submittedAt, submitTimestamp); assertEq(submittedProposal.scheduledAt, Timestamps.ZERO); - assertEq(submittedProposal.executedAt, Timestamps.ZERO); - // assertEq doesn't support comparing enumerables so far - assert(submittedProposal.status == Status.Submitted); + assertEq(submittedProposal.status, ProposalStatus.Submitted); assertEq(submittedProposal.calls.length, 1); assertEq(submittedProposal.calls[0].value, executorCalls[0].value); assertEq(submittedProposal.calls[0].target, executorCalls[0].target); assertEq(submittedProposal.calls[0].payload, executorCalls[0].payload); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _timelock.schedule(1); Timestamp scheduleTimestamp = Timestamps.now(); - Proposal memory scheduledProposal = _timelock.getProposal(1); + ITimelock.Proposal memory scheduledProposal = _timelock.getProposal(1); assertEq(scheduledProposal.id, 1); assertEq(scheduledProposal.executor, _adminExecutor); assertEq(scheduledProposal.submittedAt, submitTimestamp); assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); - assertEq(scheduledProposal.executedAt, Timestamps.ZERO); - // // assertEq doesn't support comparing enumerables so far - assert(scheduledProposal.status == Status.Scheduled); + assertEq(scheduledProposal.status, ProposalStatus.Scheduled); assertEq(scheduledProposal.calls.length, 1); assertEq(scheduledProposal.calls[0].value, executorCalls[0].value); assertEq(scheduledProposal.calls[0].target, executorCalls[0].target); assertEq(scheduledProposal.calls[0].payload, executorCalls[0].payload); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); _timelock.execute(1); - Proposal memory executedProposal = _timelock.getProposal(1); + ITimelock.Proposal memory executedProposal = _timelock.getProposal(1); Timestamp executeTimestamp = Timestamps.now(); assertEq(executedProposal.id, 1); + assertEq(executedProposal.status, ProposalStatus.Executed); assertEq(executedProposal.executor, _adminExecutor); assertEq(executedProposal.submittedAt, submitTimestamp); assertEq(executedProposal.scheduledAt, scheduleTimestamp); - assertEq(executedProposal.executedAt, executeTimestamp); + // assertEq(executedProposal.executedAt, executeTimestamp); // assertEq doesn't support comparing enumerables so far - assert(executedProposal.status == Status.Executed); assertEq(executedProposal.calls.length, 1); assertEq(executedProposal.calls[0].value, executorCalls[0].value); assertEq(executedProposal.calls[0].target, executorCalls[0].target); @@ -746,15 +762,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.cancelAllNonExecutedProposals(); - Proposal memory cancelledProposal = _timelock.getProposal(2); + ITimelock.Proposal memory cancelledProposal = _timelock.getProposal(2); assertEq(cancelledProposal.id, 2); + assertEq(cancelledProposal.status, ProposalStatus.Cancelled); assertEq(cancelledProposal.executor, _adminExecutor); assertEq(cancelledProposal.submittedAt, submitTimestamp); assertEq(cancelledProposal.scheduledAt, Timestamps.ZERO); - assertEq(cancelledProposal.executedAt, Timestamps.ZERO); + // assertEq(cancelledProposal.executedAt, Timestamps.ZERO); // assertEq doesn't support comparing enumerables so far - assert(cancelledProposal.status == Status.Cancelled); assertEq(cancelledProposal.calls.length, 1); assertEq(cancelledProposal.calls[0].value, executorCalls[0].value); assertEq(cancelledProposal.calls[0].target, executorCalls[0].target); @@ -788,13 +804,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _submitProposal(); assertEq(_timelock.canExecute(1), false); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); _scheduleProposal(1); assertEq(_timelock.canExecute(1), false); - _wait(_config.AFTER_SCHEDULE_DELAY()); + _wait(_timelock.getAfterScheduleDelay()); assertEq(_timelock.canExecute(1), true); @@ -809,9 +825,10 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_can_schedule() external { assertEq(_timelock.canExecute(1), false); _submitProposal(); - assertEq(_timelock.canSchedule(1), false); - _wait(_config.AFTER_SUBMIT_DELAY()); + _wait(_timelock.getAfterSubmitDelay()); + + assertEq(_timelock.canSchedule(1), true); assertEq(_timelock.canSchedule(1), true); @@ -825,18 +842,56 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.canSchedule(1), false); } - // EmergencyProtectedTimelock.getProposalSubmissionTime() - function test_get_proposal_submission_time() external { _submitProposal(); - assertEq(_timelock.getProposalSubmissionTime(1), Timestamps.now()); + assertEq(_timelock.getProposal(1).submittedAt, Timestamps.now()); + } + + function test_getProposalInfo() external { + _submitProposal(); + + (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) = + _timelock.getProposalInfo(1); + + assertEq(id, 1); + assert(status == ProposalStatus.Submitted); + assertEq(executor, _adminExecutor); + assertEq(submittedAt, Timestamps.from(block.timestamp)); + assertEq(scheduledAt, Timestamps.from(0)); + } + + function test_getProposalCalls() external { + ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, executorCalls); + + ExternalCall[] memory calls = _timelock.getProposalCalls(1); + + assertEq(calls.length, executorCalls.length); + assertEq(calls[0].target, executorCalls[0].target); + assertEq(calls[0].value, executorCalls[0].value); + assertEq(calls[0].payload, executorCalls[0].payload); + } + + function testFuzz_getAdminExecutor(address executor) external { + EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock( + EmergencyProtectedTimelock.SanityCheckParams({ + maxAfterSubmitDelay: Durations.from(45 days), + maxAfterScheduleDelay: Durations.from(45 days), + maxEmergencyModeDuration: Durations.from(365 days), + maxEmergencyProtectionDuration: Durations.from(365 days) + }), + executor + ); + + assertEq(timelock.getAdminExecutor(), executor); } // Utils function _submitProposal() internal { vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); } function _scheduleProposal(uint256 proposalId) internal { @@ -845,8 +900,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { } function _isEmergencyStateActivated() internal view returns (bool) { - EmergencyState memory state = _timelock.getEmergencyState(); - return state.isEmergencyModeActivated; + return _timelock.isEmergencyModeActive(); } function _activateEmergencyMode() internal { @@ -860,4 +914,16 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.deactivateEmergencyMode(); assertEq(_isEmergencyStateActivated(), false); } + + function _deployEmergencyProtectedTimelock() internal returns (EmergencyProtectedTimelock) { + return new EmergencyProtectedTimelock( + EmergencyProtectedTimelock.SanityCheckParams({ + maxAfterSubmitDelay: Durations.from(45 days), + maxAfterScheduleDelay: Durations.from(45 days), + maxEmergencyModeDuration: Durations.from(365 days), + maxEmergencyProtectionDuration: Durations.from(365 days) + }), + _adminExecutor + ); + } } diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol new file mode 100644 index 00000000..c9f22039 --- /dev/null +++ b/test/unit/HashConsensus.t.sol @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {Vm} from "forge-std/Test.sol"; + +import {HashConsensus} from "../../contracts/committees/HashConsensus.sol"; +import {Duration} from "../../contracts/types/Duration.sol"; + +contract HashConsensusInstance is HashConsensus { + constructor( + address owner, + address[] memory newMembers, + uint256 executionQuorum, + uint256 timelock + ) HashConsensus(owner, timelock) { + _addMembers(newMembers, executionQuorum); + } +} + +abstract contract HashConsensusUnitTest is UnitTest { + HashConsensus internal _hashConsensus; + + address internal _owner = makeAddr("COMMITTEE_OWNER"); + + address internal _stranger = makeAddr("STRANGER"); + + uint256 internal _membersCount = 13; + uint256 internal _quorum = 7; + address[] internal _committeeMembers = new address[](_membersCount); + + constructor() { + for (uint256 i = 0; i < _membersCount; ++i) { + _committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * _membersCount + 65))); + } + } + + function test_constructorInitializesCorrectly() public { + uint256 timelock = 1; + + vm.expectEmit(); + emit Ownable.OwnershipTransferred(address(0), _owner); + vm.expectEmit(); + emit HashConsensus.TimelockDurationSet(timelock); + for (uint256 i = 0; i < _committeeMembers.length; i++) { + vm.expectEmit(); + emit HashConsensus.MemberAdded(_committeeMembers[i]); + } + vm.expectEmit(); + emit HashConsensus.QuorumSet(_quorum); + + new HashConsensusInstance(_owner, _committeeMembers, _quorum, timelock); + } + + function test_constructorRevertsWithZeroQuorum() public { + uint256 invalidQuorum = 0; + + vm.expectRevert(abi.encodeWithSelector(HashConsensus.InvalidQuorum.selector)); + new HashConsensusInstance(_owner, _committeeMembers, invalidQuorum, 1); + } + + function test_isMember() public { + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(_hashConsensus.isMember(_committeeMembers[i]), true); + } + + assertEq(_hashConsensus.isMember(_owner), false); + assertEq(_hashConsensus.isMember(_stranger), false); + } + + function test_getMembers() public { + address[] memory committeeMembers = _hashConsensus.getMembers(); + + assertEq(committeeMembers.length, _committeeMembers.length); + + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(committeeMembers[i], _committeeMembers[i]); + } + } + + function test_addMembers_stranger_call() public { + address[] memory membersToAdd = new address[](1); + membersToAdd[0] = makeAddr("NEW_MEMBER"); + assertEq(_hashConsensus.isMember(membersToAdd[0]), false); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _stranger)); + _hashConsensus.addMembers(membersToAdd, _quorum); + + for (uint256 i = 0; i < _membersCount; ++i) { + vm.prank(_committeeMembers[i]); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _committeeMembers[i])); + _hashConsensus.addMembers(membersToAdd, _quorum); + } + } + + function test_addMembers_reverts_on_duplicate() public { + address[] memory membersToAdd = new address[](1); + membersToAdd[0] = _committeeMembers[0]; + assertEq(_hashConsensus.isMember(membersToAdd[0]), true); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("DuplicatedMember(address)", membersToAdd[0])); + _hashConsensus.addMembers(membersToAdd, _quorum); + } + + function test_addMembers_reverts_on_duplicate_in_array() public { + address[] memory membersToAdd = new address[](2); + membersToAdd[0] = makeAddr("NEW_MEMBER"); + membersToAdd[1] = makeAddr("NEW_MEMBER"); + assertEq(_hashConsensus.isMember(membersToAdd[0]), false); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("DuplicatedMember(address)", membersToAdd[1])); + _hashConsensus.addMembers(membersToAdd, _quorum); + } + + function test_addMember_reverts_on_invalid_quorum() public { + address[] memory membersToAdd = new address[](1); + membersToAdd[0] = makeAddr("NEW_MEMBER"); + assertEq(_hashConsensus.isMember(membersToAdd[0]), false); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.addMembers(membersToAdd, 0); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.addMembers(membersToAdd, _membersCount + 2); + } + + function test_addMember() public { + address[] memory membersToAdd = new address[](2); + membersToAdd[0] = makeAddr("NEW_MEMBER_1"); + membersToAdd[1] = makeAddr("NEW_MEMBER_2"); + assertEq(_hashConsensus.isMember(membersToAdd[0]), false); + assertEq(_hashConsensus.isMember(membersToAdd[1]), false); + + uint256 newQuorum = _quorum + 1; + + vm.prank(_owner); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensus.MemberAdded(membersToAdd[0]); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensus.MemberAdded(membersToAdd[1]); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensus.QuorumSet(newQuorum); + _hashConsensus.addMembers(membersToAdd, newQuorum); + + assertEq(_hashConsensus.isMember(membersToAdd[0]), true); + assertEq(_hashConsensus.isMember(membersToAdd[1]), true); + + address[] memory committeeMembers = _hashConsensus.getMembers(); + + assertEq(committeeMembers.length, _membersCount + 2); + assertEq(committeeMembers[committeeMembers.length - 2], membersToAdd[0]); + assertEq(committeeMembers[committeeMembers.length - 1], membersToAdd[1]); + } + + function test_removeMembers_stranger_call() public { + address[] memory membersToRemove = new address[](1); + membersToRemove[0] = _committeeMembers[0]; + assertEq(_hashConsensus.isMember(membersToRemove[0]), true); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _stranger)); + _hashConsensus.removeMembers(membersToRemove, _quorum); + + for (uint256 i = 0; i < _membersCount; ++i) { + vm.prank(_committeeMembers[i]); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _committeeMembers[i])); + _hashConsensus.removeMembers(membersToRemove, _quorum); + } + } + + function test_removeMembers_reverts_on_member_is_not_exist() public { + address[] memory membersToRemove = new address[](1); + membersToRemove[0] = _stranger; + assertEq(_hashConsensus.isMember(membersToRemove[0]), false); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.AccountIsNotMember.selector, _stranger)); + _hashConsensus.removeMembers(membersToRemove, _quorum); + } + + function test_removeMembers_reverts_on_member_duplicate_in_array() public { + address[] memory membersToRemove = new address[](2); + membersToRemove[0] = _committeeMembers[0]; + membersToRemove[1] = _committeeMembers[0]; + assertEq(_hashConsensus.isMember(membersToRemove[0]), true); + assertEq(_hashConsensus.isMember(membersToRemove[1]), true); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.AccountIsNotMember.selector, _committeeMembers[0])); + _hashConsensus.removeMembers(membersToRemove, _quorum); + } + + function test_removeMembers_reverts_on_invalid_quorum() public { + address[] memory membersToRemove = new address[](1); + membersToRemove[0] = _committeeMembers[0]; + assertEq(_hashConsensus.isMember(membersToRemove[0]), true); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.removeMembers(membersToRemove, 0); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.removeMembers(membersToRemove, _membersCount); + } + + function test_removeMembers() public { + address[] memory membersToRemove = new address[](2); + membersToRemove[0] = _committeeMembers[0]; + membersToRemove[1] = _committeeMembers[1]; + assertEq(_hashConsensus.isMember(membersToRemove[0]), true); + assertEq(_hashConsensus.isMember(membersToRemove[1]), true); + uint256 newQuorum = _quorum - 2; + + vm.prank(_owner); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensus.MemberRemoved(membersToRemove[0]); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensus.MemberRemoved(membersToRemove[1]); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensus.QuorumSet(newQuorum); + _hashConsensus.removeMembers(membersToRemove, newQuorum); + + assertEq(_hashConsensus.isMember(membersToRemove[0]), false); + assertEq(_hashConsensus.isMember(membersToRemove[1]), false); + + address[] memory committeeMembers = _hashConsensus.getMembers(); + + assertEq(committeeMembers.length, _membersCount - 2); + for (uint256 i = 0; i < committeeMembers.length; ++i) { + assertNotEq(committeeMembers[i], membersToRemove[0]); + assertNotEq(committeeMembers[i], membersToRemove[1]); + } + } + + function test_setTimelockDurationByOwner() public { + uint256 newTimelockDuration = 200; + + vm.expectEmit(true, false, false, true); + emit HashConsensus.TimelockDurationSet(newTimelockDuration); + + vm.prank(_owner); + _hashConsensus.setTimelockDuration(newTimelockDuration); + + assertEq(_hashConsensus.timelockDuration(), newTimelockDuration); + } + + function test_setTimelockDurationRevertsIfNotOwner() public { + uint256 newTimelockDuration = 200; + + vm.prank(address(0x123)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x123))); + _hashConsensus.setTimelockDuration(newTimelockDuration); + } + + function testTimelockDurationEventEmitted() public { + uint256 newTimelockDuration = 300; + + vm.expectEmit(true, false, false, true); + emit HashConsensus.TimelockDurationSet(newTimelockDuration); + + vm.prank(_owner); + _hashConsensus.setTimelockDuration(newTimelockDuration); + } + + function test_setQuorumByOwner() public { + uint256 newQuorum = 2; + + vm.expectEmit(true, false, false, true); + emit HashConsensus.QuorumSet(newQuorum); + + vm.prank(_owner); + _hashConsensus.setQuorum(newQuorum); + + // Assert that the quorum was updated correctly + assertEq(_hashConsensus.quorum(), newQuorum); + } + + function test_setQuorumRevertsIfNotOwner() public { + uint256 newQuorum = 2; + + vm.prank(address(0x123)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x123))); + _hashConsensus.setQuorum(newQuorum); + } + + function test_setQuorumRevertsIfZeroQuorum() public { + uint256 invalidQuorum = 0; + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.setQuorum(invalidQuorum); + } + + function test_setQuorumRevertsIfQuorumExceedsMembers() public { + uint256 invalidQuorum = _committeeMembers.length + 1; + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _hashConsensus.setQuorum(invalidQuorum); + } + + function test_quorumEventEmitted() public { + uint256 newQuorum = 3; + + vm.expectEmit(true, false, false, true); + emit HashConsensus.QuorumSet(newQuorum); + + vm.prank(_owner); + _hashConsensus.setQuorum(newQuorum); + } +} + +contract Target { + event Executed(); + + function trigger() public { + emit Executed(); + } +} + +contract HashConsensusWrapper is HashConsensus { + event OnlyMemberModifierPassed(); + + Target internal _target; + + constructor( + address owner, + address[] memory newMembers, + uint256 executionQuorum, + uint256 timelock, + Target target + ) HashConsensus(owner, timelock) { + _target = target; + _addMembers(newMembers, executionQuorum); + } + + function vote(bytes32 hash, bool support) public { + _vote(hash, support); + } + + function execute(bytes32 hash) public { + _markUsed(hash); + _target.trigger(); + } + + function getHashState(bytes32 hash) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return _getHashState(hash); + } + + function getSupport(bytes32 hash) public view returns (uint256 support) { + return _getSupport(hash); + } + + function onlyMemberProtected() public { + _checkCallerIsMember(); + emit OnlyMemberModifierPassed(); + } +} + +contract HashConsensusInternalUnitTest is HashConsensusUnitTest { + HashConsensusWrapper internal _hashConsensusWrapper; + Target internal _target; + Duration internal _timelock = Duration.wrap(3600); + + bytes internal data; + bytes32 internal dataHash; + + function setUp() public { + _target = new Target(); + _hashConsensusWrapper = + new HashConsensusWrapper(_owner, _committeeMembers, _quorum, _timelock.toSeconds(), _target); + _hashConsensus = HashConsensus(_hashConsensusWrapper); + data = abi.encode(address(_target)); + dataHash = keccak256(data); + } + + function test_getSupport() public { + assertEq(_hashConsensusWrapper.getSupport(dataHash), 0); + + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(_hashConsensusWrapper.getSupport(dataHash), i); + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(dataHash, true); + assertEq(_hashConsensusWrapper.getSupport(dataHash), i + 1); + } + + assertEq(_hashConsensusWrapper.getSupport(dataHash), _membersCount); + + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(_hashConsensusWrapper.getSupport(dataHash), _membersCount - i); + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(dataHash, false); + assertEq(_hashConsensusWrapper.getSupport(dataHash), _membersCount - i - 1); + } + + assertEq(_hashConsensusWrapper.getSupport(dataHash), 0); + } + + function test_getHashState() public { + uint256 support; + uint256 execuitionQuorum; + bool isExecuted; + + (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + assertEq(support, 0); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + + for (uint256 i = 0; i < _membersCount; ++i) { + (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + assertEq(support, i); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(dataHash, true); + + (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + assertEq(support, i + 1); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + } + + (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + assertEq(support, _membersCount); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + + _wait(_timelock); + + _hashConsensusWrapper.execute(dataHash); + + (support, execuitionQuorum, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); + assertEq(support, _membersCount); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, true); + } + + function test_vote() public { + assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); + + vm.prank(_committeeMembers[0]); + vm.expectEmit(address(_hashConsensusWrapper)); + emit HashConsensus.Voted(_committeeMembers[0], dataHash, true); + _hashConsensusWrapper.vote(dataHash, true); + assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), true); + + vm.prank(_committeeMembers[0]); + vm.recordLogs(); + _hashConsensusWrapper.vote(dataHash, true); + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 0); + assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), true); + + vm.prank(_committeeMembers[0]); + vm.expectEmit(address(_hashConsensusWrapper)); + emit HashConsensus.Voted(_committeeMembers[0], dataHash, false); + _hashConsensusWrapper.vote(dataHash, false); + assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); + + vm.prank(_committeeMembers[0]); + vm.recordLogs(); + _hashConsensusWrapper.vote(dataHash, false); + logs = vm.getRecordedLogs(); + assertEq(logs.length, 0); + assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); + } + + function test_vote_reverts_on_executed() public { + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(dataHash, true); + } + + _wait(_timelock); + + _hashConsensusWrapper.execute(dataHash); + + vm.prank(_committeeMembers[0]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, dataHash)); + _hashConsensusWrapper.vote(dataHash, true); + } + + function test_execute_events() public { + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); + _hashConsensusWrapper.execute(dataHash); + + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(dataHash, true); + } + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("TimelockNotPassed()")); + _hashConsensusWrapper.execute(dataHash); + + _wait(_timelock); + vm.prank(_stranger); + vm.expectEmit(address(_hashConsensusWrapper)); + emit HashConsensus.HashUsed(dataHash); + vm.expectEmit(address(_target)); + emit Target.Executed(); + _hashConsensusWrapper.execute(dataHash); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, dataHash)); + _hashConsensusWrapper.execute(dataHash); + } + + function test_onlyMemberModifier() public { + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, _stranger)); + _hashConsensusWrapper.onlyMemberProtected(); + + vm.prank(_committeeMembers[0]); + vm.expectEmit(address(_hashConsensus)); + emit HashConsensusWrapper.OnlyMemberModifierPassed(); + _hashConsensusWrapper.onlyMemberProtected(); + } +} diff --git a/test/unit/ResealManager.t.sol b/test/unit/ResealManager.t.sol new file mode 100644 index 00000000..4e65bb97 --- /dev/null +++ b/test/unit/ResealManager.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ResealManager} from "contracts/ResealManager.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {ISealable} from "contracts/interfaces/ISealable.sol"; +import {Durations} from "contracts/types/Duration.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ResealManagerUnitTests is UnitTest { + ResealManager internal resealManager; + + address timelock = makeAddr("timelock"); + address sealable = makeAddr("sealable"); + address private governance = makeAddr("governance"); + + function setUp() external { + vm.mockCall(timelock, abi.encodeWithSelector(ITimelock.getGovernance.selector), abi.encode(governance)); + + resealManager = new ResealManager(ITimelock(timelock)); + } + + function test_resealSuccess() public { + uint256 futureTimestamp = block.timestamp + 1000; + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(futureTimestamp) + ); + + vm.expectCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); + vm.expectCall(sealable, abi.encodeWithSelector(ISealable.pauseFor.selector, type(uint256).max)); + + vm.prank(governance); + resealManager.reseal(sealable); + } + + function test_resealFailsForPastTimestamp() public { + uint256 pastTimestamp = block.timestamp; + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(pastTimestamp) + ); + + _wait(Durations.from(1)); + + vm.prank(governance); + vm.expectRevert(ResealManager.SealableWrongPauseState.selector); + resealManager.reseal(sealable); + } + + function test_resealFailsForInfinitePause() public { + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(type(uint256).max) + ); + + vm.prank(governance); + vm.expectRevert(ResealManager.SealableWrongPauseState.selector); + resealManager.reseal(sealable); + } + + function test_resumeSuccess() public { + uint256 futureTimestamp = block.timestamp + 1000; + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(futureTimestamp) + ); + + vm.expectCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); + + vm.prank(governance); + resealManager.resume(sealable); + } + + function test_resumeFailsForPastTimestamp() public { + uint256 pastTimestamp = block.timestamp; + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(pastTimestamp) + ); + + _wait(Durations.from(1)); + + vm.prank(governance); + vm.expectRevert(ResealManager.SealableWrongPauseState.selector); + resealManager.resume(sealable); + } + + function test_revertWhenSenderIsNotGovernanceOnReseal() public { + uint256 futureTimestamp = block.timestamp + 1000; + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(futureTimestamp) + ); + + vm.prank(address(0x123)); + vm.expectRevert(abi.encodeWithSelector(ResealManager.CallerIsNotGovernance.selector, address(0x123))); + resealManager.reseal(sealable); + } + + function test_revertWhenSenderIsNotGovernanceOnResume() public { + uint256 futureTimestamp = block.timestamp + 1000; + vm.mockCall( + sealable, abi.encodeWithSelector(ISealable.getResumeSinceTimestamp.selector), abi.encode(futureTimestamp) + ); + + vm.prank(address(0x123)); + vm.expectRevert(abi.encodeWithSelector(ResealManager.CallerIsNotGovernance.selector, address(0x123))); + resealManager.resume(sealable); + } +} diff --git a/test/unit/SingleGovernance.t.sol b/test/unit/SingleGovernance.t.sol deleted file mode 100644 index f211165f..00000000 --- a/test/unit/SingleGovernance.t.sol +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Vm} from "forge-std/Test.sol"; - -import {Executor} from "contracts/Executor.sol"; -import {SingleGovernance} from "contracts/SingleGovernance.sol"; -import {IConfiguration, Configuration} from "contracts/Configuration.sol"; -import {ExecutorCall} from "contracts/libraries/Proposals.sol"; - -import {UnitTest} from "test/utils/unit-test.sol"; -import {TargetMock} from "test/utils/utils.sol"; - -import {TimelockMock} from "./mocks/TimelockMock.sol"; - -contract SingleGovernanceUnitTests is UnitTest { - TimelockMock private _timelock; - SingleGovernance private _singleGovernance; - Configuration private _config; - - address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); - address private _governance = makeAddr("GOVERNANCE"); - - function setUp() external { - Executor _executor = new Executor(address(this)); - _config = new Configuration(address(_executor), _emergencyGovernance, new address[](0)); - _timelock = new TimelockMock(); - _singleGovernance = new SingleGovernance(address(_config), _governance, address(_timelock)); - } - - function testFuzz_constructor(address governance, address timelock) external { - SingleGovernance instance = new SingleGovernance(address(_config), governance, timelock); - - assertEq(instance.GOVERNANCE(), governance); - assertEq(address(instance.TIMELOCK()), address(timelock)); - } - - function test_submit_proposal() external { - assertEq(_timelock.getSubmittedProposals().length, 0); - - vm.prank(_governance); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - - assertEq(_timelock.getSubmittedProposals().length, 1); - } - - function testFuzz_stranger_cannot_submit_proposal(address stranger) external { - vm.assume(stranger != address(0) && stranger != _governance); - - assertEq(_timelock.getSubmittedProposals().length, 0); - - vm.startPrank(stranger); - vm.expectRevert(abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger])); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - - assertEq(_timelock.getSubmittedProposals().length, 0); - } - - function test_schedule_proposal() external { - assertEq(_timelock.getScheduledProposals().length, 0); - - vm.prank(_governance); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - - _timelock.setSchedule(1); - _singleGovernance.scheduleProposal(1); - - assertEq(_timelock.getScheduledProposals().length, 1); - } - - function test_execute_proposal() external { - assertEq(_timelock.getExecutedProposals().length, 0); - - vm.prank(_governance); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - - _timelock.setSchedule(1); - _singleGovernance.scheduleProposal(1); - - _singleGovernance.executeProposal(1); - - assertEq(_timelock.getExecutedProposals().length, 1); - } - - function test_cancel_all_pending_proposals() external { - assertEq(_timelock.getLastCancelledProposalId(), 0); - - vm.startPrank(_governance); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - - _timelock.setSchedule(1); - _singleGovernance.scheduleProposal(1); - - _singleGovernance.cancelAllPendingProposals(); - - assertEq(_timelock.getLastCancelledProposalId(), 2); - } - - function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { - vm.assume(stranger != address(0) && stranger != _governance); - - assertEq(_timelock.getLastCancelledProposalId(), 0); - - vm.startPrank(stranger); - vm.expectRevert(abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger])); - _singleGovernance.cancelAllPendingProposals(); - - assertEq(_timelock.getLastCancelledProposalId(), 0); - } - - function test_can_schedule() external { - vm.prank(_governance); - _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - - assertFalse(_singleGovernance.canSchedule(1)); - - _timelock.setSchedule(1); - - assertTrue(_singleGovernance.canSchedule(1)); - } -} diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol new file mode 100644 index 00000000..52cab0fc --- /dev/null +++ b/test/unit/TimelockedGovernance.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +import {TimelockMock} from "../mocks/TimelockMock.sol"; + +contract TimelockedGovernanceUnitTests is UnitTest { + TimelockMock private _timelock; + TimelockedGovernance private _timelockedGovernance; + + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _governance = makeAddr("GOVERNANCE"); + address private _adminExecutor = makeAddr("ADMIN_EXECUTOR"); + + function setUp() external { + _timelock = new TimelockMock(_adminExecutor); + _timelockedGovernance = new TimelockedGovernance(_governance, _timelock); + } + + function testFuzz_constructor(address governance, ITimelock timelock) external { + TimelockedGovernance instance = new TimelockedGovernance(governance, timelock); + + assertEq(instance.GOVERNANCE(), governance); + assertEq(address(instance.TIMELOCK()), address(timelock)); + } + + function test_submit_proposal() external { + assertEq(_timelock.getSubmittedProposals().length, 0); + + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + + assertEq(_timelock.getSubmittedProposals().length, 1); + } + + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); + + assertEq(_timelock.getSubmittedProposals().length, 0); + + vm.startPrank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockedGovernance.CallerIsNotGovernance.selector, [stranger])); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + + assertEq(_timelock.getSubmittedProposals().length, 0); + } + + function test_schedule_proposal() external { + assertEq(_timelock.getScheduledProposals().length, 0); + + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _timelockedGovernance.scheduleProposal(1); + + assertEq(_timelock.getScheduledProposals().length, 1); + } + + function test_execute_proposal() external { + assertEq(_timelock.getExecutedProposals().length, 0); + + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _timelockedGovernance.scheduleProposal(1); + + _timelockedGovernance.executeProposal(1); + + assertEq(_timelock.getExecutedProposals().length, 1); + } + + function test_cancel_all_pending_proposals() external { + assertEq(_timelock.getLastCancelledProposalId(), 0); + + vm.startPrank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _timelockedGovernance.scheduleProposal(1); + + _timelockedGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getLastCancelledProposalId(), 2); + } + + function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); + + assertEq(_timelock.getLastCancelledProposalId(), 0); + + vm.startPrank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockedGovernance.CallerIsNotGovernance.selector, [stranger])); + _timelockedGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getLastCancelledProposalId(), 0); + } + + function test_can_schedule() external { + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + + assertFalse(_timelockedGovernance.canScheduleProposal(1)); + + _timelock.setSchedule(1); + + assertTrue(_timelockedGovernance.canScheduleProposal(1)); + } +} diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol new file mode 100644 index 00000000..f41865cc --- /dev/null +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -0,0 +1,1529 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {stdError} from "forge-std/StdError.sol"; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {ETHValue, ETHValues, ETHValueOverflow, ETHValueUnderflow} from "contracts/types/ETHValue.sol"; +import {SharesValue, SharesValues, SharesValueOverflow} from "contracts/types/SharesValue.sol"; +import {IndicesOneBased} from "contracts/types/IndexOneBased.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {Timestamps} from "contracts/types/Timestamp.sol"; +import { + AssetsAccounting, WithdrawalRequestStatus, UnstETHRecordStatus +} from "contracts/libraries/AssetsAccounting.sol"; + +import {UnitTest, Duration} from "test/utils/unit-test.sol"; + +contract AssetsAccountingUnitTests is UnitTest { + AssetsAccounting.Context private _accountingState; + + // --- + // accountStETHSharesLock() + // --- + + function testFuzz_accountStETHSharesLock_happyPath(address holder, uint128 sharesAmount) external { + SharesValue totalLockedShares = SharesValues.from(3); + SharesValue holderLockedShares = SharesValues.from(1); + + vm.assume(sharesAmount > 0); + vm.assume( + sharesAmount < type(uint128).max - Math.max(totalLockedShares.toUint256(), holderLockedShares.toUint256()) + ); + + SharesValue shares = SharesValues.from(sharesAmount); + + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + + vm.expectEmit(); + emit AssetsAccounting.StETHSharesLocked(holder, shares); + + AssetsAccounting.accountStETHSharesLock(_accountingState, holder, shares); + + checkAccountingStateTotalCounters(totalLockedShares + shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + assert(_accountingState.assets[holder].stETHLockedShares == holderLockedShares + shares); + assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + } + + function testFuzz_accountStETHSharesLock_RevertWhen_ZeroSharesProvided(address holder) external { + SharesValue shares = SharesValues.ZERO; + + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); + + AssetsAccounting.accountStETHSharesLock(_accountingState, holder, shares); + } + + function testFuzz_accountStETHSharesLock_WhenNoSharesWereLockedBefore( + address stranger, + uint128 sharesAmount + ) external { + SharesValue totalLockedShares = SharesValues.from(3); + + vm.assume(sharesAmount > 0); + vm.assume(sharesAmount < type(uint128).max - totalLockedShares.toUint256()); + + SharesValue shares = SharesValues.from(sharesAmount); + + _accountingState.stETHTotals.lockedShares = totalLockedShares; + + vm.expectEmit(); + emit AssetsAccounting.StETHSharesLocked(stranger, shares); + + AssetsAccounting.accountStETHSharesLock(_accountingState, stranger, shares); + + assert(_accountingState.stETHTotals.lockedShares == totalLockedShares + shares); + assert(_accountingState.assets[stranger].stETHLockedShares == shares); + assert(_accountingState.assets[stranger].lastAssetsLockTimestamp <= Timestamps.now()); + } + + // --- + // accountStETHSharesUnlock(State storage self, address holder, SharesValue shares) + // --- + + function testFuzz_accountStETHSharesUnlock_happyPath( + address holder, + uint128 sharesAmount, + uint128 holderSharesAmount + ) external { + SharesValue totalLockedSharesWithoutHolder = SharesValues.from(3); + vm.assume(sharesAmount > 0); + vm.assume(holderSharesAmount < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); + vm.assume(sharesAmount <= holderSharesAmount); + + SharesValue shares = SharesValues.from(sharesAmount); + SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); + SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; + + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + + vm.expectEmit(); + emit AssetsAccounting.StETHSharesUnlocked(holder, shares); + + AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + + checkAccountingStateTotalCounters(totalLockedShares - shares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + assert(_accountingState.assets[holder].stETHLockedShares == holderLockedShares - shares); + assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + } + + function testFuzz_accountStETHSharesUnlock_RevertOn_ZeroSharesProvided(address holder) external { + SharesValue shares = SharesValues.ZERO; + + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); + + AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + } + + function testFuzz_accountStETHSharesUnlock_RevertWhen_HolderHaveLessSharesThanProvided( + address holder, + uint128 sharesAmount, + uint128 holderSharesAmount + ) external { + SharesValue totalLockedSharesWithoutHolder = SharesValues.from(3); + vm.assume(sharesAmount > 0); + vm.assume(holderSharesAmount < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); + vm.assume(sharesAmount > holderSharesAmount); + + SharesValue shares = SharesValues.from(sharesAmount); + SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); + SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; + + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); + + AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + } + + function testFuzz_accountStETHSharesUnlock_RevertOn_AccountingError_TotalLockedSharesCounterIsLessThanProvidedSharesAmount( + address holder, + uint128 sharesAmount, + uint128 totalSharesAmount + ) external { + vm.assume(sharesAmount > 0); + vm.assume(totalSharesAmount < sharesAmount); + + SharesValue shares = SharesValues.from(sharesAmount); + SharesValue holderLockedShares = SharesValues.from(sharesAmount); + SharesValue totalLockedShares = SharesValues.from(totalSharesAmount); + + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder, shares); + } + + function testFuzz_accountStETHSharesUnlock_RevertWhen_NoSharesWereLockedBefore( + address stranger, + uint128 sharesAmount + ) external { + vm.assume(sharesAmount > 0); + + SharesValue shares = SharesValues.from(sharesAmount); + + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, shares)); + + AssetsAccounting.accountStETHSharesUnlock(_accountingState, stranger, shares); + } + + // --- + // accountStETHSharesUnlock(State storage self, address holder) + // --- + + function testFuzz_accountStETHSharesUnlock_simple_happyPath(address holder, uint128 holderSharesAmount) external { + SharesValue totalLockedSharesWithoutHolder = SharesValues.from(3); + vm.assume(holderSharesAmount > 0); + vm.assume(holderSharesAmount < type(uint128).max - totalLockedSharesWithoutHolder.toUint256()); + + SharesValue holderLockedShares = SharesValues.from(holderSharesAmount); + SharesValue totalLockedShares = totalLockedSharesWithoutHolder + holderLockedShares; + + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + + vm.expectEmit(); + emit AssetsAccounting.StETHSharesUnlocked(holder, holderLockedShares); + + SharesValue unlockedShares = AssetsAccounting.accountStETHSharesUnlock(_accountingState, holder); + + assert(unlockedShares == holderLockedShares); + checkAccountingStateTotalCounters( + totalLockedShares - holderLockedShares, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO + ); + assert(_accountingState.assets[holder].stETHLockedShares == holderLockedShares - holderLockedShares); + assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + } + + function testFuzz_accountStETHSharesUnlock_simple_RevertWhen_NoSharesWereLockedBefore(address stranger) external { + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, SharesValues.ZERO)); + + AssetsAccounting.accountStETHSharesUnlock(_accountingState, stranger); + } + + // --- + // accountStETHSharesWithdraw + // --- + + function testFuzz_accountStETHSharesWithdraw_happyPath( + address holder, + uint128 holderLockedSharesAmount, + uint128 totalLockedSharesAmount, + uint128 totalClaimedETHAmount + ) external { + vm.assume(totalLockedSharesAmount > 0); + vm.assume(holderLockedSharesAmount > 0); + vm.assume(holderLockedSharesAmount <= totalLockedSharesAmount); + + SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); + SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); + ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.stETHTotals.claimedETH = totalClaimedETH; + + ETHValue expectedETHWithdrawn = + ETHValues.from((uint256(totalClaimedETHAmount) * holderLockedSharesAmount) / totalLockedSharesAmount); + + vm.expectEmit(); + emit AssetsAccounting.ETHWithdrawn(holder, holderLockedShares, expectedETHWithdrawn); + + ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + + assert(ethWithdrawn == expectedETHWithdrawn); + checkAccountingStateTotalCounters(totalLockedShares, totalClaimedETH, SharesValues.ZERO, ETHValues.ZERO); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].unstETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + } + + function testFuzz_accountStETHSharesWithdraw_RevertWhen_HolderHaveZeroShares(address stranger) external { + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, SharesValues.ZERO)); + + AssetsAccounting.accountStETHSharesWithdraw(_accountingState, stranger); + } + + function testFuzz_accountStETHSharesWithdraw_RevertOn_AccountingError_TotalLockedSharesCounterIsZero( + address holder, + uint128 holderLockedSharesAmount, + uint128 totalClaimedETHAmount + ) external { + vm.assume(holderLockedSharesAmount > 0); + + SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); + ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingState.stETHTotals.lockedShares = SharesValues.ZERO; + _accountingState.stETHTotals.claimedETH = totalClaimedETH; + + vm.expectRevert(stdError.divisionError); + + AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + } + + function testFuzz_accountStETHSharesWithdraw_AccountingError_WithdrawAmountMoreThanTotalClaimedETH( + address holder, + uint128 holderLockedSharesAmount, + uint128 totalClaimedETHAmount + ) external { + uint128 totalLockedSharesAmount = 10; + vm.assume(holderLockedSharesAmount > totalLockedSharesAmount); + vm.assume(holderLockedSharesAmount < type(uint64).max); + vm.assume(totalClaimedETHAmount < type(uint64).max); + + SharesValue holderLockedShares = SharesValues.from(holderLockedSharesAmount); + SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); + ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.stETHTotals.claimedETH = totalClaimedETH; + + ETHValue expectedETHWithdrawn = + ETHValues.from((uint256(totalClaimedETHAmount) * holderLockedSharesAmount) / totalLockedSharesAmount); + + vm.expectEmit(); + emit AssetsAccounting.ETHWithdrawn(holder, holderLockedShares, expectedETHWithdrawn); + + ETHValue ethWithdrawn = AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + + assert(ethWithdrawn == expectedETHWithdrawn); + assert(ethWithdrawn.toUint256() >= totalClaimedETHAmount); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + } + + function testFuzz_accountStETHSharesWithdraw_RevertOn_AccountingError_WithdrawAmountOverflow(address holder) + external + { + SharesValue holderLockedShares = SharesValues.from(type(uint96).max); + SharesValue totalLockedShares = SharesValues.from(1); + ETHValue totalClaimedETH = ETHValues.from(type(uint96).max); + + _accountingState.assets[holder].stETHLockedShares = holderLockedShares; + _accountingState.stETHTotals.lockedShares = totalLockedShares; + _accountingState.stETHTotals.claimedETH = totalClaimedETH; + + vm.expectRevert(ETHValueOverflow.selector); + + AssetsAccounting.accountStETHSharesWithdraw(_accountingState, holder); + } + + // --- + // accountClaimedStETH + // --- + + function testFuzz_accountClaimedStETH_happyPath(uint128 ethAmount, uint128 totalClaimedETHAmount) external { + vm.assume(ethAmount < type(uint128).max / 2); + vm.assume(totalClaimedETHAmount < type(uint128).max / 2); + + ETHValue amount = ETHValues.from(ethAmount); + ETHValue totalClaimedETH = ETHValues.from(totalClaimedETHAmount); + + _accountingState.stETHTotals.claimedETH = totalClaimedETH; + + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(amount); + + AssetsAccounting.accountClaimedStETH(_accountingState, amount); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, totalClaimedETH + amount, SharesValues.ZERO, ETHValues.ZERO + ); + } + + // --- + // accountUnstETHLock + // --- + + // TODO: make a research on gas consumption when a lot of unstNFTs provided. + function testFuzz_accountUnstETHLock_happyPath( + address holder, + uint96[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 1); + vm.assume(amountsOfShares.length <= 500); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + uint256 expectedTotalUnstETHLockedAmount = 0; + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.assets[holder].unstETHIds.push(genRandomUnstEthId(1024)); + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new WithdrawalRequestStatus[](amountsOfShares.length); + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; + withdrawalRequestStatuses[i].isFinalized = false; + withdrawalRequestStatuses[i].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + expectedTotalUnstETHLockedAmount += amountsOfShares[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHLocked(holder, unstETHIds, SharesValues.from(expectedTotalUnstETHLockedAmount)); + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, + ETHValues.ZERO, + initialTotalUnfinalizedShares + SharesValues.from(expectedTotalUnstETHLockedAmount), + ETHValues.ZERO + ); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert( + _accountingState.assets[holder].unstETHLockedShares + == holderUnstETHLockedShares + SharesValues.from(expectedTotalUnstETHLockedAmount) + ); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingState.assets[holder].unstETHIds.length == amountsOfShares.length + 1); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + assert(_accountingState.unstETHRecords[unstETHIds[i]].lockedBy == holder); + assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Locked); + assert(_accountingState.unstETHRecords[unstETHIds[i]].index.toZeroBasedValue() == i + 1); + assert(_accountingState.unstETHRecords[unstETHIds[i]].shares == SharesValues.from(amountsOfShares[i])); + assert(_accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.ZERO); + } + } + + function testFuzz_accountUnstETHLock_RevertOn_UnstETHIdsLengthNotEqualToWithdrawalRequestStatusesLength( + address holder + ) external { + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + uint256[] memory unstETHIds = new uint256[](0); + + vm.expectRevert(); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + function testFuzz_accountUnstETHLock_RevertOn_WithdrawalRequestStatusIsFinalized( + address holder, + uint96[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 0); + vm.assume(amountsOfShares.length <= 500); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new WithdrawalRequestStatus[](amountsOfShares.length); + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; + withdrawalRequestStatuses[i].isFinalized = false; + withdrawalRequestStatuses[i].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + } + + withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isFinalized = true; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, + unstETHIds[unstETHIds.length - 1], + UnstETHRecordStatus.Finalized + ) + ); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + function testFuzz_accountUnstETHLock_RevertOn_WithdrawalRequestStatusIsClaimed( + address holder, + uint96[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 0); + vm.assume(amountsOfShares.length <= 500); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new WithdrawalRequestStatus[](amountsOfShares.length); + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; + withdrawalRequestStatuses[i].isFinalized = false; + withdrawalRequestStatuses[i].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + } + + withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isClaimed = true; + + vm.expectRevert(); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + function testFuzz_accountUnstETHLock_RevertOn_UnstETHRecordStatusIsNot_NotLocked( + address holder, + uint96[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 0); + vm.assume(amountsOfShares.length <= 500); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new WithdrawalRequestStatus[](amountsOfShares.length); + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; + withdrawalRequestStatuses[i].isFinalized = false; + withdrawalRequestStatuses[i].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + } + + _accountingState.unstETHRecords[unstETHIds[unstETHIds.length - 1]].status = UnstETHRecordStatus.Withdrawn; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, + unstETHIds[unstETHIds.length - 1], + UnstETHRecordStatus.Withdrawn + ) + ); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + function testFuzz_accountUnstETHLock_RevertWhen_DuplicatingUnstETHIdsProvided( + address holder, + uint96[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 1); + vm.assume(amountsOfShares.length <= 500); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new WithdrawalRequestStatus[](amountsOfShares.length); + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + withdrawalRequestStatuses[i].amountOfShares = amountsOfShares[i]; + withdrawalRequestStatuses[i].isFinalized = false; + withdrawalRequestStatuses[i].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.NotLocked; + } + + unstETHIds[unstETHIds.length - 1] = unstETHIds[unstETHIds.length - 2]; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, + unstETHIds[unstETHIds.length - 1], + UnstETHRecordStatus.Locked + ) + ); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + // TODO: is it expected behavior? + function testFuzz_accountUnstETHLock_WhenNoUnstETHIdsProvided( + address holder, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](0); + uint256[] memory unstETHIds = new uint256[](0); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHLocked(holder, unstETHIds, SharesValues.ZERO); + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, ETHValues.ZERO + ); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp <= Timestamps.now()); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + } + + function testFuzz_accountUnstETHLock_AccountingError_WithdrawalRequestStatusAmountOfSharesOverflow( + address holder, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + uint256[] memory unstETHIds = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(0); + withdrawalRequestStatuses[0].amountOfShares = uint256(type(uint128).max) + 1; + withdrawalRequestStatuses[0].isFinalized = false; + withdrawalRequestStatuses[0].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + + vm.expectRevert(SharesValueOverflow.selector); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + function testFuzz_accountUnstETHLock_AccountingError_HolderUnstETHLockedSharesOverflow( + address holder, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + SharesValue holderUnstETHLockedShares = SharesValues.from(type(uint128).max / 2 + 1); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + uint256[] memory unstETHIds = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(0); + withdrawalRequestStatuses[0].amountOfShares = uint128(type(uint128).max / 2) + 1; + withdrawalRequestStatuses[0].isFinalized = false; + withdrawalRequestStatuses[0].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + function testFuzz_accountUnstETHLock_AccountingError_TotalUnfinalizedSharesOverflow( + address holder, + uint96 holderUnstETHLockedSharesAmount + ) external { + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(type(uint128).max / 2 + 1); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + uint256[] memory unstETHIds = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(0); + withdrawalRequestStatuses[0].amountOfShares = uint128(type(uint128).max / 2) + 1; + withdrawalRequestStatuses[0].isFinalized = false; + withdrawalRequestStatuses[0].isClaimed = false; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); + } + + // --- + // accountUnstETHUnlock + // --- + + // TODO: make a research on gas consumption when a lot of unstNFTs provided. + function testFuzz_accountUnstETHUnlock_happyPath( + address holder, + uint64[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 0); + vm.assume(amountsOfShares.length <= 500); + vm.assume(holderUnstETHLockedSharesAmount > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + uint256 expectedTotalSharesUnlockedAmount = 0; + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(amountsOfShares[i]); + _accountingState.unstETHRecords[unstETHIds[i]].index = IndicesOneBased.fromOneBasedValue(i + 1); + _accountingState.assets[holder].unstETHIds.push(unstETHIds[i]); + expectedTotalSharesUnlockedAmount += amountsOfShares[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHUnlocked( + holder, unstETHIds, SharesValues.from(expectedTotalSharesUnlockedAmount), ETHValues.ZERO + ); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, + ETHValues.ZERO, + initialTotalUnfinalizedShares - SharesValues.from(expectedTotalSharesUnlockedAmount), + initialTotalFinalizedETH + ); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert( + _accountingState.assets[holder].unstETHLockedShares + == holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) + ); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + assert(_accountingState.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); + } + } + + function testFuzz_accountUnstETHUnlock_WhenFinalizedUnstETHUnlocked( + address holder, + uint64[] memory amountsOfShares, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(amountsOfShares.length > 0); + vm.assume(amountsOfShares.length <= 500); + vm.assume(holderUnstETHLockedSharesAmount > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalFinalizedETHAmount > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + uint256 expectedTotalSharesUnlockedAmount = 0; + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); + + for (uint256 i = 0; i < amountsOfShares.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; + _accountingState.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(amountsOfShares[i]); + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(amountsOfShares[i]); + _accountingState.unstETHRecords[unstETHIds[i]].index = IndicesOneBased.fromOneBasedValue(i + 1); + _accountingState.assets[holder].unstETHIds.push(unstETHIds[i]); + expectedTotalSharesUnlockedAmount += amountsOfShares[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHUnlocked( + holder, + unstETHIds, + SharesValues.from(expectedTotalSharesUnlockedAmount), + ETHValues.from(expectedTotalSharesUnlockedAmount) + ); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, + ETHValues.ZERO, + initialTotalUnfinalizedShares, + initialTotalFinalizedETH - ETHValues.from(expectedTotalSharesUnlockedAmount) + ); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert( + _accountingState.assets[holder].unstETHLockedShares + == holderUnstETHLockedShares - SharesValues.from(expectedTotalSharesUnlockedAmount) + ); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + assert(_accountingState.unstETHRecords[unstETHIds[i]].shares == SharesValues.ZERO); + } + } + + function testFuzz_accountUnstETHUnlock_RevertWhen_UnknownUnstETHIdProvided(address holder) external { + vm.assume(holder != address(0x0)); + + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + + vm.expectRevert( + abi.encodeWithSelector(AssetsAccounting.InvalidUnstETHHolder.selector, unstETHIds[0], holder, address(0x0)) + ); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + } + + function testFuzz_accountUnstETHUnlock_RevertWhen_UnstETHRecordDoesNotBelongToCurrent( + address holder, + address current + ) external { + vm.assume(holder != current); + + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + + vm.expectRevert( + abi.encodeWithSelector(AssetsAccounting.InvalidUnstETHHolder.selector, unstETHIds[0], current, holder) + ); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, current, unstETHIds); + } + + function testFuzz_accountUnstETHUnlock_RevertWhen_UnstETHRecordStatusInvalid(address holder) external { + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstETHIds[0], UnstETHRecordStatus.NotLocked + ) + ); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + } + + function testFuzz_accountUnstETHUnlock_RevertWhen_UnstETHRecordIndexInvalid_OOB(address holder) external { + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(1234); + _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(10); + _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + + vm.expectRevert(stdError.indexOOBError); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + } + + // TODO: is it expected behavior? + function testFuzz_accountUnstETHUnlock_WhenNoUnstETHIdsProvided( + address holder, + uint96 holderUnstETHLockedSharesAmount, + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(holderUnstETHLockedSharesAmount > 500 * uint128(type(uint64).max)); + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + SharesValue holderUnstETHLockedShares = SharesValues.from(holderUnstETHLockedSharesAmount); + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](0); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHUnlocked(holder, unstETHIds, SharesValues.ZERO, ETHValues.ZERO); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH + ); + assert(_accountingState.assets[holder].stETHLockedShares == SharesValues.ZERO); + assert(_accountingState.assets[holder].unstETHLockedShares == holderUnstETHLockedShares); + assert(_accountingState.assets[holder].lastAssetsLockTimestamp == Timestamps.ZERO); + assert(_accountingState.assets[holder].unstETHIds.length == 0); + } + + function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_HolderUnstETHLockedSharesUnderflow(address holder) + external + { + _accountingState.assets[holder].unstETHLockedShares = SharesValues.from(5); + + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(10); + _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); + _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + } + + function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_TotalFinalizedETHUnderflow(address holder) + external + { + _accountingState.assets[holder].unstETHLockedShares = SharesValues.from(10); + _accountingState.unstETHTotals.finalizedETH = ETHValues.from(5); + + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Finalized; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(5); + _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); + _accountingState.unstETHRecords[unstETHIds[0]].claimableAmount = ETHValues.from(10); + _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + + vm.expectRevert(ETHValueUnderflow.selector); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + } + + function testFuzz_accountUnstETHUnlock_RevertOn_AccountingError_TotalUnfinalizedSharesUnderflow(address holder) + external + { + _accountingState.assets[holder].unstETHLockedShares = SharesValues.from(10); + _accountingState.unstETHTotals.unfinalizedShares = SharesValues.from(5); + + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = genRandomUnstEthId(1234); + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(10); + _accountingState.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); + _accountingState.assets[holder].unstETHIds.push(unstETHIds[0]); + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHUnlock(_accountingState, holder, unstETHIds); + } + + // --- + // accountUnstETHFinalized + // --- + + // TODO: make a research on gas consumption when a lot of unstNFTs provided. + function testFuzz_accountUnstETHFinalized_happyPath( + uint64[] memory claimableAmounts, + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + uint256 expectedTotalSharesFinalized = 0; + uint256 expectedTotalAmountFinalized = 0; + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + uint256 sharesAmount = 5 * uint256(claimableAmounts[i]); + _accountingState.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(sharesAmount); + expectedTotalSharesFinalized += sharesAmount; + expectedTotalAmountFinalized += claimableAmounts[i]; + claimableAmountsPrepared[i] = claimableAmounts[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHFinalized( + unstETHIds, SharesValues.from(expectedTotalSharesFinalized), ETHValues.from(expectedTotalAmountFinalized) + ); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, + ETHValues.ZERO, + initialTotalUnfinalizedShares - SharesValues.from(expectedTotalSharesFinalized), + initialTotalFinalizedETH + ETHValues.from(expectedTotalAmountFinalized) + ); + } + + function testFuzz_accountUnstETHFinalized_RevertWhen_ClaimableAmountsLengthNotEqUnstETHIdsLength( + uint64[] memory claimableAmounts + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length + 1); + + vm.expectRevert(stdError.assertionError); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + function testFuzz_accountUnstETHFinalized_When_NoClaimableAmountsProvided( + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](0); + uint256[] memory claimableAmountsPrepared = new uint256[](0); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHFinalized(unstETHIds, SharesValues.from(0), ETHValues.from(0)); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH + ); + } + + function testFuzz_accountUnstETHFinalized_When_UnstETHRecordNotFound( + uint64 claimableAmount, + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](1); + uint256[] memory claimableAmountsPrepared = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(9876); + claimableAmountsPrepared[0] = claimableAmount; + + vm.expectEmit(); + emit AssetsAccounting.UnstETHFinalized(unstETHIds, SharesValues.from(0), ETHValues.from(0)); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH + ); + } + + function testFuzz_accountUnstETHFinalized_When_ClaimableAmountIsZero( + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](1); + uint256[] memory claimableAmountsPrepared = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(9876); + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); + claimableAmountsPrepared[0] = 0; + + vm.expectEmit(); + emit AssetsAccounting.UnstETHFinalized(unstETHIds, SharesValues.from(0), ETHValues.from(0)); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters( + SharesValues.ZERO, ETHValues.ZERO, initialTotalUnfinalizedShares, initialTotalFinalizedETH + ); + } + + function testFuzz_accountUnstETHFinalized_RevertOn_ClaimableAmountOverflow( + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(initialTotalUnfinalizedSharesAmount > 500 * uint128(type(uint64).max)); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](1); + uint256[] memory claimableAmountsPrepared = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(9876); + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); + claimableAmountsPrepared[0] = uint256(type(uint128).max) + 1; + + vm.expectRevert(ETHValueOverflow.selector); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + function testFuzz_accountUnstETHFinalized_RevertOn_TotalFinalizedETHOverflow( + uint128 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(initialTotalFinalizedETHAmount > type(uint96).max); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](1); + uint256[] memory claimableAmountsPrepared = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(9876); + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); + claimableAmountsPrepared[0] = uint256(type(uint128).max - 2); + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + function testFuzz_accountUnstETHFinalized_RevertOn_TotalUnfinalizedSharesUnderflow( + uint96 initialTotalFinalizedETHAmount, + uint96 initialTotalUnfinalizedSharesAmount + ) external { + vm.assume(initialTotalUnfinalizedSharesAmount < type(uint64).max); + + ETHValue initialTotalFinalizedETH = ETHValues.from(initialTotalFinalizedETHAmount); + SharesValue initialTotalUnfinalizedShares = SharesValues.from(initialTotalUnfinalizedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = initialTotalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; + + uint256[] memory unstETHIds = new uint256[](1); + uint256[] memory claimableAmountsPrepared = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(9876); + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + _accountingState.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(type(uint64).max); + claimableAmountsPrepared[0] = 1; + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHFinalized(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + // --- + // accountUnstETHClaimed + // --- + + // TODO: make a research on gas consumption when a lot of unstNFTs provided. + function testFuzz_accountUnstETHClaimed_happyPath(uint64[] memory claimableAmounts) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256 expectedTotalAmountClaimed = 0; + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + expectedTotalAmountClaimed += claimableAmounts[i]; + claimableAmountsPrepared[i] = claimableAmounts[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHClaimed(unstETHIds, ETHValues.from(expectedTotalAmountClaimed)); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + assert( + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + ); + assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); + } + } + + // TODO: Maybe need to add check for `assert(claimableAmounts.length == unstETHIds.length)` to the code + function testFuzz_accountUnstETHClaimed_RevertWhen_ClaimableAmountsLengthNotEqUnstETHIdsLength( + uint64[] memory claimableAmounts + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length + 1); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Locked; + claimableAmountsPrepared[i] = claimableAmounts[i]; + } + + vm.expectRevert(stdError.indexOOBError); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + function test_accountUnstETHClaimed_WhenNoUnstETHIdsProvided() external { + uint256 expectedTotalAmountClaimed = 0; + + uint256[] memory unstETHIds = new uint256[](0); + uint256[] memory claimableAmountsPrepared = new uint256[](0); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHClaimed(unstETHIds, ETHValues.from(expectedTotalAmountClaimed)); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + } + + function testFuzz_accountUnstETHClaimed_RevertWhen_UnstETHRecordNotFoundOrHasWrongStatus( + uint64[] memory claimableAmounts + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length + 1); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + claimableAmountsPrepared[i] = claimableAmounts[i]; + } + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstETHIds[0], UnstETHRecordStatus.NotLocked + ) + ); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + function testFuzz_accountUnstETHClaimed_RevertWhen_UnstETHRecordIsFinalizedAndClaimableAmountIsIncorrect( + uint64[] memory claimableAmounts + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length + 1); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = + ETHValues.from(uint256(claimableAmounts[i]) + 1); + claimableAmountsPrepared[i] = claimableAmounts[i]; + } + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidClaimableAmount.selector, + unstETHIds[0], + claimableAmounts[0], + uint256(claimableAmounts[0]) + 1 + ) + ); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + function testFuzz_accountUnstETHClaimed_When_UnstETHRecordIsFinalizedAndClaimableAmountIsCorrect( + uint64[] memory claimableAmounts + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256 expectedTotalAmountClaimed = 0; + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); + uint256[] memory claimableAmountsPrepared = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Finalized; + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(claimableAmounts[i]); + claimableAmountsPrepared[i] = claimableAmounts[i]; + expectedTotalAmountClaimed += claimableAmounts[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHClaimed(unstETHIds, ETHValues.from(expectedTotalAmountClaimed)); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + + checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + assert( + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + ); + assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Claimed); + } + } + + function test_accountUnstETHClaimed_RevertWhen_ClaimableAmountsOverflow() external { + uint256[] memory unstETHIds = new uint256[](1); + uint256[] memory claimableAmountsPrepared = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(1); + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Locked; + claimableAmountsPrepared[0] = uint256(type(uint128).max) + 1; + + vm.expectRevert(ETHValueOverflow.selector); + + AssetsAccounting.accountUnstETHClaimed(_accountingState, unstETHIds, claimableAmountsPrepared); + } + + // --- + // accountUnstETHWithdraw + // --- + + // TODO: make a research on gas consumption when a lot of unstNFTs provided. + function testFuzz_accountUnstETHWithdraw_happyPath(address holder, uint64[] memory claimableAmounts) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256 expectedAmountWithdrawn = 0; + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Claimed; + _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(claimableAmounts[i]); + expectedAmountWithdrawn += claimableAmounts[i]; + } + + vm.expectEmit(); + emit AssetsAccounting.UnstETHWithdrawn(unstETHIds, ETHValues.from(expectedAmountWithdrawn)); + + ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + + assert(amountWithdrawn == ETHValues.from(expectedAmountWithdrawn)); + + checkAccountingStateTotalCounters(SharesValues.ZERO, ETHValues.ZERO, SharesValues.ZERO, ETHValues.ZERO); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + assert( + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount == ETHValues.from(claimableAmounts[i]) + ); + assert(_accountingState.unstETHRecords[unstETHIds[i]].status == UnstETHRecordStatus.Withdrawn); + assert(_accountingState.unstETHRecords[unstETHIds[i]].lockedBy == holder); + } + } + + function testFuzz_accountUnstETHWithdraw_WhenNoUnstETHIdsProvided(address holder) external { + uint256[] memory unstETHIds = new uint256[](0); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHWithdrawn(unstETHIds, ETHValues.ZERO); + + ETHValue amountWithdrawn = AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + + assert(amountWithdrawn == ETHValues.ZERO); + } + + function testFuzz_accountUnstETHWithdraw_RevertWhen_UnstETHRecordNotFound( + address holder, + uint64[] memory claimableAmounts + ) external { + vm.assume(claimableAmounts.length > 0); + vm.assume(claimableAmounts.length <= 500); + + uint256[] memory unstETHIds = new uint256[](claimableAmounts.length); + + for (uint256 i = 0; i < claimableAmounts.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + } + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstETHIds[0], UnstETHRecordStatus.NotLocked + ) + ); + + AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + } + + function testFuzz_accountUnstETHWithdraw_RevertWhen_UnstETHRecordDoesNotBelongToCurrent( + address holder, + address current + ) external { + vm.assume(holder != current); + + uint256[] memory unstETHIds = new uint256[](1); + + unstETHIds[0] = genRandomUnstEthId(567); + _accountingState.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.Claimed; + _accountingState.unstETHRecords[unstETHIds[0]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[0]].claimableAmount = ETHValues.from(123); + + vm.expectRevert( + abi.encodeWithSelector(AssetsAccounting.InvalidUnstETHHolder.selector, unstETHIds[0], current, holder) + ); + + AssetsAccounting.accountUnstETHWithdraw(_accountingState, current, unstETHIds); + } + + function testFuzz_accountUnstETHWithdraw_RevertOn_WithdrawnAmountOverflow(address holder) external { + uint256[] memory unstETHIds = new uint256[](2); + + for (uint256 i = 0; i < unstETHIds.length; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + _accountingState.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus.Claimed; + _accountingState.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingState.unstETHRecords[unstETHIds[i]].claimableAmount = + ETHValues.from(uint256(type(uint128).max) / 2 + 1); + } + + vm.expectRevert(stdError.arithmeticError); + + AssetsAccounting.accountUnstETHWithdraw(_accountingState, holder, unstETHIds); + } + + // --- + // getLockedAssetsTotals + // --- + + function testFuzz_getLockedAssetsTotals_happyPath( + uint96 totalFinalizedETHAmount, + uint96 totalLockedSharesAmount, + uint96 totalUnfinalizedSharesAmount + ) external { + ETHValue totalFinalizedETH = ETHValues.from(totalFinalizedETHAmount); + SharesValue totalUnfinalizedShares = SharesValues.from(totalUnfinalizedSharesAmount); + SharesValue totalLockedShares = SharesValues.from(totalLockedSharesAmount); + + _accountingState.unstETHTotals.finalizedETH = totalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; + _accountingState.stETHTotals.lockedShares = totalLockedShares; + + (SharesValue unfinalizedShares, ETHValue finalizedETH) = + AssetsAccounting.getLockedAssetsTotals(_accountingState); + + assert(unfinalizedShares == totalLockedShares + totalUnfinalizedShares); + assert(finalizedETH == totalFinalizedETH); + } + + function test_getLockedAssetsTotals_RevertOn_UnfinalizedSharesOverflow() external { + ETHValue totalFinalizedETH = ETHValues.from(1); + SharesValue totalUnfinalizedShares = SharesValues.from(type(uint128).max - 1); + SharesValue totalLockedShares = SharesValues.from(type(uint128).max - 1); + + _accountingState.unstETHTotals.finalizedETH = totalFinalizedETH; + _accountingState.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; + _accountingState.stETHTotals.lockedShares = totalLockedShares; + + vm.expectRevert(stdError.arithmeticError); + AssetsAccounting.getLockedAssetsTotals(_accountingState); + } + + // --- + // checkMinAssetsLockDurationPassed + // --- + + function testFuzz_checkMinAssetsLockDurationPassed_happyPath(address holder) external { + Duration minAssetsLockDuration = Durations.from(0); + _accountingState.assets[holder].lastAssetsLockTimestamp = Timestamps.from(Timestamps.now().toSeconds() - 1); + + AssetsAccounting.checkMinAssetsLockDurationPassed(_accountingState, holder, minAssetsLockDuration); + } + + function testFuzz_checkMinAssetsLockDurationPassed_RevertOn_MinAssetsLockDurationNotPassed(address holder) + external + { + Duration minAssetsLockDuration = Durations.from(1); + _accountingState.assets[holder].lastAssetsLockTimestamp = Timestamps.from(Timestamps.now().toSeconds() - 1); + + vm.expectRevert( + abi.encodeWithSelector(AssetsAccounting.MinAssetsLockDurationNotPassed.selector, Timestamps.now()) + ); + + AssetsAccounting.checkMinAssetsLockDurationPassed(_accountingState, holder, minAssetsLockDuration); + } + + // --- + // helpers + // --- + + function genRandomUnstEthId(uint256 salt) internal view returns (uint256) { + return uint256(keccak256(abi.encodePacked(block.timestamp, salt))); // random id + } + + function checkAccountingStateTotalCounters( + SharesValue lockedShares, + ETHValue claimedETH, + SharesValue unfinalizedShares, + ETHValue finalizedETH + ) internal view { + assert(_accountingState.stETHTotals.lockedShares == lockedShares); + assert(_accountingState.stETHTotals.claimedETH == claimedETH); + assert(_accountingState.unstETHTotals.unfinalizedShares == unfinalizedShares); + assert(_accountingState.unstETHTotals.finalizedETH == finalizedETH); + } +} diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index d588a9a7..02856a37 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -1,47 +1,66 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Test, Vm} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Test.sol"; -import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; -import {UnitTest, Duration, Durations, Timestamp, Timestamps} from "test/utils/unit-test.sol"; +import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; contract EmergencyProtectionUnitTests is UnitTest { - using EmergencyProtection for EmergencyProtection.State; + using EmergencyProtection for EmergencyProtection.Context; + + address internal _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); - EmergencyProtection.State internal _emergencyProtection; + EmergencyProtection.Context internal _emergencyProtection; function testFuzz_setup_emergency_protection( address activationCommittee, address executionCommittee, + address emergencyGovernance, Duration protectionDuration, Duration duration ) external { vm.assume(protectionDuration > Durations.ZERO); vm.assume(duration > Durations.ZERO); - vm.assume(activationCommittee != address(0)); - vm.assume(executionCommittee != address(0)); - - vm.expectEmit(); - emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); + // vm.assume(activationCommittee != address(0)); + // vm.assume(executionCommittee != address(0)); + uint256 expectedLogEntiresCount = 2; + if (emergencyGovernance != address(0)) { + vm.expectEmit(); + emit EmergencyProtection.EmergencyGovernanceSet(emergencyGovernance); + expectedLogEntiresCount += 1; + } + + if (activationCommittee != address(0)) { + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); + expectedLogEntiresCount += 1; + } + if (executionCommittee != address(0)) { + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); + expectedLogEntiresCount += 1; + } vm.expectEmit(); - emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); - vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(protectionDuration.addTo(Timestamps.now())); + emit EmergencyProtection.EmergencyProtectionEndDateSet(protectionDuration.addTo(Timestamps.now())); vm.expectEmit(); emit EmergencyProtection.EmergencyModeDurationSet(duration); vm.recordLogs(); - _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, duration); + _setup(emergencyGovernance, activationCommittee, executionCommittee, protectionDuration, duration); Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 4); + assertEq(entries.length, expectedLogEntiresCount); - assertEq(_emergencyProtection.activationCommittee, activationCommittee); - assertEq(_emergencyProtection.executionCommittee, executionCommittee); - assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyGovernance, emergencyGovernance); + assertEq(_emergencyProtection.emergencyActivationCommittee, activationCommittee); + assertEq(_emergencyProtection.emergencyExecutionCommittee, executionCommittee); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, protectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, duration); assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -51,7 +70,7 @@ contract EmergencyProtectionUnitTests is UnitTest { Duration emergencyModeDuration = Durations.from(100 seconds); address activationCommittee = makeAddr("activationCommittee"); - _emergencyProtection.setup(activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); Duration newProtectionDuration = Durations.from(200 seconds); Duration newEmergencyModeDuration = Durations.from(300 seconds); @@ -59,19 +78,19 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); vm.expectEmit(); emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); + _setup(_emergencyGovernance, activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); - assertEq(_emergencyProtection.activationCommittee, activationCommittee); - assertEq(_emergencyProtection.executionCommittee, address(0x3)); - assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyActivationCommittee, activationCommittee); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x3)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -81,7 +100,7 @@ contract EmergencyProtectionUnitTests is UnitTest { Duration emergencyModeDuration = Durations.from(100 seconds); address executionCommittee = makeAddr("executionCommittee"); - _emergencyProtection.setup(address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); Duration newProtectionDuration = Durations.from(200 seconds); Duration newEmergencyModeDuration = Durations.from(300 seconds); @@ -89,19 +108,19 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); vm.expectEmit(); emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); + _setup(_emergencyGovernance, address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); - assertEq(_emergencyProtection.activationCommittee, address(0x2)); - assertEq(_emergencyProtection.executionCommittee, executionCommittee); - assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x2)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, executionCommittee); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -110,7 +129,7 @@ contract EmergencyProtectionUnitTests is UnitTest { Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(100 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); Duration newProtectionDuration = protectionDuration; // the new value is the same as previous one Duration newEmergencyModeDuration = Durations.from(200 seconds); @@ -123,14 +142,14 @@ contract EmergencyProtectionUnitTests is UnitTest { emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); + _setup(_emergencyGovernance, address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); - assertEq(_emergencyProtection.activationCommittee, address(0x3)); - assertEq(_emergencyProtection.executionCommittee, address(0x4)); - assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x3)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x4)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, protectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -139,7 +158,7 @@ contract EmergencyProtectionUnitTests is UnitTest { Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(100 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); Duration newProtectionDuration = Durations.from(200 seconds); Duration newEmergencyModeDuration = emergencyModeDuration; // the new value is the same as previous one @@ -149,17 +168,17 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); vm.recordLogs(); - _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); + _setup(_emergencyGovernance, address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); - assertEq(_emergencyProtection.activationCommittee, address(0x3)); - assertEq(_emergencyProtection.executionCommittee, address(0x4)); - assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x3)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x4)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } @@ -168,14 +187,14 @@ contract EmergencyProtectionUnitTests is UnitTest { Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(100 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeActivated(Timestamps.now()); + emit EmergencyProtection.EmergencyModeActivated(); vm.recordLogs(); - _emergencyProtection.activate(); + _emergencyProtection.activateEmergencyMode(); Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -187,18 +206,17 @@ contract EmergencyProtectionUnitTests is UnitTest { Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(100 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); _wait(protectionDuration.plusSeconds(1)); vm.expectRevert( abi.encodeWithSelector( - EmergencyProtection.EmergencyCommitteeExpired.selector, - Timestamps.now(), - _emergencyProtection.protectedTill + EmergencyProtection.EmergencyProtectionExpired.selector, + _emergencyProtection.emergencyProtectionEndsAfter ) ); - _emergencyProtection.activate(); + _emergencyProtection.activateEmergencyMode(); } function testFuzz_deactivate_emergency_mode( @@ -210,113 +228,66 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.assume(activationCommittee != address(0)); vm.assume(executionCommittee != address(0)); - _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); - _emergencyProtection.activate(); + _setup(_emergencyGovernance, activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); + _emergencyProtection.activateEmergencyMode(); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDeactivated(Timestamps.now()); + emit EmergencyProtection.EmergencyModeDeactivated(); vm.recordLogs(); - _emergencyProtection.deactivate(); + _emergencyProtection.deactivateEmergencyMode(); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); - assertEq(_emergencyProtection.activationCommittee, address(0)); - assertEq(_emergencyProtection.executionCommittee, address(0)); - assertEq(_emergencyProtection.protectedTill, Timestamps.ZERO); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(_emergencyProtection.emergencyModeDuration, Durations.ZERO); assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } - function test_get_emergency_state() external { - EmergencyState memory state = _emergencyProtection.getEmergencyState(); - - assertEq(state.activationCommittee, address(0)); - assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, Timestamps.ZERO); - assertEq(state.emergencyModeDuration, Durations.ZERO); - assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); - assertEq(state.isEmergencyModeActivated, false); - - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(200 seconds); - - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - - state = _emergencyProtection.getEmergencyState(); - - assertEq(state.activationCommittee, address(0x1)); - assertEq(state.executionCommittee, address(0x2)); - assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); - assertEq(state.emergencyModeDuration, emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); - assertEq(state.isEmergencyModeActivated, false); - - _emergencyProtection.activate(); - - state = _emergencyProtection.getEmergencyState(); - - assertEq(state.activationCommittee, address(0x1)); - assertEq(state.executionCommittee, address(0x2)); - assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); - assertEq(state.emergencyModeDuration, emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); - assertEq(state.isEmergencyModeActivated, true); - - _emergencyProtection.deactivate(); - - state = _emergencyProtection.getEmergencyState(); - - assertEq(state.activationCommittee, address(0)); - assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, Timestamps.ZERO); - assertEq(state.emergencyModeDuration, Durations.ZERO); - assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); - assertEq(state.isEmergencyModeActivated, false); - } - function test_is_emergency_mode_activated() external { - assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + assertEq(_emergencyProtection.isEmergencyModeActive(), false); Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(100 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + assertEq(_emergencyProtection.isEmergencyModeActive(), false); - _emergencyProtection.activate(); + _emergencyProtection.activateEmergencyMode(); - assertEq(_emergencyProtection.isEmergencyModeActivated(), true); + assertEq(_emergencyProtection.isEmergencyModeActive(), true); - _emergencyProtection.deactivate(); + _emergencyProtection.deactivateEmergencyMode(); - assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + assertEq(_emergencyProtection.isEmergencyModeActive(), false); } function test_is_emergency_mode_passed() external { - assertEq(_emergencyProtection.isEmergencyModePassed(), false); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(200 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - assertEq(_emergencyProtection.isEmergencyModePassed(), false); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); - _emergencyProtection.activate(); + _emergencyProtection.activateEmergencyMode(); - assertEq(_emergencyProtection.isEmergencyModePassed(), false); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); _wait(emergencyModeDuration.plusSeconds(1)); - assertEq(_emergencyProtection.isEmergencyModePassed(), true); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), true); - _emergencyProtection.deactivate(); + _emergencyProtection.deactivateEmergencyMode(); - assertEq(_emergencyProtection.isEmergencyModePassed(), false); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); } function test_is_emergency_protection_enabled() external { @@ -325,17 +296,17 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); - EmergencyState memory emergencyState = _emergencyProtection.getEmergencyState(); + EmergencyProtection.Context memory emergencyState = _emergencyProtection; - _wait(Durations.between(emergencyState.protectedTill, Timestamps.now())); + _wait(Durations.between(emergencyState.emergencyProtectionEndsAfter, Timestamps.now())); - // _wait(emergencyState.protectedTill.absDiff(Timestamps.now())); + // _wait(emergencyState.emergencyProtectionEndsAfter.absDiff(Timestamps.now())); - EmergencyProtection.activate(_emergencyProtection); + EmergencyProtection.activateEmergencyMode(_emergencyProtection); _wait(emergencyModeDuration); @@ -345,65 +316,37 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); - EmergencyProtection.deactivate(_emergencyProtection); + EmergencyProtection.deactivateEmergencyMode(_emergencyProtection); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); } - function testFuzz_check_activation_committee(address committee, address stranger) external { - vm.assume(committee != address(0)); - vm.assume(stranger != address(0) && stranger != committee); - - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); - _emergencyProtection.checkActivationCommittee(stranger); - _emergencyProtection.checkActivationCommittee(address(0)); - - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - - _emergencyProtection.setup(committee, address(0x2), protectionDuration, emergencyModeDuration); - - _emergencyProtection.checkActivationCommittee(committee); - - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); - _emergencyProtection.checkActivationCommittee(stranger); - } - - function testFuzz_check_execution_committee(address committee, address stranger) external { - vm.assume(committee != address(0)); - vm.assume(stranger != address(0) && stranger != committee); - - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); - _emergencyProtection.checkExecutionCommittee(stranger); - _emergencyProtection.checkExecutionCommittee(address(0)); + function test_check_emergency_mode_active() external { + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + _emergencyProtection.checkEmergencyMode(true); + _emergencyProtection.checkEmergencyMode(false); Duration protectionDuration = Durations.from(100 seconds); Duration emergencyModeDuration = Durations.from(100 seconds); - _emergencyProtection.setup(address(0x1), committee, protectionDuration, emergencyModeDuration); - - _emergencyProtection.checkExecutionCommittee(committee); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _emergencyProtection.activateEmergencyMode(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); - _emergencyProtection.checkExecutionCommittee(stranger); + _emergencyProtection.checkEmergencyMode(true); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); } - function test_check_emergency_mode_active() external { - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) - ); - _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkEmergencyModeActive(false); - - Duration protectionDuration = Durations.from(100 seconds); - Duration emergencyModeDuration = Durations.from(100 seconds); - - _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - _emergencyProtection.activate(); - - _emergencyProtection.checkEmergencyModeActive(true); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) - ); + function _setup( + address newEmergencyGovernance, + address newEmergencyActivationCommittee, + address newEmergencyExecutionCommittee, + Duration protectionDuration, + Duration emergencyModeDuration + ) internal { + _emergencyProtection.setEmergencyGovernance(newEmergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(newEmergencyActivationCommittee); + _emergencyProtection.setEmergencyExecutionCommittee(newEmergencyExecutionCommittee); + _emergencyProtection.setEmergencyProtectionEndDate(protectionDuration.addTo(Timestamps.now()), Durations.MAX); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, Durations.MAX); } } diff --git a/test/unit/libraries/EnumerableProposals.t.sol b/test/unit/libraries/EnumerableProposals.t.sol new file mode 100644 index 00000000..cf742534 --- /dev/null +++ b/test/unit/libraries/EnumerableProposals.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EnumerableProposals} from "contracts/libraries/EnumerableProposals.sol"; +import {Proposal} from "contracts/libraries/EnumerableProposals.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract EnumerableProposalsTest is UnitTest { + using EnumerableProposals for EnumerableProposals.Bytes32ToProposalMap; + + EnumerableProposals.Bytes32ToProposalMap private proposalsMap; + + bytes32 constant TEST_KEY_1 = keccak256("TEST_KEY_1"); + bytes32 constant TEST_KEY_2 = keccak256("TEST_KEY_2"); + uint256 constant TEST_PROPOSAL_TYPE = 1; + bytes constant TEST_DATA = "test data"; + + function test_pushAddsProposal() public { + bool success = proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + assertTrue(success); + assertEq(proposalsMap.length(), 1); + } + + function test_pushDoesNotAddDuplicateProposal() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + bool success = proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + assertFalse(success); + assertEq(proposalsMap.length(), 1); + } + + function test_containsReturnsTrueForExistingProposal() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + bool exists = proposalsMap.contains(TEST_KEY_1); + assertTrue(exists); + } + + function test_containsReturnsFalseForNonExistingProposal() public { + bool exists = proposalsMap.contains(TEST_KEY_1); + assertFalse(exists); + } + + function test_getReturnsCorrectProposal() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + Proposal memory proposal = proposalsMap.get(TEST_KEY_1); + assertEq(proposal.proposalType, TEST_PROPOSAL_TYPE); + assertEq(proposal.data, TEST_DATA); + } + + function test_getRevertsForNonExistingProposal() public { + vm.expectRevert(abi.encodeWithSelector(EnumerableProposals.ProposalDoesNotExist.selector, TEST_KEY_1)); + proposalsMap.get(TEST_KEY_1); + } + + function test_atReturnsCorrectProposal() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + Proposal memory proposal = proposalsMap.at(0); + assertEq(proposal.proposalType, TEST_PROPOSAL_TYPE); + assertEq(proposal.data, TEST_DATA); + } + + function test_atRevertsForOutOfBoundsIndex() public { + vm.expectRevert(); + proposalsMap.at(0); + } + + function test_getOrderedKeysReturnsAllKeys() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + proposalsMap.push(TEST_KEY_2, TEST_PROPOSAL_TYPE, TEST_DATA); + bytes32[] memory keys = proposalsMap.getOrderedKeys(); + assertEq(keys.length, 2); + assertEq(keys[0], TEST_KEY_1); + assertEq(keys[1], TEST_KEY_2); + } + + function test_getOrderedKeysWithPagination() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + proposalsMap.push(TEST_KEY_2, TEST_PROPOSAL_TYPE, TEST_DATA); + bytes32[] memory keys = proposalsMap.getOrderedKeys(0, 1); + assertEq(keys.length, 1); + assertEq(keys[0], TEST_KEY_1); + } + + function test_getOrderedKeysWithPaginationRevertsForInvalidOffset() public { + vm.expectRevert(EnumerableProposals.OffsetOutOfBounds.selector); + proposalsMap.getOrderedKeys(2, 1); + } + + function test_getOrderedKeysWithLimitExceedingRemainingKeys() public { + proposalsMap.push(TEST_KEY_1, TEST_PROPOSAL_TYPE, TEST_DATA); + proposalsMap.push(TEST_KEY_2, TEST_PROPOSAL_TYPE, TEST_DATA); + + bytes32[] memory keys = proposalsMap.getOrderedKeys(1, 5); + + assertEq(keys.length, 1); + assertEq(keys[0], TEST_KEY_2); + } +} diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol new file mode 100644 index 00000000..eb4be119 --- /dev/null +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Vm} from "forge-std/Test.sol"; + +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; + +import {Executor} from "contracts/Executor.sol"; +import { + ExecutableProposals, ExternalCall, Status as ProposalStatus +} from "contracts/libraries/ExecutableProposals.sol"; + +import {TargetMock} from "test/utils/target-mock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ExecutableProposalsUnitTests is UnitTest { + using ExecutableProposals for ExecutableProposals.Context; + + Executor private _executor; + TargetMock private _targetMock; + ExecutableProposals.Context internal _proposals; + + uint256 private constant PROPOSAL_ID_OFFSET = 1; + + function setUp() external { + _targetMock = new TargetMock(); + _executor = new Executor(address(this)); + } + + function test_submit_reverts_if_empty_proposals() external { + vm.expectRevert(ExecutableProposals.EmptyCalls.selector); + _proposals.submit(address(0), new ExternalCall[](0)); + } + + function test_submit_proposal() external { + uint256 proposalsCount = _proposals.getProposalsCount(); + + ExternalCall[] memory calls = _getMockTargetRegularStaffCalls(address(_targetMock)); + + uint256 expectedProposalId = proposalsCount + PROPOSAL_ID_OFFSET; + + vm.expectEmit(); + emit ExecutableProposals.ProposalSubmitted(expectedProposalId, address(_executor), calls); + + vm.recordLogs(); + + _proposals.submit(address(_executor), calls); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + + proposalsCount = _proposals.getProposalsCount(); + + ExecutableProposals.Proposal memory proposal = _proposals.proposals[expectedProposalId]; + + assertEq(proposal.data.status, ProposalStatus.Submitted); + assertEq(proposal.data.executor, address(_executor)); + assertEq(proposal.data.submittedAt, Timestamps.now()); + assertEq(proposal.data.scheduledAt, Timestamps.ZERO); + + assertEq(proposal.calls.length, 1); + + for (uint256 i = 0; i < calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function testFuzz_schedule_proposal(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + + uint256 expectedProposalId = 1; + ExecutableProposals.Proposal memory proposal = _proposals.proposals[expectedProposalId]; + + Timestamp submittedAt = Timestamps.now(); + + assertEq(proposal.data.status, ProposalStatus.Submitted); + assertEq(proposal.data.submittedAt, submittedAt); + assertEq(proposal.data.scheduledAt, Timestamps.ZERO); + + _wait(delay); + + vm.expectEmit(); + emit ExecutableProposals.ProposalScheduled(expectedProposalId); + _proposals.schedule(expectedProposalId, delay); + + proposal = _proposals.proposals[expectedProposalId]; + + assertEq(proposal.data.status, ProposalStatus.Scheduled); + assertEq(proposal.data.submittedAt, submittedAt); + assertEq(proposal.data.scheduledAt, Timestamps.now()); + } + + function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { + vm.assume(proposalId > 0); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + _proposals.schedule(proposalId, Durations.ZERO); + } + + function test_cannot_schedule_proposal_twice() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = 1; + _proposals.schedule(proposalId, Durations.ZERO); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + _proposals.schedule(proposalId, Durations.ZERO); + } + + function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + + _wait(delay.minusSeconds(1 seconds)); + + vm.expectRevert( + abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, PROPOSAL_ID_OFFSET) + ); + _proposals.schedule(PROPOSAL_ID_OFFSET, delay); + } + + function test_cannot_schedule_cancelled_proposal() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.cancelAll(); + + uint256 proposalId = _proposals.getProposalsCount(); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + _proposals.schedule(proposalId, Durations.ZERO); + } + + function testFuzz_execute_proposal(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + _proposals.schedule(proposalId, Durations.ZERO); + + Timestamp submittedAndScheduledAt = Timestamps.now(); + + ExecutableProposals.Proposal memory proposal = _proposals.proposals[proposalId]; + + assertEq(proposal.data.status, ProposalStatus.Scheduled); + assertEq(proposal.data.submittedAt, submittedAndScheduledAt); + assertEq(proposal.data.scheduledAt, submittedAndScheduledAt); + + _wait(delay); + + // TODO: figure out why event is not emitted + // vm.expectEmit(); + // emit ExecutableProposals.ProposalExecuted(); + _proposals.execute(proposalId, delay); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + + proposal = _proposals.proposals[proposalId]; + + assertEq(proposal.data.status, ProposalStatus.Executed); + assertEq(proposal.data.submittedAt, submittedAndScheduledAt); + assertEq(proposal.data.scheduledAt, submittedAndScheduledAt); + } + + function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { + vm.assume(proposalId > 0); + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + _proposals.execute(proposalId, Durations.ZERO); + } + + function test_cannot_execute_unscheduled_proposal() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + _proposals.execute(proposalId, Durations.ZERO); + } + + function test_cannot_execute_twice() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + _proposals.schedule(proposalId, Durations.ZERO); + _proposals.execute(proposalId, Durations.ZERO); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + _proposals.execute(proposalId, Durations.ZERO); + } + + function test_cannot_execute_cancelled_proposal() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + _proposals.schedule(proposalId, Durations.ZERO); + _proposals.cancelAll(); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + _proposals.execute(proposalId, Durations.ZERO); + } + + function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + _proposals.schedule(proposalId, Durations.ZERO); + + _wait(delay.minusSeconds(1 seconds)); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterScheduleDelayNotPassed.selector, proposalId)); + _proposals.execute(proposalId, delay); + } + + function test_cancel_all_proposals() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + + uint256 proposalsCount = _proposals.getProposalsCount(); + + _proposals.schedule(proposalsCount, Durations.ZERO); + + vm.expectEmit(); + emit ExecutableProposals.ProposalsCancelledTill(proposalsCount); + _proposals.cancelAll(); + + assertEq(_proposals.lastCancelledProposalId, proposalsCount); + } + + // TODO: change this test completely to use getters + function test_get_proposal_info_and_external_calls() external { + ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); + _proposals.submit(address(_executor), expectedCalls); + uint256 proposalId = _proposals.getProposalsCount(); + + (ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) = + _proposals.getProposalInfo(proposalId); + + Timestamp expectedSubmittedAt = Timestamps.now(); + + assertEq(status, ProposalStatus.Submitted); + assertEq(executor, address(_executor)); + assertEq(submittedAt, expectedSubmittedAt); + assertEq(scheduledAt, Timestamps.ZERO); + + ExternalCall[] memory calls = _proposals.getProposalCalls(proposalId); + + assertEq(calls.length, expectedCalls.length); + for (uint256 i = 0; i < calls.length; ++i) { + assertEq(calls[i].value, expectedCalls[i].value); + assertEq(calls[i].target, expectedCalls[i].target); + assertEq(calls[i].payload, expectedCalls[i].payload); + } + + _proposals.schedule(proposalId, Durations.ZERO); + + Timestamp expectedScheduledAt = Timestamps.now(); + + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + + assertEq(status, ProposalStatus.Scheduled); + assertEq(executor, address(_executor)); + assertEq(submittedAt, expectedSubmittedAt); + assertEq(scheduledAt, expectedScheduledAt); + + calls = _proposals.getProposalCalls(proposalId); + + assertEq(calls.length, expectedCalls.length); + for (uint256 i = 0; i < calls.length; ++i) { + assertEq(calls[i].value, expectedCalls[i].value); + assertEq(calls[i].target, expectedCalls[i].target); + assertEq(calls[i].payload, expectedCalls[i].payload); + } + + _proposals.execute(proposalId, Durations.ZERO); + + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + + assertEq(status, ProposalStatus.Executed); + assertEq(executor, address(_executor)); + assertEq(submittedAt, expectedSubmittedAt); + assertEq(scheduledAt, expectedScheduledAt); + + calls = _proposals.getProposalCalls(proposalId); + + assertEq(calls.length, expectedCalls.length); + for (uint256 i = 0; i < calls.length; ++i) { + assertEq(calls[i].value, expectedCalls[i].value); + assertEq(calls[i].target, expectedCalls[i].target); + assertEq(calls[i].payload, expectedCalls[i].payload); + } + } + + function test_get_cancelled_proposal() external { + ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); + _proposals.submit(address(_executor), expectedCalls); + uint256 proposalId = _proposals.getProposalsCount(); + + (ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) = + _proposals.getProposalInfo(proposalId); + + Timestamp expectedSubmittedAt = Timestamps.now(); + + assertEq(status, ProposalStatus.Submitted); + assertEq(executor, address(_executor)); + assertEq(submittedAt, expectedSubmittedAt); + assertEq(scheduledAt, Timestamps.ZERO); + + ExternalCall[] memory calls = _proposals.getProposalCalls(proposalId); + + assertEq(calls.length, expectedCalls.length); + for (uint256 i = 0; i < calls.length; ++i) { + assertEq(calls[i].value, expectedCalls[i].value); + assertEq(calls[i].target, expectedCalls[i].target); + assertEq(calls[i].payload, expectedCalls[i].payload); + } + + ExecutableProposals.cancelAll(_proposals); + + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + + assertEq(status, ProposalStatus.Cancelled); + assertEq(executor, address(_executor)); + assertEq(submittedAt, expectedSubmittedAt); + assertEq(scheduledAt, Timestamps.ZERO); + + calls = _proposals.getProposalCalls(proposalId); + + assertEq(calls.length, expectedCalls.length); + for (uint256 i = 0; i < calls.length; ++i) { + assertEq(calls[i].value, expectedCalls[i].value); + assertEq(calls[i].target, expectedCalls[i].target); + assertEq(calls[i].payload, expectedCalls[i].payload); + } + } + + function testFuzz_get_not_existing_proposal(uint256 proposalId) external { + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); + _proposals.getProposalInfo(proposalId); + + vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); + _proposals.getProposalCalls(proposalId); + } + + function test_count_proposals() external { + assertEq(_proposals.getProposalsCount(), 0); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 1); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 2); + + _proposals.schedule(1, Durations.ZERO); + assertEq(_proposals.getProposalsCount(), 2); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 3); + + _proposals.schedule(2, Durations.ZERO); + assertEq(_proposals.getProposalsCount(), 3); + + _proposals.execute(1, Durations.ZERO); + assertEq(_proposals.getProposalsCount(), 3); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 4); + + _proposals.cancelAll(); + assertEq(_proposals.getProposalsCount(), 4); + } + + function test_can_execute_proposal() external { + Duration delay = Durations.from(100 seconds); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + + assert(!_proposals.canExecute(proposalId, Durations.ZERO)); + + _proposals.schedule(proposalId, Durations.ZERO); + + assert(!_proposals.canExecute(proposalId, delay)); + + _wait(delay); + + assert(_proposals.canExecute(proposalId, delay)); + + _proposals.execute(proposalId, Durations.ZERO); + + assert(!_proposals.canExecute(proposalId, delay)); + } + + function test_can_not_execute_cancelled_proposal() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + _proposals.schedule(proposalId, Durations.ZERO); + + assert(_proposals.canExecute(proposalId, Durations.ZERO)); + _proposals.cancelAll(); + + assert(!_proposals.canExecute(proposalId, Durations.ZERO)); + } + + function test_can_schedule_proposal() external { + Duration delay = Durations.from(100 seconds); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + assert(!_proposals.canSchedule(proposalId, delay)); + + _wait(delay); + + assert(_proposals.canSchedule(proposalId, delay)); + + _proposals.schedule(proposalId, delay); + _proposals.execute(proposalId, Durations.ZERO); + + assert(!_proposals.canSchedule(proposalId, delay)); + } + + function test_can_not_schedule_cancelled_proposal() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.getProposalsCount(); + assert(_proposals.canSchedule(proposalId, Durations.ZERO)); + + _proposals.cancelAll(); + + assert(!_proposals.canSchedule(proposalId, Durations.ZERO)); + } +} diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol deleted file mode 100644 index ff8a544f..00000000 --- a/test/unit/libraries/Proposals.t.sol +++ /dev/null @@ -1,413 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Vm} from "forge-std/Test.sol"; - -import {Executor} from "contracts/Executor.sol"; -import {Proposals, ExecutorCall, Proposal, Status} from "contracts/libraries/Proposals.sol"; - -import {TargetMock} from "test/utils/utils.sol"; -import {UnitTest, Timestamps, Timestamp, Durations, Duration} from "test/utils/unit-test.sol"; - -contract ProposalsUnitTests is UnitTest { - using Proposals for Proposals.State; - - TargetMock private _targetMock; - Proposals.State internal _proposals; - Executor private _executor; - - uint256 private constant PROPOSAL_ID_OFFSET = 1; - - function setUp() external { - _targetMock = new TargetMock(); - _executor = new Executor(address(this)); - } - - function test_submit_reverts_if_empty_proposals() external { - vm.expectRevert(Proposals.EmptyCalls.selector); - Proposals.submit(_proposals, address(0), new ExecutorCall[](0)); - } - - function test_submit_proposal() external { - uint256 proposalsCount = _proposals.count(); - - ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); - - vm.expectEmit(); - emit Proposals.ProposalSubmitted(proposalsCount + PROPOSAL_ID_OFFSET, address(_executor), calls); - - vm.recordLogs(); - - Proposals.submit(_proposals, address(_executor), calls); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); - - proposalsCount = _proposals.count(); - - Proposals.ProposalPacked memory proposal = _proposals.proposals[proposalsCount - PROPOSAL_ID_OFFSET]; - - assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, Timestamps.now()); - assertEq(proposal.executedAt, Timestamps.ZERO); - assertEq(proposal.scheduledAt, Timestamps.ZERO); - assertEq(proposal.calls.length, 1); - - for (uint256 i = 0; i < proposal.calls.length; i++) { - assertEq(proposal.calls[i].target, address(_targetMock)); - assertEq(proposal.calls[i].value, calls[i].value); - assertEq(proposal.calls[i].payload, calls[i].payload); - } - } - - function testFuzz_schedule_proposal(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; - - Timestamp submittedAt = Timestamps.now(); - - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, Timestamps.ZERO); - assertEq(proposal.executedAt, Timestamps.ZERO); - - uint256 proposalId = _proposals.count(); - - _wait(delay); - - vm.expectEmit(); - emit Proposals.ProposalScheduled(proposalId); - Proposals.schedule(_proposals, proposalId, delay); - - proposal = _proposals.proposals[0]; - - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, Timestamps.now()); - assertEq(proposal.executedAt, Timestamps.ZERO); - } - - function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { - vm.assume(proposalId > 0); - - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - } - - function test_cannot_schedule_proposal_twice() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = 1; - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - } - - function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - - _wait(delay.minusSeconds(1 seconds)); - - vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, PROPOSAL_ID_OFFSET)); - Proposals.schedule(_proposals, PROPOSAL_ID_OFFSET, delay); - } - - function test_cannot_schedule_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - Proposals.cancelAll(_proposals); - - uint256 proposalId = _proposals.count(); - - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - } - - function testFuzz_execute_proposal(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - - Timestamp submittedAndScheduledAt = Timestamps.now(); - - assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); - assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); - assertEq(_proposals.proposals[0].executedAt, Timestamps.ZERO); - - _wait(delay); - - // TODO: figure out why event is not emitted - // vm.expectEmit(); - // emit Proposals.ProposalExecuted(); - Proposals.execute(_proposals, proposalId, delay); - - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 0); - - Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; - - assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); - assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); - assertEq(proposal.executedAt, Timestamps.now()); - } - - function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { - vm.assume(proposalId > 0); - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, Durations.ZERO); - } - - function test_cannot_execute_unscheduled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, Durations.ZERO); - } - - function test_cannot_execute_twice() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - Proposals.execute(_proposals, proposalId, Durations.ZERO); - - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, Durations.ZERO); - } - - function test_cannot_execute_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - Proposals.cancelAll(_proposals); - - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, Durations.ZERO); - } - - function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - - _wait(delay.minusSeconds(1 seconds)); - - vm.expectRevert(abi.encodeWithSelector(Proposals.AfterScheduleDelayNotPassed.selector, proposalId)); - Proposals.execute(_proposals, proposalId, delay); - } - - function test_cancel_all_proposals() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - - uint256 proposalsCount = _proposals.count(); - - Proposals.schedule(_proposals, proposalsCount, Durations.ZERO); - - vm.expectEmit(); - emit Proposals.ProposalsCancelledTill(proposalsCount); - Proposals.cancelAll(_proposals); - - assertEq(_proposals.lastCancelledProposalId, proposalsCount); - } - - function test_get_proposal() external { - ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); - Proposals.submit(_proposals, address(_executor), calls); - uint256 proposalId = _proposals.count(); - - Proposal memory proposal = _proposals.get(proposalId); - - Timestamp submittedAt = Timestamps.now(); - - assertEq(proposal.id, proposalId); - assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, Timestamps.ZERO); - assertEq(proposal.executedAt, Timestamps.ZERO); - assertEq(proposal.calls.length, 1); - assert(proposal.status == Status.Submitted); - - for (uint256 i = 0; i < proposal.calls.length; i++) { - assertEq(proposal.calls[i].target, address(_targetMock)); - assertEq(proposal.calls[i].value, calls[i].value); - assertEq(proposal.calls[i].payload, calls[i].payload); - } - - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - - Timestamp scheduledAt = Timestamps.now(); - - proposal = _proposals.get(proposalId); - - assertEq(proposal.id, proposalId); - assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, scheduledAt); - assertEq(proposal.executedAt, Timestamps.ZERO); - assertEq(proposal.calls.length, 1); - assert(proposal.status == Status.Scheduled); - - for (uint256 i = 0; i < proposal.calls.length; i++) { - assertEq(proposal.calls[i].target, address(_targetMock)); - assertEq(proposal.calls[i].value, calls[i].value); - assertEq(proposal.calls[i].payload, calls[i].payload); - } - - Proposals.execute(_proposals, proposalId, Durations.ZERO); - - Timestamp executedAt = Timestamps.now(); - - proposal = _proposals.get(proposalId); - - assertEq(proposal.id, proposalId); - assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, scheduledAt); - assertEq(proposal.executedAt, executedAt); - assertEq(proposal.calls.length, 1); - assert(proposal.status == Status.Executed); - - for (uint256 i = 0; i < proposal.calls.length; i++) { - assertEq(proposal.calls[i].target, address(_targetMock)); - assertEq(proposal.calls[i].value, calls[i].value); - assertEq(proposal.calls[i].payload, calls[i].payload); - } - } - - function test_get_cancelled_proposal() external { - ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); - Proposals.submit(_proposals, address(_executor), calls); - uint256 proposalId = _proposals.count(); - - Proposal memory proposal = _proposals.get(proposalId); - - Timestamp submittedAt = Timestamps.now(); - - assertEq(proposal.id, proposalId); - assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, Timestamps.ZERO); - assertEq(proposal.executedAt, Timestamps.ZERO); - assertEq(proposal.calls.length, 1); - assert(proposal.status == Status.Submitted); - - for (uint256 i = 0; i < proposal.calls.length; i++) { - assertEq(proposal.calls[i].target, address(_targetMock)); - assertEq(proposal.calls[i].value, calls[i].value); - assertEq(proposal.calls[i].payload, calls[i].payload); - } - - Proposals.cancelAll(_proposals); - - proposal = _proposals.get(proposalId); - - assertEq(proposal.id, proposalId); - assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, Timestamps.ZERO); - assertEq(proposal.executedAt, Timestamps.ZERO); - assertEq(proposal.calls.length, 1); - assert(proposal.status == Status.Cancelled); - - for (uint256 i = 0; i < proposal.calls.length; i++) { - assertEq(proposal.calls[i].target, address(_targetMock)); - assertEq(proposal.calls[i].value, calls[i].value); - assertEq(proposal.calls[i].payload, calls[i].payload); - } - } - - function testFuzz_get_not_existing_proposal(uint256 proposalId) external { - vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotFound.selector, proposalId)); - _proposals.get(proposalId); - } - - function test_count_proposals() external { - assertEq(_proposals.count(), 0); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - assertEq(_proposals.count(), 1); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - assertEq(_proposals.count(), 2); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - assertEq(_proposals.count(), 3); - - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - assertEq(_proposals.count(), 4); - - Proposals.schedule(_proposals, 1, Durations.ZERO); - assertEq(_proposals.count(), 4); - - Proposals.schedule(_proposals, 2, Durations.ZERO); - assertEq(_proposals.count(), 4); - - Proposals.execute(_proposals, 1, Durations.ZERO); - assertEq(_proposals.count(), 4); - - Proposals.cancelAll(_proposals); - assertEq(_proposals.count(), 4); - } - - function test_can_execute_proposal() external { - Duration delay = Durations.from(100 seconds); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - - assert(!_proposals.canExecute(proposalId, Durations.ZERO)); - - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - - assert(!_proposals.canExecute(proposalId, delay)); - - _wait(delay); - - assert(_proposals.canExecute(proposalId, delay)); - - Proposals.execute(_proposals, proposalId, Durations.ZERO); - - assert(!_proposals.canExecute(proposalId, delay)); - } - - function test_can_not_execute_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, Durations.ZERO); - - assert(_proposals.canExecute(proposalId, Durations.ZERO)); - Proposals.cancelAll(_proposals); - - assert(!_proposals.canExecute(proposalId, Durations.ZERO)); - } - - function test_can_schedule_proposal() external { - Duration delay = Durations.from(100 seconds); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - assert(!_proposals.canSchedule(proposalId, delay)); - - _wait(delay); - - assert(_proposals.canSchedule(proposalId, delay)); - - Proposals.schedule(_proposals, proposalId, delay); - Proposals.execute(_proposals, proposalId, Durations.ZERO); - - assert(!_proposals.canSchedule(proposalId, delay)); - } - - function test_can_not_schedule_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - uint256 proposalId = _proposals.count(); - assert(_proposals.canSchedule(proposalId, Durations.ZERO)); - - Proposals.cancelAll(_proposals); - - assert(!_proposals.canSchedule(proposalId, Durations.ZERO)); - } -} diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol new file mode 100644 index 00000000..6419a6de --- /dev/null +++ b/test/unit/libraries/Proposers.t.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {UnitTest} from "test/utils/unit-test.sol"; + +import {Proposers} from "contracts/libraries/Proposers.sol"; + +contract ProposersLibraryUnitTests is UnitTest { + using Proposers for Proposers.Context; + + address internal immutable _ADMIN_EXECUTOR = makeAddr("ADMIN_EXECUTOR"); + address internal immutable _ADMIN_PROPOSER = makeAddr("ADMIN_PROPOSER"); + address internal immutable _DEFAULT_EXECUTOR = makeAddr("DEFAULT_EXECUTOR"); + address internal immutable _DEFAULT_PROPOSER = makeAddr("DEFAULT_PROPOSER"); + + Proposers.Context internal _proposers; + + // --- + // register() + // --- + function test_register_HappyPath() external { + // adding admin proposer + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); + + assertEq(allProposers.length, 1); + assertEq(allProposers[0].account, _ADMIN_PROPOSER); + assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); + + // adding non admin proposer + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + allProposers = _proposers.getAllProposers(); + + assertEq(allProposers.length, 2); + assertEq(allProposers[1].account, _DEFAULT_PROPOSER); + assertEq(allProposers[1].executor, _DEFAULT_EXECUTOR); + } + + function test_register_RevertOn_InvalidProposerAccount() external { + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidProposerAccount.selector, address(0))); + _proposers.register(address(0), _ADMIN_EXECUTOR); + } + + function test_register_RevertOn_InvalidExecutor() external { + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); + _proposers.register(_ADMIN_PROPOSER, address(0)); + } + + function test_register_RevertOn_ProposerAlreadyRegistered() external { + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerAlreadyRegistered.selector, _ADMIN_PROPOSER)); + _proposers.register(_ADMIN_PROPOSER, _DEFAULT_EXECUTOR); + } + + function test_register_Emit_ProposerRegistered() external { + vm.expectEmit(true, true, true, false); + emit Proposers.ProposerRegistered(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + } + + // --- + // unregister() + // --- + + function test_unregister_HappyPath() external { + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + assertEq(_proposers.proposers.length, 1); + assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isExecutor(_ADMIN_EXECUTOR)); + + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + assertEq(_proposers.proposers.length, 2); + assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isExecutor(_DEFAULT_EXECUTOR)); + + _proposers.unregister(_DEFAULT_PROPOSER); + assertEq(_proposers.proposers.length, 1); + assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isExecutor(_DEFAULT_EXECUTOR)); + + _proposers.unregister(_ADMIN_PROPOSER); + assertEq(_proposers.proposers.length, 0); + assertFalse(_proposers.isProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isExecutor(_ADMIN_EXECUTOR)); + } + + function test_unregister_RevertOn_ProposerIsNotRegistered() external { + assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + _proposers.unregister(_DEFAULT_PROPOSER); + + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + _proposers.unregister(_DEFAULT_PROPOSER); + } + + function test_uregister_Emit_ProposerUnregistered() external { + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + + vm.expectEmit(true, true, true, false); + emit Proposers.ProposerUnregistered(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + _proposers.unregister(_ADMIN_PROPOSER); + } + + // --- + // getProposer() + // --- + + function test_getProposer_HappyPath() external { + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + + Proposers.Proposer memory adminProposer = _proposers.getProposer(_ADMIN_PROPOSER); + assertEq(adminProposer.account, _ADMIN_PROPOSER); + assertEq(adminProposer.executor, _ADMIN_EXECUTOR); + + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + + Proposers.Proposer memory defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); + assertEq(defaultProposer.account, _DEFAULT_PROPOSER); + assertEq(defaultProposer.executor, _DEFAULT_EXECUTOR); + } + + function test_getProposer_RevertOn_RetrievingUnregisteredProposer() external { + assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + _proposers.getProposer(_DEFAULT_PROPOSER); + + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + _proposers.getProposer(_DEFAULT_PROPOSER); + } + + // --- + // getAllProposer() + // --- + + function test_getAllProposers_HappyPath() external { + Proposers.Proposer[] memory emptyProposers = _proposers.getAllProposers(); + assertEq(emptyProposers.length, 0); + + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + + Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); + assertEq(allProposers.length, 1); + + assertEq(allProposers[0].account, _ADMIN_PROPOSER); + assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); + + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + + allProposers = _proposers.getAllProposers(); + assertEq(allProposers.length, 2); + + assertEq(allProposers[0].account, _ADMIN_PROPOSER); + assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); + + assertEq(allProposers[1].account, _DEFAULT_PROPOSER); + assertEq(allProposers[1].executor, _DEFAULT_EXECUTOR); + } +} diff --git a/test/unit/libraries/SealableCalls.t.sol b/test/unit/libraries/SealableCalls.t.sol new file mode 100644 index 00000000..d847e50f --- /dev/null +++ b/test/unit/libraries/SealableCalls.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {SealableCalls} from "contracts/libraries/SealableCalls.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {SealableMock} from "test/mocks/SealableMock.sol"; + +contract SealableCallsUnitTests is UnitTest { + SealableMock private _sealableMock; + + function setUp() public { + _sealableMock = new SealableMock(); + } + + function testCallPauseForSuccess() public { + (bool success, bytes memory lowLevelError) = SealableCalls.callPauseFor(_sealableMock, 1 days); + + assertTrue(success); + assertEq(lowLevelError.length, 0); + + (bool isPausedSuccess,, bool isPaused) = SealableCalls.callIsPaused(_sealableMock); + assertTrue(isPausedSuccess); + assertTrue(isPaused); + } + + function testCallPauseForFailure() public { + _sealableMock.setShouldRevertPauseFor(true); + + (bool success, bytes memory lowLevelError) = SealableCalls.callPauseFor(_sealableMock, 1 days); + bytes memory expectedError = abi.encodeWithSignature("Error(string)", "pauseFor failed"); + + assertFalse(success); + assertEq(keccak256(lowLevelError), keccak256(expectedError)); + } + + function testCallIsPausedSuccess() public { + SealableCalls.callPauseFor(_sealableMock, 1 days); + + (bool success, bytes memory lowLevelError, bool isPaused) = SealableCalls.callIsPaused(_sealableMock); + + assertTrue(success); + assertTrue(isPaused); + assertEq(lowLevelError.length, 0); + } + + function testCallIsPausedFailure() public { + _sealableMock.setShouldRevertIsPaused(true); + + (bool success, bytes memory lowLevelError, bool isPaused) = SealableCalls.callIsPaused(_sealableMock); + bytes memory expectedError = abi.encodeWithSignature("Error(string)", "isPaused failed"); + + assertFalse(success); + assertFalse(isPaused); + assertEq(keccak256(lowLevelError), keccak256(expectedError)); + } + + function testCallResumeSuccess() public { + SealableCalls.callPauseFor(_sealableMock, 1 days); + + (bool success, bytes memory lowLevelError) = SealableCalls.callResume(_sealableMock); + + assertFalse(success); + assertEq(lowLevelError.length, 0); + + (bool isPausedSuccess,, bool isPaused) = SealableCalls.callIsPaused(_sealableMock); + assertTrue(isPausedSuccess); + assertFalse(isPaused); + } + + function testCallResumeFailure() public { + _sealableMock.setShouldRevertResume(true); + + (bool success, bytes memory lowLevelError) = SealableCalls.callResume(_sealableMock); + bytes memory expectedError = abi.encodeWithSignature("Error(string)", "resume failed"); + + assertFalse(success); + assertEq(keccak256(lowLevelError), keccak256(expectedError)); + } +} diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol new file mode 100644 index 00000000..e823f77d --- /dev/null +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {WithdrawalsBatchesQueue, State} from "contracts/libraries/WithdrawalBatchesQueue.sol"; + +contract WithdrawalsBatchesQueueTest is UnitTest { + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + + uint256 internal constant _DEFAULT_BOUNDARY_UNST_ETH_ID = 777; + WithdrawalsBatchesQueue.Context internal _batchesQueue; + + // --- + // open() + // --- + + function test_open_HappyPath() external { + assertEq(_batchesQueue.info.state, State.Absent); + assertEq(_batchesQueue.batches.length, 0); + + _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); + + assertEq(_batchesQueue.info.state, State.Opened); + assertEq(_batchesQueue.batches.length, 1); + assertEq(_batchesQueue.batches[0].firstUnstETHId, _DEFAULT_BOUNDARY_UNST_ETH_ID); + assertEq(_batchesQueue.batches[0].lastUnstETHId, _DEFAULT_BOUNDARY_UNST_ETH_ID); + } + + function test_open_RevertOn_CallFromOpenedState() external { + _batchesQueue.info.state = State.Opened; + assertEq(_batchesQueue.info.state, State.Opened); + + vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalBatchesQueueIsNotInAbsentState.selector); + _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); + } + + function test_open_Emit_WithdrawalBatchesQueueOpened() external { + assertEq(_batchesQueue.info.state, State.Absent); + + vm.expectEmit(true, false, false, false); + emit WithdrawalsBatchesQueue.WithdrawalBatchesQueueOpened(_DEFAULT_BOUNDARY_UNST_ETH_ID); + + _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); + } + + // --- + // addUnstETHIds() + // --- + + function test_addUnstETHIds_HappyPath_AddIntoNewBatch() external { + _openBatchesQueue(); + + uint256 unstETHIdsLength = 3; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + + uint256[] memory unstETHIds = + _generateFakeUnstETHIds({length: unstETHIdsLength, firstUnstETHId: firstUnstETHId}); + _batchesQueue.addUnstETHIds(unstETHIds); + + assertEq(_batchesQueue.batches.length, 2); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, unstETHIdsLength); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + assertEq(_batchesQueue.info.lastClaimedBatchIndex, 0); + assertEq(_batchesQueue.info.lastClaimedUnstETHIdIndex, 0); + + assertEq(_batchesQueue.batches[1].firstUnstETHId, firstUnstETHId); + assertEq(_batchesQueue.batches[1].lastUnstETHId, firstUnstETHId + unstETHIdsLength - 1); + } + + function test_addUnstETHIds_HappyPath_AddIntoSameBatch() external { + _openBatchesQueue(); + + uint256 firstAddingUnstETHIdsLength = 3; + uint256 firstAddingFirstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + + uint256[] memory firstAddingUnstETHIds = + _generateFakeUnstETHIds({length: firstAddingUnstETHIdsLength, firstUnstETHId: firstAddingFirstUnstETHId}); + + _batchesQueue.addUnstETHIds(firstAddingUnstETHIds); + + assertEq(_batchesQueue.batches.length, 2); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, firstAddingUnstETHIdsLength); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + assertEq(_batchesQueue.info.lastClaimedBatchIndex, 0); + assertEq(_batchesQueue.info.lastClaimedUnstETHIdIndex, 0); + + assertEq(_batchesQueue.batches[1].firstUnstETHId, firstAddingFirstUnstETHId); + assertEq(_batchesQueue.batches[1].lastUnstETHId, firstAddingUnstETHIds[firstAddingUnstETHIds.length - 1]); + + uint256 secondAddingUnstETHIdsLength = 7; + uint256 secondAddingFirstUnstETHId = firstAddingUnstETHIds[firstAddingUnstETHIds.length - 1] + 1; + uint256[] memory secondAddingUnstETHIds = + _generateFakeUnstETHIds({length: secondAddingUnstETHIdsLength, firstUnstETHId: secondAddingFirstUnstETHId}); + + _batchesQueue.addUnstETHIds(secondAddingUnstETHIds); + + assertEq(_batchesQueue.batches.length, 2); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, firstAddingUnstETHIdsLength + secondAddingUnstETHIdsLength); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + assertEq(_batchesQueue.info.lastClaimedBatchIndex, 0); + assertEq(_batchesQueue.info.lastClaimedUnstETHIdIndex, 0); + + assertEq(_batchesQueue.batches[1].firstUnstETHId, firstAddingFirstUnstETHId); + assertEq(_batchesQueue.batches[1].lastUnstETHId, secondAddingUnstETHIds[secondAddingUnstETHIds.length - 1]); + } + + function testFuzz_addUnstETHIds_HappyPath( + uint256 seedUnstETHId, + uint16 unstETHIdsCount, + uint256 firstUnstETHId + ) external { + vm.assume(unstETHIdsCount > 0); + vm.assume(firstUnstETHId > seedUnstETHId); + vm.assume(type(uint256).max - unstETHIdsCount >= firstUnstETHId); + + _openBatchesQueue(seedUnstETHId); + + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + _batchesQueue.addUnstETHIds(unstETHIds); + + assertEq(_batchesQueue.batches.length, 2, "Invalid batches length"); + assertEq(_batchesQueue.batches[1].firstUnstETHId, firstUnstETHId, "Invalid firstUnstETHId value"); + assertEq( + _batchesQueue.batches[1].lastUnstETHId, firstUnstETHId + unstETHIdsCount - 1, "Invalid lastUnstETHId value" + ); + } + + function test_addUnstETHIds_RevertOn_QueueNotInOpenedState() external { + vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalBatchesQueueIsNotInOpenedState.selector); + _batchesQueue.addUnstETHIds(new uint256[](0)); + } + + function test_addUnstETHIds_RevertOn_EmptyUnstETHIdsArray() external { + _openBatchesQueue(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _batchesQueue.addUnstETHIds(new uint256[](0)); + } + + function test_addUnstETHIds_RevertOn_NonSequentialUnstETHIds() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + // violate the order of the NFT ids + unstETHIds[2] = unstETHIds[3]; + + vm.expectRevert(); + _batchesQueue.addUnstETHIds(unstETHIds); + } + + function test_addUnstETHIds_RevertOn_FirstAddedUnstETHIdLessThanLastAddedUnstETHId() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256[] memory invalidUnstETHIdsSequence = + _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: _DEFAULT_BOUNDARY_UNST_ETH_ID - 1}); + + // check for the empty queue & boundary item + vm.expectRevert(WithdrawalsBatchesQueue.InvalidUnstETHIdsSequence.selector); + _batchesQueue.addUnstETHIds(invalidUnstETHIdsSequence); + + // check for the non empty queue + uint256[] memory validUnstETHIdsSequence = + _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: _DEFAULT_BOUNDARY_UNST_ETH_ID + 1}); + _batchesQueue.addUnstETHIds(validUnstETHIdsSequence); + + vm.expectRevert(WithdrawalsBatchesQueue.InvalidUnstETHIdsSequence.selector); + _batchesQueue.addUnstETHIds(invalidUnstETHIdsSequence); + } + + function test_addUnstETHIds_RevertOn_FirstAddedUnstETHIdEqualToLastAddedUnstETHId() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID; + uint256[] memory invalidUnstETHIdsSequence = + _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + // check for the empty queue & boundary item + vm.expectRevert(WithdrawalsBatchesQueue.InvalidUnstETHIdsSequence.selector); + _batchesQueue.addUnstETHIds(invalidUnstETHIdsSequence); + + // check for the non empty queue + uint256[] memory validUnstETHIdsSequence = + _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: _DEFAULT_BOUNDARY_UNST_ETH_ID + 1}); + _batchesQueue.addUnstETHIds(validUnstETHIdsSequence); + + invalidUnstETHIdsSequence = _generateFakeUnstETHIds({ + length: unstETHIdsCount, + firstUnstETHId: validUnstETHIdsSequence[validUnstETHIdsSequence.length - 1] + }); + + vm.expectRevert(WithdrawalsBatchesQueue.InvalidUnstETHIdsSequence.selector); + _batchesQueue.addUnstETHIds(invalidUnstETHIdsSequence); + } + + function test_addUnstETHIds_Emit_UnstETHIdsAdded() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 7; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + vm.expectEmit(true, false, false, false); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstETHIds); + + _batchesQueue.addUnstETHIds(unstETHIds); + } + + // --- + // claimNextBatch() + // --- + + function test_claimNextBatch_HappyPath_MultipleBatches() external { + _openBatchesQueue(); + + uint256 firstBatchUnstETHIdsCount = 5; + uint256 firstBatchFirstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory firstUnstETHIdsBatch = + _generateFakeUnstETHIds({length: firstBatchUnstETHIdsCount, firstUnstETHId: firstBatchFirstUnstETHId}); + _batchesQueue.addUnstETHIds(firstUnstETHIdsBatch); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, firstBatchUnstETHIdsCount); + + uint256 secondBatchUnstETHIdsCount = 13; + uint256 secondBatchFirstUnstETHId = firstUnstETHIdsBatch[firstUnstETHIdsBatch.length - 1] + 1; + uint256[] memory secondUnstETHIdsBatch = + _generateFakeUnstETHIds({length: secondBatchUnstETHIdsCount, firstUnstETHId: secondBatchFirstUnstETHId}); + _batchesQueue.addUnstETHIds(secondUnstETHIdsBatch); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, firstBatchUnstETHIdsCount + secondBatchUnstETHIdsCount); + + uint256 firstResultingBatchUnstETHIdsCount = 8; + uint256[] memory firstResultingBatch = _batchesQueue.claimNextBatch(firstResultingBatchUnstETHIdsCount); + + assertEq(firstResultingBatch.length, firstResultingBatchUnstETHIdsCount); + + // in the first resulting batch must be all unstETHIds from the first added batch of unstETHIds + for (uint256 i = 0; i < firstBatchUnstETHIdsCount; ++i) { + assertEq(firstResultingBatch[i], firstBatchFirstUnstETHId + i); + } + // the rest items is taken from the second adding batch + uint256 firstResultingBatchUnstETHIdsFromSecondAddingBatch = + firstResultingBatchUnstETHIdsCount - firstBatchUnstETHIdsCount; + for (uint256 i = 0; i < firstResultingBatchUnstETHIdsFromSecondAddingBatch; ++i) { + assertEq(firstResultingBatch[firstBatchUnstETHIdsCount + i], secondBatchFirstUnstETHId + i); + } + + uint256[] memory secondResultingBatch = _batchesQueue.claimNextBatch(64); + + assertEq( + secondResultingBatch.length, + firstBatchUnstETHIdsCount + secondBatchUnstETHIdsCount - firstResultingBatchUnstETHIdsCount + ); + + // the rest items is taken from the second adding batch + for (uint256 i = 0; i < secondResultingBatch.length; ++i) { + assertEq( + secondResultingBatch[i], + secondBatchFirstUnstETHId + firstResultingBatchUnstETHIdsFromSecondAddingBatch + i + ); + } + } + + function test_claimNextBatch_HappyPath_SingleBatch() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, unstETHIdsCount); + assertEq(_batchesQueue.batches.length, 2); + + uint256 maxUnstETHIdsCount = 3; + uint256[] memory claimedIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + assertEq(claimedIds.length, maxUnstETHIdsCount); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, maxUnstETHIdsCount); + + for (uint256 i = 0; i < maxUnstETHIdsCount; ++i) { + assertEq(claimedIds[i], _DEFAULT_BOUNDARY_UNST_ETH_ID + i + 1); + } + } + + // --- + // close() + // --- + + function test_close_HappyPath() external { + _openBatchesQueue(); + assertEq(_batchesQueue.info.state, State.Opened); + + _batchesQueue.close(); + assertEq(_batchesQueue.info.state, State.Closed); + } + + function test_close_RevertOn_QueueNotInOpenedState() external { + vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalBatchesQueueIsNotInOpenedState.selector); + _batchesQueue.close(); + + _batchesQueue.open({boundaryUnstETHId: 1}); + _batchesQueue.close(); + + vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalBatchesQueueIsNotInOpenedState.selector); + _batchesQueue.close(); + } + + function test_close_Emit_WithdrawalBatchesQueueClosed() external { + _openBatchesQueue(); + assertEq(_batchesQueue.info.state, State.Opened); + + vm.expectEmit(true, false, false, false); + emit WithdrawalsBatchesQueue.WithdrawalBatchesQueueClosed(); + + _batchesQueue.close(); + } + + // --- + // calcRequestAmounts() + // --- + + function test_calcRequestAmounts_HappyPath_WithoutReminder() external { + _openBatchesQueue(); + assertEq(_batchesQueue.info.state, State.Opened); + + uint256 minRequestAmount = 1; + uint256 maxRequestAmount = 10; + uint256 remainingAmount = 50; + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minRequestAmount, + maxRequestAmount: maxRequestAmount, + remainingAmount: remainingAmount + }); + + uint256[] memory expected = new uint256[](5); + for (uint256 i = 0; i < 5; ++i) { + expected[i] = 10; + } + + assertEq(requestAmounts.length, expected.length); + for (uint256 i = 0; i < requestAmounts.length; ++i) { + assertEq(requestAmounts[i], expected[i]); + } + } + + function test_calcRequestAmounts_HappyPath_WithReminder() external { + _openBatchesQueue(); + assertEq(_batchesQueue.info.state, State.Opened); + + uint256 minRequestAmount = 1; + uint256 maxRequestAmount = 10; + uint256 remainingAmount = 55; + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minRequestAmount, + maxRequestAmount: maxRequestAmount, + remainingAmount: remainingAmount + }); + + uint256[] memory expected = new uint256[](6); + for (uint256 i = 0; i < 5; ++i) { + expected[i] = 10; + } + expected[5] = 5; + + assertEq(requestAmounts.length, expected.length); + for (uint256 i = 0; i < requestAmounts.length; ++i) { + assertEq(requestAmounts[i], expected[i]); + } + } + + // --- + // getNextWithdrawalsBatches() + // --- + + function test_getNextWithdrawalsBatches_HappyPath_SingleBatch() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.batches.length, 2); + + uint256 limit = 3; + uint256[] memory nextIds = _batchesQueue.getNextWithdrawalsBatches(limit); + + assertEq(nextIds.length, limit); + for (uint256 i = 0; i < limit; ++i) { + assertEq(nextIds[i], _DEFAULT_BOUNDARY_UNST_ETH_ID + i + 1); + } + + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + } + + function test_getNextWithdrawalsBatches_HappyPath_MultipleBatches() external { + _openBatchesQueue(); + + uint256 firstAddingUnstETHIdsCount = 5; + uint256 firstAddingFirstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory firstAddingUnstETHIds = + _generateFakeUnstETHIds({length: firstAddingUnstETHIdsCount, firstUnstETHId: firstAddingFirstUnstETHId}); + _batchesQueue.addUnstETHIds(firstAddingUnstETHIds); + + uint256 secondAddingUnstETHIdsCount = 9; + uint256 secondAddingFirstUnstETHId = firstAddingUnstETHIds[firstAddingUnstETHIds.length - 1] + 2; + uint256[] memory secondAddingUnstETHIds = + _generateFakeUnstETHIds({length: secondAddingUnstETHIdsCount, firstUnstETHId: secondAddingFirstUnstETHId}); + _batchesQueue.addUnstETHIds(secondAddingUnstETHIds); + + assertEq(_batchesQueue.batches.length, 3); + + uint256 limit = 7; + uint256[] memory nextIds = _batchesQueue.getNextWithdrawalsBatches(limit); + + assertEq(nextIds.length, limit); + for (uint256 i = 0; i < firstAddingUnstETHIdsCount; ++i) { + assertEq(nextIds[i], _DEFAULT_BOUNDARY_UNST_ETH_ID + i + 1); + } + + for (uint256 i = 0; i < limit - firstAddingUnstETHIdsCount; ++i) { + assertEq( + nextIds[firstAddingUnstETHIdsCount + i], firstAddingUnstETHIds[firstAddingUnstETHIds.length - 1] + i + 2 + ); + } + + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + } + + function test_getNextWithdrawalsBatches_HappyPath_ExactBatch() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.batches.length, 2); + + uint256 limit = 5; + uint256[] memory nextIds = _batchesQueue.getNextWithdrawalsBatches(limit); + + assertEq(nextIds.length, limit); + for (uint256 i = 0; i < limit; ++i) { + assertEq(nextIds[i], _DEFAULT_BOUNDARY_UNST_ETH_ID + i + 1); + } + + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + } + + function test_getNextWithdrawalsBatches_HappyPath_MoreThanAvailable() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.batches.length, 2); + + uint256 limit = 10; + uint256[] memory nextIds = _batchesQueue.getNextWithdrawalsBatches(limit); + + assertEq(nextIds.length, 5); + for (uint256 i = 0; i < 5; ++i) { + assertEq(nextIds[i], _DEFAULT_BOUNDARY_UNST_ETH_ID + i + 1); + } + + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); + } + + // --- + // getBoundaryUnstETHId() + // --- + + function test_getBoundaryUnstETHId_HappyPath() external { + _openBatchesQueue(); + + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + _batchesQueue.addUnstETHIds(unstETHIds); + + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + + _batchesQueue.close(); + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + } + + function test_getBoundaryUnstETHId_RevertOn_QueueInAbsentState() external { + vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalBatchesQueueIsInAbsentState.selector); + _batchesQueue.getBoundaryUnstETHId(); + } + + // --- + // getTotalUnstETHIdsCount() + // --- + + function test_getTotalUnstETHIdsCount_HappyPath() external { + assertEq(_batchesQueue.getTotalUnstETHIdsCount(), 0); + + _openBatchesQueue(); + assertEq(_batchesQueue.getTotalUnstETHIdsCount(), 0); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.getTotalUnstETHIdsCount(), 5); + + _batchesQueue.close(); + assertEq(_batchesQueue.getTotalUnstETHIdsCount(), 5); + } + + // --- + // getLastClaimedOrBoundaryUnstETHId() + // --- + + function test_getLastClaimedOrBoundaryUnstETHId_HappyPath_EmptyQueueReturnsBoundaryUnstETHId() external { + _openBatchesQueue(); + assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + } + + function test_getLastClaimedOrBoundaryUnstETHId_HappyPath_NotEmptyQueueReturnsLastClaimedUnstETHId() external { + _openBatchesQueue(); + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.getTotalUnstETHIdsCount(), 5); + + assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + + uint256 maxUnstETHIdsCount = 3; + _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), unstETHIds[2]); + + _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), unstETHIds[unstETHIds.length - 1]); + } + + function test_getLastClaimedOrBoundaryUnstETHId_RevertOn_AbsentQueueState() external { + vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalBatchesQueueIsInAbsentState.selector); + _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); + } + + // --- + // isClosed() + // --- + + function test_isClosed_HappyPath() external { + assertEq(_batchesQueue.isClosed(), false); + + _openBatchesQueue(); + assertEq(_batchesQueue.isClosed(), false); + + _batchesQueue.close(); + assertEq(_batchesQueue.isClosed(), true); + } + + // --- + // Helper Methods + // --- + + function _openBatchesQueue() internal { + _openBatchesQueue(_DEFAULT_BOUNDARY_UNST_ETH_ID); + } + + function _openBatchesQueue(uint256 seedUnstETHId) internal { + _batchesQueue.open(seedUnstETHId); + assertEq(_batchesQueue.batches.length, 1); + assertEq(_batchesQueue.info.state, State.Opened); + } + + function _generateFakeUnstETHIds(uint256 length, uint256 firstUnstETHId) internal returns (uint256[] memory res) { + res = new uint256[](length); + for (uint256 i = 0; i < length; ++i) { + res[i] = firstUnstETHId + i; + } + } + + function assertEq(State a, State b) internal { + assertEq(uint256(a), uint256(b)); + } +} diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol new file mode 100644 index 00000000..c6464c6f --- /dev/null +++ b/test/utils/SetupDeployment.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; + +// --- +// Types +// --- + +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Durations, Duration} from "contracts/types/Duration.sol"; + +// --- +// Interfaces +// --- +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; + +// --- +// Contracts +// --- +import {TargetMock} from "./target-mock.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; + +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; + +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; + +import {Escrow} from "contracts/Escrow.sol"; +import {ResealManager} from "contracts/ResealManager.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import { + DualGovernanceConfig, + IDualGovernanceConfigProvider, + ImmutableDualGovernanceConfigProvider +} from "contracts/DualGovernanceConfigProvider.sol"; + +import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +// --- +// Util Libraries +// --- + +import {Random} from "./random.sol"; +import {LidoUtils} from "./lido-utils.sol"; + +// --- +// Lido Addresses +// --- + +abstract contract SetupDeployment is Test { + using Random for Random.Context; + // --- + // Helpers + // --- + + Random.Context internal _random; + LidoUtils.Context internal _lido; + + // --- + // Emergency Protected Timelock Deployment Parameters + // --- + + Duration internal immutable _AFTER_SUBMIT_DELAY = Durations.from(3 days); + Duration internal immutable _MAX_AFTER_SUBMIT_DELAY = Durations.from(45 days); + + Duration internal immutable _AFTER_SCHEDULE_DELAY = Durations.from(3 days); + Duration internal immutable _MAX_AFTER_SCHEDULE_DELAY = Durations.from(45 days); + + Duration internal immutable _EMERGENCY_MODE_DURATION = Durations.from(180 days); + Duration internal immutable _MAX_EMERGENCY_MODE_DURATION = Durations.from(365 days); + + Duration internal immutable _EMERGENCY_PROTECTION_DURATION = Durations.from(90 days); + Duration internal immutable _MAX_EMERGENCY_PROTECTION_DURATION = Durations.from(365 days); + + uint256 internal immutable _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = 3; + uint256 internal immutable _EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT = 5; + + uint256 internal immutable _EMERGENCY_EXECUTION_COMMITTEE_QUORUM = 5; + uint256 internal immutable _EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT = 8; + + // --- + // Dual Governance Deployment Parameters + // --- + uint256 internal immutable TIEBREAKER_CORE_QUORUM = 1; + Duration internal immutable TIEBREAKER_EXECUTION_DELAY = Durations.from(30 days); + + uint256 internal immutable TIEBREAKER_SUB_COMMITTEES_COUNT = 2; + uint256 internal immutable TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT = 5; + uint256 internal immutable TIEBREAKER_SUB_COMMITTEE_QUORUM = 5; + + Duration internal immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT = Durations.from(90 days); + Duration internal immutable TIEBREAKER_ACTIVATION_TIMEOUT = Durations.from(365 days); + Duration internal immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT = Durations.from(730 days); + uint256 internal immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; + + // + + // --- + // Emergency Protected Timelock Contracts + // --- + Executor internal _adminExecutor; + EmergencyProtectedTimelock internal _timelock; + TimelockedGovernance internal _emergencyGovernance; + EmergencyActivationCommittee internal _emergencyActivationCommittee; + EmergencyExecutionCommittee internal _emergencyExecutionCommittee; + + // --- + // Dual Governance Contracts + // --- + ResealManager internal _resealManager; + DualGovernance internal _dualGovernance; + ImmutableDualGovernanceConfigProvider internal _dualGovernanceConfigProvider; + + ResealCommittee internal _resealCommittee; + TiebreakerCore internal _tiebreakerCoreCommittee; + TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; + + // --- + // Timelocked Governance Contracts + // --- + TimelockedGovernance internal _timelockedGovernance; + + // --- + // Target Mock Helper Contract + // --- + + TargetMock internal _targetMock; + + // --- + // Constructor + // --- + + constructor(LidoUtils.Context memory lido, Random.Context memory random) { + _lido = lido; + _random = random; + _targetMock = new TargetMock(); + } + + // --- + // Whole Setup Deployments + // --- + + function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { + _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); + _timelockedGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); + _finalizeEmergencyProtectedTimelockDeploy(_timelockedGovernance); + } + + function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { + _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); + _resealManager = _deployResealManager(_timelock); + _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); + _dualGovernance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + + _tiebreakerCoreCommittee = _deployEmptyTiebreakerCoreCommittee({ + owner: address(this), // temporary set owner to deployer, to add sub committees manually + dualGovernance: _dualGovernance, + timelock: TIEBREAKER_EXECUTION_DELAY.toSeconds() + }); + address[] memory coreCommitteeMembers = new address[](TIEBREAKER_SUB_COMMITTEES_COUNT); + + for (uint256 i = 0; i < TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + address[] memory members = _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); + _tiebreakerSubCommittees.push( + _deployTiebreakerSubCommittee({ + owner: address(_adminExecutor), + quorum: TIEBREAKER_SUB_COMMITTEE_QUORUM, + members: members, + tiebreakerCore: _tiebreakerCoreCommittee + }) + ); + coreCommitteeMembers[i] = address(_tiebreakerSubCommittees[i]); + } + + _tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, coreCommitteeMembers.length); + + _tiebreakerCoreCommittee.transferOwnership(address(_adminExecutor)); + + _resealCommittee = _deployResealCommittee(); + + // --- + // Finalize Setup + // --- + _adminExecutor.execute( + address(_dualGovernance), + 0, + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), address(_adminExecutor))) + ); + _adminExecutor.execute( + address(_dualGovernance), + 0, + abi.encodeCall(_dualGovernance.setTiebreakerActivationTimeout, TIEBREAKER_ACTIVATION_TIMEOUT) + ); + _adminExecutor.execute( + address(_dualGovernance), + 0, + abi.encodeCall(_dualGovernance.setTiebreakerCommittee, address(_tiebreakerCoreCommittee)) + ); + _adminExecutor.execute( + address(_dualGovernance), + 0, + abi.encodeCall(_dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(_lido.withdrawalQueue)) + ); + _adminExecutor.execute( + address(_dualGovernance), 0, abi.encodeCall(_dualGovernance.setResealCommittee, address(_resealCommittee)) + ); + + _finalizeEmergencyProtectedTimelockDeploy(_dualGovernance); + + // --- + // Grant Reseal Manager Roles + // --- + vm.startPrank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_resealManager) + ); + _lido.withdrawalQueue.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(_resealManager) + ); + vm.stopPrank(); + } + + // --- + // Emergency Protected Timelock Deployment + // --- + + function _deployEmergencyProtectedTimelockContracts(bool isEmergencyProtectionEnabled) internal { + _adminExecutor = _deployExecutor(address(this)); + _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); + + if (isEmergencyProtectionEnabled) { + _emergencyActivationCommittee = _deployEmergencyActivationCommittee({ + quorum: _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + members: _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT), + owner: address(_adminExecutor), + timelock: _timelock + }); + + _emergencyExecutionCommittee = _deployEmergencyExecutionCommittee({ + quorum: _EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + members: _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT), + owner: address(_adminExecutor), + timelock: _timelock + }); + _emergencyGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); + + _adminExecutor.execute( + address(_timelock), + 0, + abi.encodeCall( + _timelock.setupEmergencyProtection, + ( + address(_emergencyGovernance), + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), + _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + _EMERGENCY_MODE_DURATION + ) + ) + ); + } + } + + function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { + _adminExecutor.execute( + address(_timelock), 0, abi.encodeCall(_timelock.setDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) + ); + _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (address(governance)))); + _adminExecutor.transferOwnership(address(_timelock)); + } + + function _deployExecutor(address owner) internal returns (Executor) { + return new Executor(owner); + } + + function _deployEmergencyProtectedTimelock(Executor adminExecutor) internal returns (EmergencyProtectedTimelock) { + return new EmergencyProtectedTimelock({ + adminExecutor: address(adminExecutor), + sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ + maxAfterSubmitDelay: _MAX_AFTER_SUBMIT_DELAY, + maxAfterScheduleDelay: _MAX_AFTER_SCHEDULE_DELAY, + maxEmergencyModeDuration: _MAX_EMERGENCY_MODE_DURATION, + maxEmergencyProtectionDuration: _MAX_EMERGENCY_PROTECTION_DURATION + }) + }); + } + + function _deployEmergencyActivationCommittee( + EmergencyProtectedTimelock timelock, + address owner, + uint256 quorum, + address[] memory members + ) internal returns (EmergencyActivationCommittee) { + return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); + } + + function _deployEmergencyExecutionCommittee( + EmergencyProtectedTimelock timelock, + address owner, + uint256 quorum, + address[] memory members + ) internal returns (EmergencyExecutionCommittee) { + return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); + } + + function _deployResealCommittee() internal returns (ResealCommittee) { + uint256 quorum = 3; + uint256 membersCount = 5; + address[] memory committeeMembers = new address[](membersCount); + for (uint256 i = 0; i < membersCount; ++i) { + committeeMembers[i] = makeAddr(string(abi.encode(0xFA + i * membersCount + 65))); + } + + return new ResealCommittee(address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), 0); + } + + function _deployTimelockedGovernance( + address governance, + ITimelock timelock + ) internal returns (TimelockedGovernance) { + return new TimelockedGovernance(governance, timelock); + } + + // --- + // Dual Governance Deployment + // --- + + function _deployDualGovernanceConfigProvider() internal returns (ImmutableDualGovernanceConfigProvider) { + return new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% + // + minAssetsLockDuration: Durations.from(5 hours), + dynamicTimelockMinDuration: Durations.from(3 days), + dynamicTimelockMaxDuration: Durations.from(30 days), + // + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionDelay: Durations.from(7 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + } + + function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { + return new ResealManager(timelock); + } + + function _deployDualGovernance( + ITimelock timelock, + IResealManager resealManager, + IDualGovernanceConfigProvider configProvider + ) internal returns (DualGovernance) { + return new DualGovernance({ + dependencies: DualGovernance.ExternalDependencies({ + stETH: _lido.stETH, + wstETH: _lido.wstETH, + withdrawalQueue: _lido.withdrawalQueue, + timelock: timelock, + resealManager: resealManager, + configProvider: configProvider + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: 4, + minTiebreakerActivationTimeout: MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + maxTiebreakerActivationTimeout: MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + maxSealableWithdrawalBlockersCount: MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + }) + }); + } + + function _deployEmptyTiebreakerCoreCommittee( + address owner, + IDualGovernance dualGovernance, + uint256 timelock + ) internal returns (TiebreakerCore) { + return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: timelock}); + } + + function _deployTiebreakerSubCommittee( + address owner, + uint256 quorum, + address[] memory members, + TiebreakerCore tiebreakerCore + ) internal returns (TiebreakerSubCommittee) { + return new TiebreakerSubCommittee({ + owner: owner, + executionQuorum: quorum, + committeeMembers: members, + tiebreakerCore: address(tiebreakerCore) + }); + } + + // --- + // Helper methods + // --- + + function _generateRandomAddresses(uint256 count) internal returns (address[] memory addresses) { + addresses = new address[](count); + for (uint256 i = 0; i < count; ++i) { + addresses[i] = _random.nextAddress(); + } + } +} diff --git a/test/utils/evm-script-utils.sol b/test/utils/evm-script-utils.sol new file mode 100644 index 00000000..6cd91ba2 --- /dev/null +++ b/test/utils/evm-script-utils.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +library EvmScriptUtils { + struct EvmScriptCall { + address target; + bytes data; + } + + function encodeEvmCallScript(address target, bytes memory data) internal pure returns (bytes memory) { + EvmScriptCall[] memory calls = new EvmScriptCall[](1); + calls[0] = EvmScriptCall(target, data); + return encodeEvmCallScript(calls); + } + + function encodeEvmCallScript(EvmScriptCall[] memory calls) internal pure returns (bytes memory) { + bytes memory script = new bytes(4); + script[3] = 0x01; + + for (uint256 i = 0; i < calls.length; ++i) { + EvmScriptCall memory call = calls[i]; + script = bytes.concat(script, bytes20(call.target), bytes4(uint32(call.data.length)), call.data); + } + + return script; + } +} diff --git a/test/utils/executor-calls.sol b/test/utils/executor-calls.sol index 3233d4ba..c5ca8600 100644 --- a/test/utils/executor-calls.sol +++ b/test/utils/executor-calls.sol @@ -1,35 +1,35 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ExecutorCall} from "contracts/interfaces/IExecutor.sol"; +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; -// Syntax sugar for more convenient creation of ExecutorCall arrays -library ExecutorCallHelpers { - // calls with explicit ExecutorCall definition +// Syntax sugar for more convenient creation of ExternalCall arrays +library ExternalCallHelpers { + // calls with explicit ExternalCall definition - function create(ExecutorCall[1] memory calls) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](1); + function create(ExternalCall[1] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](1); for (uint256 i = 0; i < 1; ++i) { res[i] = calls[i]; } } - function create(ExecutorCall[2] memory calls) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](2); + function create(ExternalCall[2] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](2); for (uint256 i = 0; i < 2; ++i) { res[i] = calls[i]; } } - function create(ExecutorCall[3] memory calls) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](3); + function create(ExternalCall[3] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](3); for (uint256 i = 0; i < 3; ++i) { res[i] = calls[i]; } } - function create(ExecutorCall[4] memory calls) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](4); + function create(ExternalCall[4] memory calls) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](4); for (uint256 i = 0; i < 4; ++i) { res[i] = calls[i]; } @@ -40,8 +40,8 @@ library ExecutorCallHelpers { function create( address[1] memory targets, bytes[1] memory payloads - ) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](1); + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](1); res[0].target = targets[0]; res[0].payload = payloads[0]; } @@ -49,8 +49,8 @@ library ExecutorCallHelpers { function create( address[2] memory targets, bytes[2] memory payloads - ) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](2); + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](2); for (uint256 i = 0; i < 2; ++i) { res[i].target = targets[i]; res[i].payload = payloads[i]; @@ -60,8 +60,8 @@ library ExecutorCallHelpers { function create( address[3] memory targets, bytes[3] memory payloads - ) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](3); + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](3); for (uint256 i = 0; i < 3; ++i) { res[i].target = targets[i]; res[i].payload = payloads[i]; @@ -71,8 +71,8 @@ library ExecutorCallHelpers { function create( address[4] memory targets, bytes[4] memory payloads - ) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](4); + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](4); for (uint256 i = 0; i < 4; ++i) { res[i].target = targets[i]; res[i].payload = payloads[i]; @@ -82,8 +82,8 @@ library ExecutorCallHelpers { function create( address[10] memory targets, bytes[10] memory payloads - ) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](10); + ) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](10); for (uint256 i = 0; i < 10; ++i) { res[i].target = targets[i]; res[i].payload = payloads[i]; @@ -92,38 +92,38 @@ library ExecutorCallHelpers { // same target different calls - function create(address target, bytes memory payload) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](1); + function create(address target, bytes memory payload) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](1); res[0].target = target; res[0].payload = payload; } - function create(address target, bytes[2] memory payloads) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](2); + function create(address target, bytes[2] memory payloads) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](2); for (uint256 i = 0; i < 2; ++i) { res[i].target = target; res[i].payload = payloads[i]; } } - function create(address target, bytes[3] memory payloads) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](3); + function create(address target, bytes[3] memory payloads) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](3); for (uint256 i = 0; i < 3; ++i) { res[i].target = target; res[i].payload = payloads[i]; } } - function create(address target, bytes[4] memory payloads) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](4); + function create(address target, bytes[4] memory payloads) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](4); for (uint256 i = 0; i < 4; ++i) { res[i].target = target; res[i].payload = payloads[i]; } } - function create(address target, bytes[5] memory payloads) internal pure returns (ExecutorCall[] memory res) { - res = new ExecutorCall[](5); + function create(address target, bytes[5] memory payloads) internal pure returns (ExternalCall[] memory res) { + res = new ExternalCall[](5); for (uint256 i = 0; i < 5; ++i) { res[i].target = target; res[i].payload = payloads[i]; diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol deleted file mode 100644 index 56660cf4..00000000 --- a/test/utils/interfaces.sol +++ /dev/null @@ -1,117 +0,0 @@ -pragma solidity 0.8.26; - -import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; -import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; - -struct WithdrawalRequestStatus { - uint256 amountOfStETH; - uint256 amountOfShares; - address owner; - uint256 timestamp; - bool isFinalized; - bool isClaimed; -} - -interface IAragonAgent { - function RUN_SCRIPT_ROLE() external pure returns (bytes32); -} - -interface IAragonVoting { - function newVote( - bytes calldata script, - string calldata metadata, - bool castVote, - bool executesIfDecided_deprecated - ) external returns (uint256 voteId); - - function CREATE_VOTES_ROLE() external view returns (bytes32); - function vote(uint256 voteId, bool support, bool executesIfDecided_deprecated) external; - function canExecute(uint256 voteId) external view returns (bool); - function executeVote(uint256 voteId) external; - function votesLength() external view returns (uint256); - function voteTime() external view returns (uint64); - function minAcceptQuorumPct() external view returns (uint64); -} - -interface IAragonACL { - function getPermissionManager(address app, bytes32 role) external view returns (address); - function grantPermission(address grantee, address app, bytes32 role) external; - function hasPermission(address who, address app, bytes32 role) external view returns (bool); -} - -interface IAragonForwarder { - function forward(bytes memory evmScript) external; -} - -interface IStEth is IERC20 { - function STAKING_CONTROL_ROLE() external view returns (bytes32); - function submit(address referral) external payable returns (uint256); - function removeStakingLimit() external; - function sharesOf(address account) external view returns (uint256); - function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256); - function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256); - function getStakeLimitFullInfo() - external - view - returns ( - bool isStakingPaused, - bool isStakingLimitSet, - uint256 currentStakeLimit, - uint256 maxStakeLimit, - uint256 maxStakeLimitGrowthBlocks, - uint256 prevStakeLimit, - uint256 prevStakeBlockNumber - ); - function getTotalShares() external view returns (uint256); -} - -interface IWstETH is IERC20 { - function wrap(uint256 stETHAmount) external returns (uint256); - function unwrap(uint256 wstETHAmount) external returns (uint256); -} - -interface IWithdrawalQueue is IERC721 { - function PAUSE_ROLE() external pure returns (bytes32); - function RESUME_ROLE() external pure returns (bytes32); - - /// @notice Returns amount of ether available for claim for each provided request id - /// @param _requestIds array of request ids - /// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())` - /// @return claimableEthValues amount of claimable ether for each request, amount is equal to 0 if request - /// is not finalized or already claimed - function getClaimableEther( - uint256[] calldata _requestIds, - uint256[] calldata _hints - ) external view returns (uint256[] memory claimableEthValues); - function getWithdrawalStatus(uint256[] calldata _requestIds) - external - view - returns (WithdrawalRequestStatus[] memory statuses); - function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory); - function requestWithdrawals(uint256[] calldata amounts, address owner) external returns (uint256[] memory); - function setApprovalForAll(address _operator, bool _approved) external; - function balanceOf(address owner) external view returns (uint256); - function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); - function getLastRequestId() external view returns (uint256); - function findCheckpointHints( - uint256[] calldata _requestIds, - uint256 _firstIndex, - uint256 _lastIndex - ) external view returns (uint256[] memory hintIds); - function getLastCheckpointIndex() external view returns (uint256); - function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external; - function getLastFinalizedRequestId() external view returns (uint256); - function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable; - function grantRole(bytes32 role, address account) external; - function hasRole(bytes32 role, address account) external view returns (bool); - function isPaused() external view returns (bool); - function resume() external; - function pauseFor(uint256 duration) external; - function getResumeSinceTimestamp() external view returns (uint256); -} - -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} diff --git a/test/utils/interfaces/IAragonACL.sol b/test/utils/interfaces/IAragonACL.sol new file mode 100644 index 00000000..d22e8f0d --- /dev/null +++ b/test/utils/interfaces/IAragonACL.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IAragonACL { + function getPermissionManager(address app, bytes32 role) external view returns (address); + function grantPermission(address grantee, address app, bytes32 role) external; + function hasPermission(address who, address app, bytes32 role) external view returns (bool); +} diff --git a/test/utils/interfaces/IAragonAgent.sol b/test/utils/interfaces/IAragonAgent.sol new file mode 100644 index 00000000..fbbf1f13 --- /dev/null +++ b/test/utils/interfaces/IAragonAgent.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IAragonForwarder} from "./IAragonForwarder.sol"; + +interface IAragonAgent is IAragonForwarder { + function RUN_SCRIPT_ROLE() external pure returns (bytes32); +} diff --git a/test/utils/interfaces/IAragonForwarder.sol b/test/utils/interfaces/IAragonForwarder.sol new file mode 100644 index 00000000..909840de --- /dev/null +++ b/test/utils/interfaces/IAragonForwarder.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IAragonForwarder { + function forward(bytes memory evmScript) external; +} diff --git a/test/utils/interfaces/IAragonVoting.sol b/test/utils/interfaces/IAragonVoting.sol new file mode 100644 index 00000000..cafc3295 --- /dev/null +++ b/test/utils/interfaces/IAragonVoting.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IAragonVoting { + function newVote( + bytes calldata script, + string calldata metadata, + bool castVote, + bool executesIfDecided_deprecated + ) external returns (uint256 voteId); + + function CREATE_VOTES_ROLE() external view returns (bytes32); + function vote(uint256 voteId, bool support, bool executesIfDecided_deprecated) external; + function canExecute(uint256 voteId) external view returns (bool); + function executeVote(uint256 voteId) external; + function votesLength() external view returns (uint256); + function voteTime() external view returns (uint64); + function minAcceptQuorumPct() external view returns (uint64); +} diff --git a/test/utils/interfaces/IPotentiallyDangerousContract.sol b/test/utils/interfaces/IPotentiallyDangerousContract.sol new file mode 100644 index 00000000..62e5b2d5 --- /dev/null +++ b/test/utils/interfaces/IPotentiallyDangerousContract.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IPotentiallyDangerousContract { + function doRegularStaff(uint256 magic) external; + function doRugPool() external; + function doControversialStaff() external; +} diff --git a/test/utils/interfaces/IStETH.sol b/test/utils/interfaces/IStETH.sol new file mode 100644 index 00000000..713854cd --- /dev/null +++ b/test/utils/interfaces/IStETH.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IStETH as IStETHBase} from "contracts/interfaces/IStETH.sol"; + +interface IStETH is IStETHBase { + function getTotalShares() external view returns (uint256); + function sharesOf(address account) external view returns (uint256); + + function removeStakingLimit() external; + function getCurrentStakeLimit() external view returns (uint256); + function getStakeLimitFullInfo() + external + view + returns ( + bool isStakingPaused, + bool isStakingLimitSet, + uint256 currentStakeLimit, + uint256 maxStakeLimit, + uint256 maxStakeLimitGrowthBlocks, + uint256 prevStakeLimit, + uint256 prevStakeBlockNumber + ); + function STAKING_CONTROL_ROLE() external view returns (bytes32); + + function submit(address referral) external payable returns (uint256); +} diff --git a/test/utils/interfaces/IWithdrawalQueue.sol b/test/utils/interfaces/IWithdrawalQueue.sol new file mode 100644 index 00000000..685951a7 --- /dev/null +++ b/test/utils/interfaces/IWithdrawalQueue.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IWithdrawalQueue as IWithdrawalQueueBase} from "contracts/interfaces/IWithdrawalQueue.sol"; + +interface IWithdrawalQueue is IWithdrawalQueueBase { + function getLastRequestId() external view returns (uint256); + function setApprovalForAll(address _operator, bool _approved) external; + function grantRole(bytes32 role, address account) external; + function pauseFor(uint256 duration) external; + function isPaused() external returns (bool); + function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable; +} diff --git a/test/utils/interfaces/IWstETH.sol b/test/utils/interfaces/IWstETH.sol new file mode 100644 index 00000000..3684907f --- /dev/null +++ b/test/utils/interfaces/IWstETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IWstETH as IWstETHBase} from "contracts/interfaces/IWstETH.sol"; + +interface IWstETH is IWstETHBase { +/// @dev event though in the tests there is no need in additional methods of the WstETH token, +/// it's kept for consistency +} diff --git a/test/utils/lido-utils.sol b/test/utils/lido-utils.sol new file mode 100644 index 00000000..38ac97c0 --- /dev/null +++ b/test/utils/lido-utils.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Vm} from "forge-std/Test.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; + +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; + +import {IAragonACL} from "./interfaces/IAragonACL.sol"; +import {IAragonAgent} from "./interfaces/IAragonAgent.sol"; +import {IAragonVoting} from "./interfaces/IAragonVoting.sol"; +import {IAragonForwarder} from "./interfaces/IAragonForwarder.sol"; + +import {EvmScriptUtils} from "./evm-script-utils.sol"; + +import { + ST_ETH, + WST_ETH, + WITHDRAWAL_QUEUE, + DAO_ACL, + LDO_TOKEN, + DAO_AGENT, + DAO_VOTING, + DAO_TOKEN_MANAGER +} from "./mainnet-addresses.sol"; + +uint256 constant ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value + +library LidoUtils { + struct Context { + // core + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + // aragon governance + IAragonACL acl; + IERC20 ldoToken; + IAragonAgent agent; + IAragonVoting voting; + IAragonForwarder tokenManager; + } + + Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + address internal constant DEFAULT_LDO_WHALE = address(0x1D0_1D0_1D0_1D0_1d0_1D0_1D0_1D0_1D0_1d0_1d0_1d0_1D0_1); + + function mainnet() internal pure returns (Context memory ctx) { + ctx.stETH = IStETH(ST_ETH); + ctx.wstETH = IWstETH(WST_ETH); + ctx.withdrawalQueue = IWithdrawalQueue(WITHDRAWAL_QUEUE); + + ctx.acl = IAragonACL(DAO_ACL); + ctx.agent = IAragonAgent(DAO_AGENT); + ctx.voting = IAragonVoting(DAO_VOTING); + ctx.ldoToken = IERC20(LDO_TOKEN); + ctx.tokenManager = IAragonForwarder(DAO_TOKEN_MANAGER); + } + + function calcAmountFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalSupply = self.stETH.totalSupply(); + uint256 amount = + totalSupply * PercentD16.unwrap(percentage) / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00)); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: amount, denominator: totalSupply}); + return amount * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function calcSharesFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalShares = self.stETH.getTotalShares(); + uint256 shares = + totalShares * PercentD16.unwrap(percentage) / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00)); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: shares, denominator: totalShares}); + return shares * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function calcAmountToDepositFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalSupply = self.stETH.totalSupply(); + /// @dev Calculate amount and shares using the following rule: + /// bal / (totalSupply + bal) = percentage => bal = totalSupply * percentage / (1 - percentage) + uint256 amount = totalSupply * PercentD16.unwrap(percentage) + / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00) - percentage); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: amount, denominator: totalSupply + amount}); + return amount * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function calcSharesToDepositFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalShares = self.stETH.getTotalShares(); + /// @dev Calculate amount and shares using the following rule: + /// bal / (totalShares + bal) = percentage => bal = totalShares * percentage / (1 - percentage) + uint256 shares = totalShares * PercentD16.unwrap(percentage) + / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00) - percentage); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: shares, denominator: totalShares + shares}); + return shares * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function submitStETH( + Context memory self, + address account, + uint256 balance + ) internal returns (uint256 sharesMinted) { + vm.deal(account, balance + 0.1 ether); + + vm.prank(account); + sharesMinted = self.stETH.submit{value: balance}(address(0)); + } + + function submitWstETH( + Context memory self, + address account, + uint256 balance + ) internal returns (uint256 wstEthMinted) { + uint256 stEthAmount = self.wstETH.getStETHByWstETH(balance); + submitStETH(self, account, stEthAmount); + + vm.startPrank(account); + self.stETH.approve(address(self.wstETH), stEthAmount); + wstEthMinted = self.wstETH.wrap(stEthAmount); + vm.stopPrank(); + } + + function finalizeWithdrawalQueue(Context memory self) internal { + finalizeWithdrawalQueue(self, self.withdrawalQueue.getLastRequestId()); + } + + function finalizeWithdrawalQueue(Context memory self, uint256 id) internal { + vm.deal(address(self.withdrawalQueue), 10_000_000 ether); + uint256 finalizationShareRate = self.stETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate + vm.prank(address(self.stETH)); + self.withdrawalQueue.finalize(id, finalizationShareRate); + + bytes32 lockedEtherAmountSlot = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); + + vm.store(address(self.withdrawalQueue), lockedEtherAmountSlot, bytes32(address(self.withdrawalQueue).balance)); + } + + /// @param rebaseFactor - rebase factor with 10 ** 16 precision + /// 10 ** 18 => equal to no rebase + /// 10 ** 18 - 1 => equal to decrease equal to 10 ** -18 % + /// 10 ** 18 + 1 => equal to increase equal to 10 ** -18 % + function simulateRebase(Context memory self, PercentD16 rebaseFactor) internal { + bytes32 clBeaconBalanceSlot = keccak256("lido.Lido.beaconBalance"); + uint256 totalSupply = self.stETH.totalSupply(); + + uint256 oldClBalance = uint256(vm.load(address(self.stETH), clBeaconBalanceSlot)); + uint256 newClBalance = PercentD16.unwrap(rebaseFactor) * oldClBalance / 10 ** 18; + + vm.store(address(self.stETH), clBeaconBalanceSlot, bytes32(newClBalance)); + + // validate that total supply of the token updated expectedly + if (rebaseFactor > PercentsD16.fromBasisPoints(100_00)) { + uint256 clBalanceDelta = newClBalance - oldClBalance; + assert(self.stETH.totalSupply() == totalSupply + clBalanceDelta); + } else { + uint256 clBalanceDelta = oldClBalance - newClBalance; + assert(self.stETH.totalSupply() == totalSupply - clBalanceDelta); + } + } + + function removeStakingLimit(Context memory self) external { + bytes32 stakingLimitSlot = keccak256("lido.Lido.stakeLimit"); + uint256 stakingLimitEncodedData = uint256(vm.load(address(self.stETH), stakingLimitSlot)); + // See the self encoding here: https://github.com/lidofinance/lido-dao/blob/5fcedc6e9a9f3ec154e69cff47c2b9e25503a78a/contracts/0.4.24/lib/StakeLimitUtils.sol#L10 + // To remove staking limit, most significant 96 bits must be set to zero + stakingLimitEncodedData &= 2 ** 160 - 1; + vm.store(address(self.stETH), stakingLimitSlot, bytes32(stakingLimitEncodedData)); + assert(self.stETH.getCurrentStakeLimit() == type(uint256).max); + } + + // --- + // ACL + // --- + + function grantPermission(Context memory self, address app, bytes32 role, address grantee) internal { + if (!self.acl.hasPermission(grantee, app, role)) { + address manager = self.acl.getPermissionManager(app, role); + vm.prank(manager); + self.acl.grantPermission(grantee, app, role); + assert(self.acl.hasPermission(grantee, app, role)); + } + } + + // --- + // Aragon Governance + // --- + + function setupLDOWhale(Context memory self, address account) internal { + vm.startPrank(address(self.agent)); + self.ldoToken.transfer(account, self.ldoToken.balanceOf(address(self.agent))); + vm.stopPrank(); + + assert(self.ldoToken.balanceOf(account) >= self.voting.minAcceptQuorumPct()); + + // need to increase block number since MiniMe snapshotting relies on it + vm.roll(block.number + 1); + vm.warp(block.timestamp + 15); + } + + function supportVoteAndWaitTillDecided(Context memory self, uint256 voteId, address voter) internal { + supportVote(self, voteId, voter); + vm.warp(block.timestamp + self.voting.voteTime()); + } + + function supportVote(Context memory self, uint256 voteId, address voter) internal { + vote(self, voteId, voter, true); + } + + function vote(Context memory self, uint256 voteId, address voter, bool support) internal { + vm.prank(voter); + self.voting.vote(voteId, support, false); + } + + // Creates vote with given description and script, votes for it, and waits until it can be executed + function adoptVote( + Context memory self, + string memory description, + bytes memory script + ) internal returns (uint256 voteId) { + if (self.ldoToken.balanceOf(DEFAULT_LDO_WHALE) < self.voting.minAcceptQuorumPct()) { + setupLDOWhale(self, DEFAULT_LDO_WHALE); + } + bytes memory voteScript = EvmScriptUtils.encodeEvmCallScript( + address(self.voting), abi.encodeCall(self.voting.newVote, (script, description, false, false)) + ); + + voteId = self.voting.votesLength(); + + vm.prank(DEFAULT_LDO_WHALE); + self.tokenManager.forward(voteScript); + supportVoteAndWaitTillDecided(self, voteId, DEFAULT_LDO_WHALE); + } + + function executeVote(Context memory self, uint256 voteId) internal { + self.voting.executeVote(voteId); + } +} diff --git a/test/utils/mainnet-addresses.sol b/test/utils/mainnet-addresses.sol index b5db4b7f..8d9643f9 100644 --- a/test/utils/mainnet-addresses.sol +++ b/test/utils/mainnet-addresses.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; address constant DAO_ACL = 0x9895F0F17cc1d1891b6f18ee0b483B6f221b37Bb; diff --git a/test/utils/percents.sol b/test/utils/percents.sol deleted file mode 100644 index 43eff5cd..00000000 --- a/test/utils/percents.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -error InvalidPercentsString(string value); - -struct Percents { - uint256 value; - uint256 precision; -} - -uint256 constant PRECISION = 16; - -function percents(uint256 value) pure returns (Percents memory result) { - result.value = value; - result.precision = PRECISION; -} - -function percents(string memory value) pure returns (Percents memory result) { - result = percents(value, PRECISION); -} - -function percents(string memory value, uint256 precision) pure returns (Percents memory result) { - uint256 integerPart; - uint256 fractionalPart; - uint256 fractionalPartLength; - - bytes memory bvalue = bytes(value); - uint256 length = bytes(value).length; - bytes1 dot = bytes1("."); - - bool isFractionalPart = false; - for (uint256 i = 0; i < length; ++i) { - if (bytes1(bvalue[i]) == dot) { - if (isFractionalPart) { - revert InvalidPercentsString(value); - } - isFractionalPart = true; - } else if (uint8(bvalue[i]) >= 48 && uint8(bvalue[i]) <= 57) { - if (isFractionalPart) { - fractionalPartLength += 1; - fractionalPart = 10 * fractionalPart + (uint8(bvalue[i]) - 48); - } else { - integerPart = 10 * integerPart + (uint8(bvalue[i]) - 48); - } - } else { - revert InvalidPercentsString(value); - } - } - result.precision = precision; - result.value = 10 ** precision * integerPart + 10 ** (precision - fractionalPartLength) * fractionalPart; -} diff --git a/test/utils/random.sol b/test/utils/random.sol new file mode 100644 index 00000000..a84e0bec --- /dev/null +++ b/test/utils/random.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BSL-1.1 +pragma solidity 0.8.26; + +library Random { + struct Context { + bytes32 seed; + bytes32 value; + } + + function create(uint256 seed) internal pure returns (Context memory self) { + self.seed = bytes32(seed); + self.value = self.seed; + } + + function nextUint256(Context storage self) internal returns (uint256) { + return uint256(_nextValue(self)); + } + + /// @param maxValue - exclusive upper bound of the random + /// @return random uint256 in range [0, maxValue). When maxValue is 0, returns 0 + function nextUint256(Context storage self, uint256 maxValue) internal returns (uint256) { + if (maxValue == 0) { + return 0; + } + return nextUint256(self) % maxValue; + } + + /// @param minValue - inclusive lower bound + /// @param maxValue - exclusive upper bound of the random + function nextUint256(Context storage self, uint256 minValue, uint256 maxValue) internal returns (uint256) { + return minValue + nextUint256(self, maxValue - minValue); + } + + function nextBool(Context storage self) internal returns (bool) { + return nextUint256(self) % 2 == 0; + } + + function nextAddress(Context storage self) internal returns (address) { + return address(uint160(nextUint256(self))); + } + + function _nextValue(Context storage self) private returns (bytes32) { + self.value = keccak256(abi.encode(self.value)); + return self.value; + } +} diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 62199d75..5bb066dc 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -1,112 +1,71 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Test} from "forge-std/Test.sol"; -import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Durations, Duration as DurationType} from "contracts/types/Duration.sol"; - +import {console} from "forge-std/Test.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import { - TransparentUpgradeableProxy, - ProxyAdmin -} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; -import {IConfiguration, Configuration} from "contracts/Configuration.sol"; -import {Executor} from "contracts/Executor.sol"; - -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; -import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; - -import {ResealManager} from "contracts/ResealManager.sol"; - -import { - ExecutorCall, - EmergencyState, - EmergencyProtection, - EmergencyProtectedTimelock -} from "contracts/EmergencyProtectedTimelock.sol"; - -import {SingleGovernance, IGovernance} from "contracts/SingleGovernance.sol"; -import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernance.sol"; - -import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; - -import {Percents, percents} from "../utils/percents.sol"; -import { - IERC20, - IStEth, - IWstETH, - IWithdrawalQueue, - WithdrawalRequestStatus, - IDangerousContract -} from "../utils/interfaces.sol"; -import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; -import {Utils, TargetMock, console} from "../utils/utils.sol"; - -import {DAO_VOTING, ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -struct Balances { - uint256 stETHAmount; - uint256 stETHShares; - uint256 wstETHAmount; - uint256 wstETHShares; -} +// --- +// Types +// --- -uint256 constant PERCENTS_PRECISION = 16; +import {PercentD16} from "contracts/types/PercentD16.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -function countDigits(uint256 number) pure returns (uint256 digitsCount) { - do { - digitsCount++; - } while (number / 10 != 0); -} +import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; -DurationType constant ONE_SECOND = DurationType.wrap(1); +// --- +// Interfaces +// --- -contract ScenarioTestBlueprint is Test { - address internal immutable _ADMIN_PROPOSER = DAO_VOTING; - DurationType internal immutable _EMERGENCY_MODE_DURATION = Durations.from(180 days); - DurationType internal immutable _EMERGENCY_PROTECTION_DURATION = Durations.from(90 days); - address internal immutable _EMERGENCY_ACTIVATION_COMMITTEE = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); - address internal immutable _EMERGENCY_EXECUTION_COMMITTEE = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); +import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; - DurationType internal immutable _SEALING_DURATION = Durations.from(14 days); - DurationType internal immutable _SEALING_COMMITTEE_LIFETIME = Durations.from(365 days); - address internal immutable _SEALING_COMMITTEE = makeAddr("SEALING_COMMITTEE"); +// --- +// Main Contracts +// --- - IStEth public immutable _ST_ETH = IStEth(ST_ETH); - IWstETH public immutable _WST_ETH = IWstETH(WST_ETH); - IWithdrawalQueue public immutable _WITHDRAWAL_QUEUE = IWithdrawalQueue(WITHDRAWAL_QUEUE); +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; +import {ProposalStatus, EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {IGovernance} from "contracts/TimelockedGovernance.sol"; +import {State as DGState, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; - EmergencyActivationCommittee internal _emergencyActivationCommittee; - EmergencyExecutionCommittee internal _emergencyExecutionCommittee; - TiebreakerCore internal _tiebreakerCommittee; - TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; +// --- +// Test Utils +// --- - TargetMock internal _target; +import {TargetMock} from "../utils/target-mock.sol"; - IConfiguration internal _config; - IConfiguration internal _configImpl; - ProxyAdmin internal _configProxyAdmin; - TransparentUpgradeableProxy internal _configProxy; +import {Random} from "../utils/random.sol"; +import {ExternalCallHelpers} from "../utils/executor-calls.sol"; - Escrow internal _escrowMasterCopy; +import {LidoUtils, EvmScriptUtils} from "./lido-utils.sol"; - Executor internal _adminExecutor; +import {EvmScriptUtils} from "../utils/evm-script-utils.sol"; - EmergencyProtectedTimelock internal _timelock; - SingleGovernance internal _singleGovernance; - DualGovernance internal _dualGovernance; +import {SetupDeployment} from "./SetupDeployment.sol"; +import {TestingAssertEqExtender} from "./testing-assert-eq-extender.sol"; - ResealManager internal _resealManager; +uint256 constant FORK_BLOCK_NUMBER = 20218312; - address[] internal _sealableWithdrawalBlockers = [WITHDRAWAL_QUEUE]; +contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { + using LidoUtils for LidoUtils.Context; + + constructor() SetupDeployment(LidoUtils.mainnet(), Random.create(block.timestamp)) { + /// Maybe not the best idea to do it in the constructor, consider move it into setUp method + vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); + vm.rollFork(FORK_BLOCK_NUMBER); + _lido.removeStakingLimit(); + } // --- // Helper Getters // --- + + function _getAdminExecutor() internal view returns (address) { + return _timelock.getAdminExecutor(); + } + function _getVetoSignallingEscrow() internal view returns (Escrow) { return Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); } @@ -116,8 +75,10 @@ contract ScenarioTestBlueprint is Test { return Escrow(payable(rageQuitEscrow)); } - function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { - return ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); + function _getMockTargetRegularStaffCalls() internal view returns (ExternalCall[] memory) { + return ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (42)) + ); } function _getVetoSignallingState() @@ -125,13 +86,11 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - DurationType duration_; - Timestamp activatedAt_; - Timestamp enteredAt_; - (isActive, duration_, activatedAt_, enteredAt_) = _dualGovernance.getVetoSignallingState(); - duration = DurationType.unwrap(duration_); - enteredAt = Timestamp.unwrap(enteredAt_); - activatedAt = Timestamp.unwrap(activatedAt_); + DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + isActive = stateContext.state == DGState.VetoSignalling; + duration = _dualGovernance.getDynamicDelayDuration().toSeconds(); + enteredAt = stateContext.enteredAt.toSeconds(); + activatedAt = stateContext.vetoSignallingActivatedAt.toSeconds(); } function _getVetoSignallingDeactivationState() @@ -139,68 +98,77 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 enteredAt) { - Timestamp enteredAt_; - DurationType duration_; - (isActive, duration_, enteredAt_) = _dualGovernance.getVetoSignallingDeactivationState(); - duration = DurationType.unwrap(duration_); - enteredAt = Timestamp.unwrap(enteredAt_); + DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + isActive = stateContext.state == DGState.VetoSignallingDeactivation; + duration = _dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().toSeconds(); + enteredAt = stateContext.enteredAt.toSeconds(); } // --- - // Network Configuration + // Balances Manipulation // --- - function _selectFork() internal { - Utils.selectFork(); + + function _setupStETHBalance(address account, uint256 amount) internal { + _lido.submitStETH(account, amount); } - // --- - // Balances Manipulation - // --- + function _setupStETHBalance(address account, PercentD16 tvlPercentage) internal { + _lido.submitStETH(account, _lido.calcAmountToDepositFromPercentageOfTVL(tvlPercentage)); + } - function _depositStETH( - address account, - uint256 amountToMint - ) internal returns (uint256 sharesMinted, uint256 amountMinted) { - return Utils.depositStETH(account, amountToMint); + function _setupWstETHBalance(address account, uint256 amount) internal { + _lido.submitWstETH(account, amount); } - function _setupStETHWhale(address vetoer) internal returns (uint256 shares, uint256 amount) { - Utils.removeLidoStakingLimit(); - return Utils.setupStETHWhale(vetoer, percents("10.0")); + function _setupWstETHBalance(address account, PercentD16 tvlPercentage) internal { + _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _setupStETHWhale( - address vetoer, - Percents memory vetoPowerInPercents - ) internal returns (uint256 shares, uint256 amount) { - Utils.removeLidoStakingLimit(); - return Utils.setupStETHWhale(vetoer, vetoPowerInPercents); + function _submitStETH( + address account, + uint256 amountToMint + ) internal returns (uint256 sharesMinted, uint256 amountMinted) { + _lido.submitStETH(account, amountToMint); } function _getBalances(address vetoer) internal view returns (Balances memory balances) { - uint256 stETHAmount = _ST_ETH.balanceOf(vetoer); - uint256 wstETHShares = _WST_ETH.balanceOf(vetoer); + uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); + uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); balances = Balances({ stETHAmount: stETHAmount, - stETHShares: _ST_ETH.getSharesByPooledEth(stETHAmount), - wstETHAmount: _ST_ETH.getPooledEthByShares(wstETHShares), + stETHShares: _lido.stETH.getSharesByPooledEth(stETHAmount), + wstETHAmount: _lido.stETH.getPooledEthByShares(wstETHShares), wstETHShares: wstETHShares }); } + // --- + // Withdrawal Queue Operations + // --- + function _finalizeWithdrawalQueue() internal { + _lido.finalizeWithdrawalQueue(); + } + + function _finalizeWithdrawalQueue(uint256 id) internal { + _lido.finalizeWithdrawalQueue(id); + } + + function _simulateRebase(PercentD16 rebaseFactor) internal { + _lido.simulateRebase(rebaseFactor); + } + // --- // Escrow Manipulation // --- - function _lockStETH(address vetoer, Percents memory vetoPowerInPercents) internal returns (uint256 amount) { - (, amount) = _setupStETHWhale(vetoer, vetoPowerInPercents); - _lockStETH(vetoer, amount); + function _lockStETH(address vetoer, PercentD16 tvlPercentage) internal { + _lockStETH(vetoer, _lido.calcAmountFromPercentageOfTVL(tvlPercentage)); } function _lockStETH(address vetoer, uint256 amount) internal { Escrow escrow = _getVetoSignallingEscrow(); vm.startPrank(vetoer); - if (_ST_ETH.allowance(vetoer, address(escrow)) < amount) { - _ST_ETH.approve(address(escrow), amount); + if (_lido.stETH.allowance(vetoer, address(escrow)) < amount) { + _lido.stETH.approve(address(escrow), amount); } escrow.lockStETH(amount); vm.stopPrank(); @@ -212,11 +180,15 @@ contract ScenarioTestBlueprint is Test { vm.stopPrank(); } + function _lockWstETH(address vetoer, PercentD16 tvlPercentage) internal { + _lockStETH(vetoer, _lido.calcSharesFromPercentageOfTVL(tvlPercentage)); + } + function _lockWstETH(address vetoer, uint256 amount) internal { Escrow escrow = _getVetoSignallingEscrow(); vm.startPrank(vetoer); - if (_WST_ETH.allowance(vetoer, address(escrow)) < amount) { - _WST_ETH.approve(address(escrow), amount); + if (_lido.wstETH.allowance(vetoer, address(escrow)) < amount) { + _lido.wstETH.approve(address(escrow), amount); } escrow.lockWstETH(amount); vm.stopPrank(); @@ -224,7 +196,7 @@ contract ScenarioTestBlueprint is Test { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); - uint256 wstETHBalanceBefore = _WST_ETH.balanceOf(vetoer); + uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); @@ -234,7 +206,7 @@ contract ScenarioTestBlueprint is Test { // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before // sending funds to the user assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); - assertApproxEqAbs(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(_lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { @@ -243,19 +215,19 @@ contract ScenarioTestBlueprint is Test { LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesLocked += statuses[i].amountOfShares; } vm.startPrank(vetoer); - _WITHDRAWAL_QUEUE.setApprovalForAll(address(escrow), true); + _lido.withdrawalQueue.setApprovalForAll(address(escrow), true); escrow.lockUnstETH(unstETHIds); - _WITHDRAWAL_QUEUE.setApprovalForAll(address(escrow), false); + _lido.withdrawalQueue.setApprovalForAll(address(escrow), false); vm.stopPrank(); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), address(escrow)); + assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); @@ -274,16 +246,17 @@ contract ScenarioTestBlueprint is Test { LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesUnlocked = 0; - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesUnlocked += statuses[i].amountOfShares; } - vm.prank(vetoer); + vm.startPrank(vetoer); escrow.unlockUnstETH(unstETHIds); + vm.stopPrank(); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), vetoer); + assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); @@ -307,28 +280,50 @@ contract ScenarioTestBlueprint is Test { // --- // Proposals Submission // --- + function _submitProposalViaDualGovernance( + string memory description, + ExternalCall[] memory calls + ) internal returns (uint256 proposalId) { + proposalId = _submitProposal(_dualGovernance, description, calls); + } + + function _submitProposalViaTimelockedGovernance( + string memory description, + ExternalCall[] memory calls + ) internal returns (uint256 proposalId) { + proposalId = _submitProposal(_timelockedGovernance, description, calls); + } + function _submitProposal( IGovernance governance, string memory description, - ExecutorCall[] memory calls + ExternalCall[] memory calls ) internal returns (uint256 proposalId) { uint256 proposalsCountBefore = _timelock.getProposalsCount(); bytes memory script = - Utils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submitProposal, (calls))); - uint256 voteId = Utils.adoptVote(DAO_VOTING, description, script); + EvmScriptUtils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submitProposal, (calls))); + uint256 voteId = _lido.adoptVote(description, script); // The scheduled calls count is the same until the vote is enacted assertEq(_timelock.getProposalsCount(), proposalsCountBefore); // executing the vote - Utils.executeVote(DAO_VOTING, voteId); + _lido.executeVote(voteId); proposalId = _timelock.getProposalsCount(); // new call is scheduled but has not executable yet assertEq(proposalId, proposalsCountBefore + 1); } + function _scheduleProposalViaDualGovernance(uint256 proposalId) internal { + _scheduleProposal(_dualGovernance, proposalId); + } + + function _scheduleProposalViaTimelockedGovernance(uint256 proposalId) internal { + _scheduleProposal(_timelockedGovernance, proposalId); + } + function _scheduleProposal(IGovernance governance, uint256 proposalId) internal { governance.scheduleProposal(proposalId); } @@ -346,22 +341,21 @@ contract ScenarioTestBlueprint is Test { // Assertions // --- - function _assertSubmittedProposalData(uint256 proposalId, ExecutorCall[] memory calls) internal { - _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), calls); + function _assertSubmittedProposalData(uint256 proposalId, ExternalCall[] memory calls) internal { + _assertSubmittedProposalData(proposalId, _timelock.getAdminExecutor(), calls); } - function _assertSubmittedProposalData(uint256 proposalId, address executor, ExecutorCall[] memory calls) internal { - Proposal memory proposal = _timelock.getProposal(proposalId); + function _assertSubmittedProposalData(uint256 proposalId, address executor, ExternalCall[] memory calls) internal { + EmergencyProtectedTimelock.Proposal memory proposal = _timelock.getProposal(proposalId); assertEq(proposal.id, proposalId, "unexpected proposal id"); - assertEq(uint256(proposal.status), uint256(ProposalStatus.Submitted), "unexpected status value"); + assertEq(proposal.status, ProposalStatus.Submitted, "unexpected status value"); assertEq(proposal.executor, executor, "unexpected executor"); assertEq(Timestamp.unwrap(proposal.submittedAt), block.timestamp, "unexpected scheduledAt"); - assertEq(Timestamp.unwrap(proposal.executedAt), 0, "unexpected executedAt"); assertEq(proposal.calls.length, calls.length, "unexpected calls length"); for (uint256 i = 0; i < proposal.calls.length; ++i) { - ExecutorCall memory expected = calls[i]; - ExecutorCall memory actual = proposal.calls[i]; + ExternalCall memory expected = calls[i]; + ExternalCall memory actual = proposal.calls[i]; assertEq(actual.value, expected.value); assertEq(actual.target, expected.target); @@ -369,8 +363,8 @@ contract ScenarioTestBlueprint is Test { } } - function _assertTargetMockCalls(address sender, ExecutorCall[] memory calls) internal { - TargetMock.Call[] memory called = _target.getCalls(); + function _assertTargetMockCalls(address sender, ExternalCall[] memory calls) internal { + TargetMock.Call[] memory called = _targetMock.getCalls(); assertEq(called.length, calls.length); for (uint256 i = 0; i < calls.length; ++i) { @@ -379,11 +373,11 @@ contract ScenarioTestBlueprint is Test { assertEq(called[i].data, calls[i].payload); assertEq(called[i].blockNumber, block.number); } - _target.reset(); + _targetMock.reset(); } - function _assertTargetMockCalls(address[] memory senders, ExecutorCall[] memory calls) internal { - TargetMock.Call[] memory called = _target.getCalls(); + function _assertTargetMockCalls(address[] memory senders, ExternalCall[] memory calls) internal { + TargetMock.Call[] memory called = _targetMock.getCalls(); assertEq(called.length, calls.length); assertEq(called.length, senders.length); @@ -393,15 +387,23 @@ contract ScenarioTestBlueprint is Test { assertEq(called[i].data, calls[i].payload, "Unexpected payload"); assertEq(called[i].blockNumber, block.number); } - _target.reset(); + _targetMock.reset(); } function _assertCanExecute(uint256 proposalId, bool canExecute) internal { assertEq(_timelock.canExecute(proposalId), canExecute, "unexpected canExecute() value"); } + function _assertCanScheduleViaDualGovernance(uint256 proposalId, bool canSchedule) internal { + _assertCanSchedule(_dualGovernance, proposalId, canSchedule); + } + + function _assertCanScheduleViaTimelockedGovernance(uint256 proposalId, bool canSchedule) internal { + _assertCanSchedule(_timelockedGovernance, proposalId, canSchedule); + } + function _assertCanSchedule(IGovernance governance, uint256 proposalId, bool canSchedule) internal { - assertEq(governance.canSchedule(proposalId), canSchedule, "unexpected canSchedule() value"); + assertEq(governance.canScheduleProposal(proposalId), canSchedule, "unexpected canSchedule() value"); } function _assertCanScheduleAndExecute(IGovernance governance, uint256 proposalId) internal { @@ -414,46 +416,54 @@ contract ScenarioTestBlueprint is Test { function _assertProposalSubmitted(uint256 proposalId) internal { assertEq( - _timelock.getProposal(proposalId).status, ProposalStatus.Submitted, "Proposal not in 'Submitted' state" + _timelock.getProposal(proposalId).status, + ProposalStatus.Submitted, + "TimelockProposal not in 'Submitted' state" ); } function _assertProposalScheduled(uint256 proposalId) internal { assertEq( - _timelock.getProposal(proposalId).status, ProposalStatus.Scheduled, "Proposal not in 'Scheduled' state" + _timelock.getProposal(proposalId).status, + ProposalStatus.Scheduled, + "TimelockProposal not in 'Scheduled' state" ); } function _assertProposalExecuted(uint256 proposalId) internal { - assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Executed, "Proposal not in 'Executed' state"); + assertEq( + _timelock.getProposal(proposalId).status, + ProposalStatus.Executed, + "TimelockProposal not in 'Executed' state" + ); } - function _assertProposalCanceled(uint256 proposalId) internal { + function _assertProposalCancelled(uint256 proposalId) internal { assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Cancelled, "Proposal not in 'Canceled' state"); } function _assertNormalState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.Normal)); + assertEq(_dualGovernance.getCurrentState(), DGState.Normal); } function _assertVetoSignalingState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignalling)); + assertEq(_dualGovernance.getCurrentState(), DGState.VetoSignalling); } function _assertVetoSignalingDeactivationState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignallingDeactivation)); + assertEq(_dualGovernance.getCurrentState(), DGState.VetoSignallingDeactivation); } function _assertRageQuitState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.RageQuit)); + assertEq(_dualGovernance.getCurrentState(), DGState.RageQuit); } function _assertVetoCooldownState() internal { - assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoCooldown)); + assertEq(_dualGovernance.getCurrentState(), DGState.VetoCooldown); } function _assertNoTargetMockCalls() internal { - assertEq(_target.getCalls().length, 0, "Unexpected target calls count"); + assertEq(_targetMock.getCalls().length, 0, "Unexpected target calls count"); } // --- @@ -511,158 +521,6 @@ contract ScenarioTestBlueprint is Test { /* solhint-enable no-console */ } - // --- - // Test Setup Deployment - // --- - - function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { - _deployAdminExecutor(address(this)); - _deployConfigImpl(); - _deployConfigProxy(address(this)); - _deployEscrowMasterCopy(); - _deployUngovernedTimelock(); - _deployDualGovernance(); - _deployEmergencyActivationCommittee(); - _deployEmergencyExecutionCommittee(); - _deployTiebreaker(); - _finishTimelockSetup(address(_dualGovernance), isEmergencyProtectionEnabled); - } - - function _deploySingleGovernanceSetup(bool isEmergencyProtectionEnabled) internal { - _deployAdminExecutor(address(this)); - _deployConfigImpl(); - _deployConfigProxy(address(this)); - _deployEscrowMasterCopy(); - _deployUngovernedTimelock(); - _deploySingleGovernance(); - _deployEmergencyActivationCommittee(); - _deployEmergencyExecutionCommittee(); - _deployTiebreaker(); - _finishTimelockSetup(address(_singleGovernance), isEmergencyProtectionEnabled); - } - - function _deployTarget() internal { - _target = new TargetMock(); - } - - function _deployAdminExecutor(address owner) internal { - _adminExecutor = new Executor(owner); - } - - function _deployConfigImpl() internal { - _configImpl = new Configuration(address(_adminExecutor), address(DAO_VOTING), _sealableWithdrawalBlockers); - } - - function _deployConfigProxy(address owner) internal { - _configProxy = new TransparentUpgradeableProxy(address(_configImpl), address(owner), new bytes(0)); - _configProxyAdmin = ProxyAdmin(Utils.predictDeployedAddress(address(_configProxy), 1)); - _config = Configuration(address(_configProxy)); - } - - function _deployUngovernedTimelock() internal { - _timelock = new EmergencyProtectedTimelock(address(_config)); - } - - function _deploySingleGovernance() internal { - _singleGovernance = new SingleGovernance(address(_config), DAO_VOTING, address(_timelock)); - } - - function _deployDualGovernance() internal { - _dualGovernance = - new DualGovernance(address(_config), address(_timelock), address(_escrowMasterCopy), _ADMIN_PROPOSER); - } - - function _deployEscrowMasterCopy() internal { - _escrowMasterCopy = new Escrow(ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, address(_config)); - } - - function _deployTiebreaker() internal { - uint256 subCommitteeMembersCount = 5; - uint256 subCommitteeQuorum = 5; - uint256 subCommitteesCount = 2; - - _tiebreakerCommittee = - new TiebreakerCore(address(_adminExecutor), new address[](0), 1, address(_dualGovernance), 0); - - for (uint256 i = 0; i < subCommitteesCount; ++i) { - address[] memory committeeMembers = new address[](subCommitteeMembersCount); - for (uint256 j = 0; j < subCommitteeMembersCount; j++) { - committeeMembers[j] = makeAddr(string(abi.encode(i + j * subCommitteeMembersCount + 65))); - } - _tiebreakerSubCommittees.push( - new TiebreakerSubCommittee( - address(_adminExecutor), committeeMembers, subCommitteeQuorum, address(_tiebreakerCommittee) - ) - ); - - vm.prank(address(_adminExecutor)); - _tiebreakerCommittee.addMember(address(_tiebreakerSubCommittees[i]), i + 1); - } - } - - function _deployEmergencyActivationCommittee() internal { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * membersCount + 65))); - } - _emergencyActivationCommittee = - new EmergencyActivationCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); - } - - function _deployEmergencyExecutionCommittee() internal { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFD + i * membersCount + 65))); - } - _emergencyExecutionCommittee = - new EmergencyExecutionCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); - } - - function _finishTimelockSetup(address governance, bool isEmergencyProtectionEnabled) internal { - if (isEmergencyProtectionEnabled) { - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtection, - ( - address(_emergencyActivationCommittee), - address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION, - _EMERGENCY_MODE_DURATION - ) - ) - ); - } - - _resealManager = new ResealManager(address(_timelock)); - - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_resealManager) - ); - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole( - 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(_resealManager) - ); - - if (governance == address(_dualGovernance)) { - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall( - _dualGovernance.setTiebreakerProtection, (address(_tiebreakerCommittee), address(_resealManager)) - ) - ); - } - _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (governance))); - _adminExecutor.transferOwnership(address(_timelock)); - } - // --- // Utils Methods // --- @@ -672,25 +530,25 @@ contract ScenarioTestBlueprint is Test { console.log(string.concat(">>> ", text, " <<<")); } - function _wait(DurationType duration) internal { + function _wait(Duration duration) internal { vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } function _waitAfterSubmitDelayPassed() internal { - _wait(_config.AFTER_SUBMIT_DELAY() + ONE_SECOND); + _wait(_timelock.getAfterSubmitDelay() + Durations.from(1 seconds)); } function _waitAfterScheduleDelayPassed() internal { - _wait(_config.AFTER_SCHEDULE_DELAY() + ONE_SECOND); + _wait(_timelock.getAfterScheduleDelay() + Durations.from(1 seconds)); } - function _executeEmergencyActivate() internal { + function _executeActivateEmergencyMode() internal { address[] memory members = _emergencyActivationCommittee.getMembers(); for (uint256 i = 0; i < _emergencyActivationCommittee.quorum(); ++i) { vm.prank(members[i]); - _emergencyActivationCommittee.approveEmergencyActivate(); + _emergencyActivationCommittee.approveActivateEmergencyMode(); } - _emergencyActivationCommittee.executeEmergencyActivate(); + _emergencyActivationCommittee.executeActivateEmergencyMode(); } function _executeEmergencyExecute(uint256 proposalId) internal { @@ -711,21 +569,21 @@ contract ScenarioTestBlueprint is Test { _emergencyExecutionCommittee.executeEmergencyReset(); } - struct Duration { + struct DurationStruct { uint256 _days; uint256 _hours; uint256 _minutes; uint256 _seconds; } - function _toDuration(uint256 timestamp) internal view returns (Duration memory duration) { + function _toDuration(uint256 timestamp) internal view returns (DurationStruct memory duration) { duration._days = timestamp / 1 days; duration._hours = (timestamp - 1 days * duration._days) / 1 hours; duration._minutes = (timestamp - 1 days * duration._days - 1 hours * duration._hours) / 1 minutes; duration._seconds = timestamp % 1 minutes; } - function _formatDuration(Duration memory duration) internal pure returns (string memory) { + function _formatDuration(DurationStruct memory duration) internal pure returns (string memory) { // format example: 1d:22h:33m:12s return string( abi.encodePacked( @@ -740,37 +598,4 @@ contract ScenarioTestBlueprint is Test { ) ); } - - function assertEq(uint40 a, uint40 b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(Timestamp a, Timestamp b) internal { - assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); - } - - function assertEq(DurationType a, DurationType b) internal { - assertEq(uint256(DurationType.unwrap(a)), uint256(DurationType.unwrap(b))); - } - - function assertEq(ProposalStatus a, ProposalStatus b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(ProposalStatus a, ProposalStatus b, string memory message) internal { - assertEq(uint256(a), uint256(b), message); - } - - function assertEq(State a, State b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(Balances memory b1, Balances memory b2, uint256 stETHSharesEpsilon) internal { - assertEq(b1.wstETHShares, b2.wstETHShares); - assertEq(b1.wstETHAmount, b2.wstETHAmount); - - uint256 stETHAmountEpsilon = _ST_ETH.getPooledEthByShares(stETHSharesEpsilon); - assertApproxEqAbs(b1.stETHShares, b2.stETHShares, stETHSharesEpsilon); - assertApproxEqAbs(b1.stETHAmount, b2.stETHAmount, stETHAmountEpsilon); - } } diff --git a/test/utils/target-mock.sol b/test/utils/target-mock.sol new file mode 100644 index 00000000..8e2f19c3 --- /dev/null +++ b/test/utils/target-mock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// May be used as a mock contract to collect method calls +contract TargetMock { + struct Call { + uint256 value; + address sender; + uint256 blockNumber; + bytes data; + } + + Call[] public calls; + + function getCallsLength() external view returns (uint256) { + return calls.length; + } + + function getCalls() external view returns (Call[] memory calls_) { + calls_ = calls; + } + + function reset() external { + for (uint256 i = 0; i < calls.length; ++i) { + calls.pop(); + } + } + + fallback() external payable { + calls.push(Call({value: msg.value, sender: msg.sender, blockNumber: block.number, data: msg.data})); + } + + receive() external payable {} +} diff --git a/test/utils/testing-assert-eq-extender.sol b/test/utils/testing-assert-eq-extender.sol new file mode 100644 index 00000000..c8f78d7d --- /dev/null +++ b/test/utils/testing-assert-eq-extender.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; + +import {Duration} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; + +import {ProposalStatus} from "contracts/EmergencyProtectedTimelock.sol"; +import {State as DualGovernanceState} from "contracts/DualGovernance.sol"; + +contract TestingAssertEqExtender is Test { + struct Balances { + uint256 stETHAmount; + uint256 stETHShares; + uint256 wstETHAmount; + uint256 wstETHShares; + } + + function assertEq(Duration a, Duration b) internal { + assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); + } + + function assertEq(Timestamp a, Timestamp b) internal { + assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); + } + + function assertEq(ProposalStatus a, ProposalStatus b) internal { + assertEq(uint256(a), uint256(b)); + } + + function assertEq(ProposalStatus a, ProposalStatus b, string memory message) internal { + assertEq(uint256(a), uint256(b), message); + } + + function assertEq(DualGovernanceState a, DualGovernanceState b) internal { + assertEq(uint256(a), uint256(b)); + } + + function assertEq(Balances memory b1, Balances memory b2, uint256 sharesEpsilon) internal { + assertEq(b1.wstETHShares, b2.wstETHShares); + assertEq(b1.wstETHAmount, b2.wstETHAmount); + + assertApproxEqAbs(b1.stETHShares, b2.stETHShares, sharesEpsilon); + assertApproxEqAbs(b1.stETHAmount, b2.stETHAmount, sharesEpsilon); + } + + function assertEq(PercentD16 a, PercentD16 b) internal { + assertEq(PercentD16.unwrap(a), PercentD16.unwrap(b)); + } +} diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol index 8e4ff80e..2e41ec31 100644 --- a/test/utils/unit-test.sol +++ b/test/utils/unit-test.sol @@ -1,29 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Duration} from "contracts/types/Duration.sol"; // solhint-disable-next-line -import {Test, console} from "forge-std/Test.sol"; -import {ExecutorCall} from "contracts/libraries/Proposals.sol"; -import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; -import {IDangerousContract} from "test/utils/interfaces.sol"; +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; +import {ExternalCallHelpers} from "test/utils/executor-calls.sol"; +import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; +import {TestingAssertEqExtender} from "./testing-assert-eq-extender.sol"; -contract UnitTest is Test { +contract UnitTest is TestingAssertEqExtender { function _wait(Duration duration) internal { vm.warp(block.timestamp + Duration.unwrap(duration)); } - function _getTargetRegularStaffCalls(address targetMock) internal pure returns (ExecutorCall[] memory) { - return ExecutorCallHelpers.create(address(targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); - } - - function assertEq(Timestamp a, Timestamp b) internal { - assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); - } - - function assertEq(Duration a, Duration b) internal { - assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); + function _getMockTargetRegularStaffCalls(address targetMock) internal pure returns (ExternalCall[] memory) { + return ExternalCallHelpers.create( + address(targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (42)) + ); } } diff --git a/test/utils/utils.sol b/test/utils/utils.sol deleted file mode 100644 index a259cfd1..00000000 --- a/test/utils/utils.sol +++ /dev/null @@ -1,211 +0,0 @@ -pragma solidity 0.8.26; - -// solhint-disable-next-line -import "forge-std/console2.sol"; -import "forge-std/Vm.sol"; -import "forge-std/Test.sol"; - -import {stdStorage, StdStorage} from "forge-std/Test.sol"; - -import {Percents, percents} from "../utils/percents.sol"; - -import "./mainnet-addresses.sol"; -import "./interfaces.sol"; - -// May be used as a mock contract to collect method calls -contract TargetMock { - struct Call { - uint256 value; - address sender; - uint256 blockNumber; - bytes data; - } - - Call[] public calls; - - function getCallsLength() external view returns (uint256) { - return calls.length; - } - - function getCalls() external view returns (Call[] memory calls_) { - calls_ = calls; - } - - function reset() external { - for (uint256 i = 0; i < calls.length; ++i) { - calls.pop(); - } - } - - fallback() external payable { - calls.push(Call({value: msg.value, sender: msg.sender, blockNumber: block.number, data: msg.data})); - } -} - -library Utils { - using stdStorage for StdStorage; - - Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - - struct EvmScriptCall { - address target; - bytes data; - } - - function selectFork() internal { - vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); - vm.rollFork(20218312); - } - - function encodeEvmCallScript(address target, bytes memory data) internal pure returns (bytes memory) { - EvmScriptCall[] memory calls = new EvmScriptCall[](1); - calls[0] = EvmScriptCall(target, data); - return encodeEvmCallScript(calls); - } - - function encodeEvmCallScript(EvmScriptCall[] memory calls) internal pure returns (bytes memory) { - bytes memory script = new bytes(4); - script[3] = 0x01; - - for (uint256 i = 0; i < calls.length; ++i) { - EvmScriptCall memory call = calls[i]; - script = bytes.concat(script, bytes20(call.target), bytes4(uint32(call.data.length)), call.data); - } - - return script; - } - - function setupLdoWhale(address addr) internal { - vm.startPrank(DAO_AGENT); - IERC20(LDO_TOKEN).transfer(addr, IERC20(LDO_TOKEN).balanceOf(DAO_AGENT)); - vm.stopPrank(); - // solhint-disable-next-line - console.log( - "LDO whale %x balance: %d LDO at block %d", addr, IERC20(LDO_TOKEN).balanceOf(addr) / 10 ** 18, block.number - ); - assert(IERC20(LDO_TOKEN).balanceOf(addr) >= IAragonVoting(DAO_VOTING).minAcceptQuorumPct()); - // need to increase block number since MiniMe snapshotting relies on it - vm.roll(block.number + 1); - vm.warp(block.timestamp + 15); - } - - function setupStETHWhale(address addr) internal returns (uint256 shares, uint256 balance) { - // 15% of total stETH supply - return setupStETHWhale(addr, percents("30.00")); - } - - function setupStETHWhale( - address addr, - Percents memory totalSupplyPercentage - ) internal returns (uint256 shares, uint256 balance) { - uint256 ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value - // bal / (totalSupply + bal) = percentage => bal = totalSupply * percentage / (1 - percentage) - shares = ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION - + IStEth(ST_ETH).getTotalShares() * totalSupplyPercentage.value - / (100 * 10 ** totalSupplyPercentage.precision - totalSupplyPercentage.value); - // to compensate StETH wei lost on submit/transfers, generate slightly larger eth amount - return depositStETH(addr, IStEth(ST_ETH).getPooledEthByShares(shares)); - } - - function depositStETH( - address addr, - uint256 amountToMint - ) internal returns (uint256 sharesMinted, uint256 amountMinted) { - uint256 sharesBalanceBefore = IStEth(ST_ETH).sharesOf(addr); - uint256 amountBalanceBefore = IStEth(ST_ETH).balanceOf(addr); - - // solhint-disable-next-line - console.log("setting ETH balance of address %x to %d ETH", addr, amountToMint / 10 ** 18); - vm.deal(addr, amountToMint); - vm.prank(addr); - IStEth(ST_ETH).submit{value: amountToMint}(address(0)); - - sharesMinted = IStEth(ST_ETH).sharesOf(addr) - sharesBalanceBefore; - amountMinted = IStEth(ST_ETH).balanceOf(addr) - amountBalanceBefore; - - // solhint-disable-next-line - console.log("stETH balance of address %x: %d stETH", addr, (amountMinted) / 10 ** 18); - } - - function removeLidoStakingLimit() external { - grantPermission(ST_ETH, IStEth(ST_ETH).STAKING_CONTROL_ROLE(), address(this)); - (, bool isStakingLimitSet,,,,,) = IStEth(ST_ETH).getStakeLimitFullInfo(); - if (isStakingLimitSet) { - IStEth(ST_ETH).removeStakingLimit(); - } - // solhint-disable-next-line - console.log("Lido staking limit removed"); - } - - function grantPermission(address app, bytes32 role, address grantee) internal { - IAragonACL acl = IAragonACL(DAO_ACL); - if (!acl.hasPermission(grantee, app, role)) { - // solhint-disable-next-line - console.log("granting permission %x on %x to %x", uint256(role), app, grantee); - address manager = acl.getPermissionManager(app, role); - vm.prank(manager); - acl.grantPermission(grantee, app, role); - assert(acl.hasPermission(grantee, app, role)); - } - } - - function supportVoteAndWaitTillDecided(uint256 voteId, address voter) internal { - supportVote(voteId, voter); - vm.warp(block.timestamp + IAragonVoting(DAO_VOTING).voteTime()); - } - - function supportVote(uint256 voteId, address voter) internal { - vote(voteId, voter, true); - } - - function vote(uint256 voteId, address voter, bool support) internal { - // solhint-disable-next-line - console.log("voting from %x at block %d", voter, block.number); - vm.prank(voter); - IAragonVoting(DAO_VOTING).vote(voteId, support, false); - } - - // Creates vote with given description and script, votes for it, and waits until it can be executed - function adoptVote( - address voting, - string memory description, - bytes memory script - ) internal returns (uint256 voteId) { - uint256 ldoWhalePrivateKey = uint256(keccak256(abi.encodePacked("LDO_WHALE"))); - address ldoWhale = vm.addr(ldoWhalePrivateKey); - if (IERC20(LDO_TOKEN).balanceOf(ldoWhale) < IAragonVoting(DAO_VOTING).minAcceptQuorumPct()) { - setupLdoWhale(ldoWhale); - } - bytes memory voteScript = Utils.encodeEvmCallScript( - voting, abi.encodeCall(IAragonVoting.newVote, (script, description, false, false)) - ); - - voteId = IAragonVoting(voting).votesLength(); - - vm.prank(ldoWhale); - IAragonForwarder(DAO_TOKEN_MANAGER).forward(voteScript); - supportVoteAndWaitTillDecided(voteId, ldoWhale); - } - - function executeVote(address voting, uint256 voteId) internal { - IAragonVoting(voting).executeVote(voteId); - } - - function predictDeployedAddress(address _origin, uint256 _nonce) public pure returns (address) { - bytes memory data; - if (_nonce == 0x00) { - data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(0x80)); - } else if (_nonce <= 0x7f) { - data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(uint8(_nonce))); - } else if (_nonce <= 0xff) { - data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), _origin, bytes1(0x81), uint8(_nonce)); - } else if (_nonce <= 0xffff) { - data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), _origin, bytes1(0x82), uint16(_nonce)); - } else if (_nonce <= 0xffffff) { - data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), _origin, bytes1(0x83), uint24(_nonce)); - } else { - data = abi.encodePacked(bytes1(0xda), bytes1(0x94), _origin, bytes1(0x84), uint32(_nonce)); - } - return address(uint160(uint256(keccak256(data)))); - } -} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..4ce5fa17 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4185 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + +"@babel/code-frame@^7.0.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@ethereumjs/rlp@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" + integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== + +"@ethereumjs/util@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4" + integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== + dependencies: + "@ethereumjs/rlp" "^4.0.1" + ethereum-cryptography "^2.0.0" + micro-ftch "^0.3.1" + +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + +"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + +"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + +"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + +"@ethersproject/contracts@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" + integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + +"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" + integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" + integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + +"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" + integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + +"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/providers@5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/solidity@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" + integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + +"@ethersproject/units@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" + integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/wallet@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" + integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/json-wallets" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" + integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + +"@metamask/eth-sig-util@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" + integrity sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ== + dependencies: + ethereumjs-abi "^0.6.8" + ethereumjs-util "^6.2.1" + ethjs-util "^0.1.6" + tweetnacl "^1.0.3" + tweetnacl-util "^0.15.1" + +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" + integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@1.4.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" + integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nomicfoundation/edr-darwin-arm64@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.4.1.tgz#210e6b5eaff9278814e8f19800182d1071554855" + integrity sha512-XuiUUnWAVNw7JYv7nRqDWfpBm21HOxCRBQ8lQnRnmiets9Ss2X5Ul9mvBheIPh/D0wBzwJ8TRtsSrorpwE79cA== + +"@nomicfoundation/edr-darwin-x64@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.4.1.tgz#81e660de77d1d73317c9a5140349d1197cddef9a" + integrity sha512-N1MfJqEX5ixaXlyyrHnaYxzwIT27Nc/jUgLI7ts4/9kRvPTvyZRYmXS1ciKhmUFr/WvFckTCix2RJbZoGGtX7g== + +"@nomicfoundation/edr-linux-arm64-gnu@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.4.1.tgz#6e1ce12080a35505c7f3eaf772f4e171db8b7f9a" + integrity sha512-bSPOfmcFjJwDgWOV5kgZHeqg2OWu1cINrHSGjig0aVHehjcoX4Sgayrj6fyAxcOV5NQKA6WcyTFll6NrCxzWRA== + +"@nomicfoundation/edr-linux-arm64-musl@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.4.1.tgz#a467a6c8631053d10a8641f67618b9bdf057c636" + integrity sha512-F/+DgOdeBFQDrk+SX4aFffJFBgJfd75ZtE2mjcWNAh/qWiS7NfUxdQX/5OvNo/H6EY4a+3bZH6Bgzqg4mEWvMw== + +"@nomicfoundation/edr-linux-x64-gnu@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.4.1.tgz#63753d05767b4bc0d4f9f9be8399928c790c931e" + integrity sha512-POHhTWczIXCPhzKtY0Vt/l+VCqqCx5gNR5ErwSrNnLz/arfQobZFAU+nc61BX3Jch82TW8b3AbfGI73Kh7gO0w== + +"@nomicfoundation/edr-linux-x64-musl@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.4.1.tgz#44d128b9a09e3f61b08617213a58cd84dd15c418" + integrity sha512-uu8oNp4Ozg3H1x1We0FF+rwXfFiAvsOm5GQ+OBx9YYOXnfDPWqguQfGIkhrti9GD0iYhfQ/WOG5wvp0IzzgGSg== + +"@nomicfoundation/edr-win32-x64-msvc@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.4.1.tgz#1667b725337ca6f27ec58c63337b6a62a0d7ed09" + integrity sha512-PaZHFw455z89ZiKYNTnKu+/TiVZVRI+mRJsbRTe2N0VlYfUBS1o2gdXBM12oP1t198HR7xQwEPPAslTFxGBqHA== + +"@nomicfoundation/edr@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.4.1.tgz#7d698454d228ffc5399f1c58799104b53e1b60ae" + integrity sha512-NgrMo2rI9r28uidumvd+K2/AJLdxtXsUlJr3hj/pM6S1FCd/HiWaLeLa/cjCVPcE2u1rYAa3W6UFxLCB7S5Dhw== + dependencies: + "@nomicfoundation/edr-darwin-arm64" "0.4.1" + "@nomicfoundation/edr-darwin-x64" "0.4.1" + "@nomicfoundation/edr-linux-arm64-gnu" "0.4.1" + "@nomicfoundation/edr-linux-arm64-musl" "0.4.1" + "@nomicfoundation/edr-linux-x64-gnu" "0.4.1" + "@nomicfoundation/edr-linux-x64-musl" "0.4.1" + "@nomicfoundation/edr-win32-x64-msvc" "0.4.1" + +"@nomicfoundation/ethereumjs-common@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.4.tgz#9901f513af2d4802da87c66d6f255b510bef5acb" + integrity sha512-9Rgb658lcWsjiicr5GzNCjI1llow/7r0k50dLL95OJ+6iZJcVbi15r3Y0xh2cIO+zgX0WIHcbzIu6FeQf9KPrg== + dependencies: + "@nomicfoundation/ethereumjs-util" "9.0.4" + +"@nomicfoundation/ethereumjs-rlp@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.4.tgz#66c95256fc3c909f6fb18f6a586475fc9762fa30" + integrity sha512-8H1S3s8F6QueOc/X92SdrA4RDenpiAEqMg5vJH99kcQaCy/a3Q6fgseo75mgWlbanGJXSlAPtnCeG9jvfTYXlw== + +"@nomicfoundation/ethereumjs-tx@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.4.tgz#b0ceb58c98cc34367d40a30d255d6315b2f456da" + integrity sha512-Xjv8wAKJGMrP1f0n2PeyfFCCojHd7iS3s/Ab7qzF1S64kxZ8Z22LCMynArYsVqiFx6rzYy548HNVEyI+AYN/kw== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + ethereum-cryptography "0.1.3" + +"@nomicfoundation/ethereumjs-util@9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.4.tgz#84c5274e82018b154244c877b76bc049a4ed7b38" + integrity sha512-sLOzjnSrlx9Bb9EFNtHzK/FJFsfg2re6bsGqinFinH1gCqVfz9YYlXiMWwDM4C/L4ywuHFCYwfKTVr/QHQcU0Q== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + ethereum-cryptography "0.1.3" + +"@nomicfoundation/hardhat-chai-matchers@^2.0.2": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.0.7.tgz#89d90b2d77a00f6fd8fe42eabe40a82b5e065075" + integrity sha512-RQfsiTwdf0SP+DtuNYvm4921X6VirCQq0Xyh+mnuGlTwEFSPZ/o27oQC+l+3Y/l48DDU7+ZcYBR+Fp+Rp94LfQ== + dependencies: + "@types/chai-as-promised" "^7.1.3" + chai-as-promised "^7.1.1" + deep-eql "^4.0.1" + ordinal "^1.0.3" + +"@nomicfoundation/hardhat-ethers@^3.0.0": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.6.tgz#e8ba7f9719de360c03501b85dae4999bb3a7e1c5" + integrity sha512-/xzkFQAaHQhmIAYOQmvHBPwL+NkwLzT9gRZBsgWUYeV+E6pzXsBQsHfRYbAZ3XEYare+T7S+5Tg/1KDJgepSkA== + dependencies: + debug "^4.1.1" + lodash.isequal "^4.5.0" + +"@nomicfoundation/hardhat-foundry@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.1.2.tgz#4f5aaa1803b8f5d974dcbc361beb72d49c815562" + integrity sha512-f5Vhj3m2qvKGpr6NAINYwNgILDsai8dVCsFb1rAVLkJxOmD2pAtfCmOH5SBVr9yUI5B1z9rbTwPBJVrqnb+PXQ== + dependencies: + chalk "^2.4.2" + +"@nomicfoundation/hardhat-network-helpers@^1.0.0": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.11.tgz#64096829661b960b88679bd5c4fbcb50654672d1" + integrity sha512-uGPL7QSKvxrHRU69dx8jzoBvuztlLCtyFsbgfXIwIjnO3dqZRz2GNMHJoO3C3dIiUNM6jdNF4AUnoQKDscdYrA== + dependencies: + ethereumjs-util "^7.1.4" + +"@nomicfoundation/hardhat-toolbox@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-3.0.0.tgz#83e2c28a745aa4eb1236072166367b0de68b4c76" + integrity sha512-MsteDXd0UagMksqm9KvcFG6gNKYNa3GGNCy73iQ6bEasEgg2v8Qjl6XA5hjs8o5UD5A3153B6W2BIVJ8SxYUtA== + +"@nomicfoundation/hardhat-verify@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-verify/-/hardhat-verify-1.1.1.tgz#6a433d777ce0172d1f0edf7f2d3e1df14b3ecfc1" + integrity sha512-9QsTYD7pcZaQFEA3tBb/D/oCStYDiEVDN7Dxeo/4SCyHRSm86APypxxdOMEPlGmXsAvd+p1j/dTODcpxb8aztA== + dependencies: + "@ethersproject/abi" "^5.1.2" + "@ethersproject/address" "^5.0.2" + cbor "^8.1.0" + chalk "^2.4.2" + debug "^4.1.1" + lodash.clonedeep "^4.5.0" + semver "^6.3.0" + table "^6.8.0" + undici "^5.14.0" + +"@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz#3a9c3b20d51360b20affb8f753e756d553d49557" + integrity sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw== + +"@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz#74dcfabeb4ca373d95bd0d13692f44fcef133c28" + integrity sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw== + +"@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz#4af5849a89e5a8f511acc04f28eb5d4460ba2b6a" + integrity sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA== + +"@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz#54036808a9a327b2ff84446c130a6687ee702a8e" + integrity sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA== + +"@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz#466cda0d6e43691986c944b909fc6dbb8cfc594e" + integrity sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g== + +"@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz#2b35826987a6e94444140ac92310baa088ee7f94" + integrity sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg== + +"@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz#e6363d13b8709ca66f330562337dbc01ce8bbbd9" + integrity sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA== + +"@nomicfoundation/solidity-analyzer@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz#8bcea7d300157bf3a770a851d9f5c5e2db34ac55" + integrity sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA== + optionalDependencies: + "@nomicfoundation/solidity-analyzer-darwin-arm64" "0.1.2" + "@nomicfoundation/solidity-analyzer-darwin-x64" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-arm64-musl" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-x64-gnu" "0.1.2" + "@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2" + "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2" + +"@openzeppelin/contracts@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.1.tgz#93da90fc209a0a4ff09c1deb037fbb35e4020890" + integrity sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w== + +"@pnpm/config.env-replace@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" + integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== + +"@pnpm/network.ca-file@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" + integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== + dependencies: + graceful-fs "4.2.10" + +"@pnpm/npm-conf@^2.1.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" + integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== + dependencies: + "@pnpm/config.env-replace" "^1.1.0" + "@pnpm/network.ca-file" "^1.0.1" + config-chain "^1.1.11" + +"@scure/base@~1.1.0", "@scure/base@~1.1.6": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" + integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== + +"@scure/bip32@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" + integrity sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw== + dependencies: + "@noble/hashes" "~1.2.0" + "@noble/secp256k1" "~1.7.0" + "@scure/base" "~1.1.0" + +"@scure/bip32@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" + integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + +"@scure/bip39@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" + integrity sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg== + dependencies: + "@noble/hashes" "~1.2.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" + integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== + dependencies: + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + +"@sentry/core@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" + integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/hub@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" + integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== + dependencies: + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/minimal@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" + integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/types" "5.30.0" + tslib "^1.9.3" + +"@sentry/node@^5.18.1": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" + integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== + dependencies: + "@sentry/core" "5.30.0" + "@sentry/hub" "5.30.0" + "@sentry/tracing" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/tracing@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" + integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/types@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" + integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== + +"@sentry/utils@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" + integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== + dependencies: + "@sentry/types" "5.30.0" + tslib "^1.9.3" + +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + +"@solidity-parser/parser@^0.14.0": + version "0.14.5" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804" + integrity sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg== + dependencies: + antlr4ts "^0.5.0-alpha.4" + +"@solidity-parser/parser@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.18.0.tgz#8e77a02a09ecce957255a2f48c9a7178ec191908" + integrity sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA== + +"@szmarczak/http-timer@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" + integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + dependencies: + defer-to-connect "^2.0.1" + +"@types/bn.js@^4.11.3": + version "4.11.6" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" + integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg== + dependencies: + "@types/node" "*" + +"@types/bn.js@^5.1.0": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" + integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== + dependencies: + "@types/node" "*" + +"@types/chai-as-promised@^7.1.3": + version "7.1.8" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz#f2b3d82d53c59626b5d6bbc087667ccb4b677fe9" + integrity sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw== + dependencies: + "@types/chai" "*" + +"@types/chai@*": + version "4.3.16" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.16.tgz#b1572967f0b8b60bf3f87fe1d854a5604ea70c82" + integrity sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ== + +"@types/concat-stream@^1.6.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@types/concat-stream/-/concat-stream-1.6.1.tgz#24bcfc101ecf68e886aaedce60dfd74b632a1b74" + integrity sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA== + dependencies: + "@types/node" "*" + +"@types/form-data@0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-0.0.33.tgz#c9ac85b2a5fd18435b8c85d9ecb50e6d6c893ff8" + integrity sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw== + dependencies: + "@types/node" "*" + +"@types/glob@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/http-cache-semantics@^4.0.2": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + +"@types/lru-cache@^5.1.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" + integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== + +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/node@*": + version "20.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" + integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ== + dependencies: + undici-types "~5.26.4" + +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + +"@types/node@^10.0.3": + version "10.17.60" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== + +"@types/node@^8.0.0": + version "8.10.66" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3" + integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== + +"@types/pbkdf2@^3.0.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.2.tgz#2dc43808e9985a2c69ff02e2d2027bd4fe33e8dc" + integrity sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew== + dependencies: + "@types/node" "*" + +"@types/qs@^6.2.31": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/secp256k1@^4.0.1": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.6.tgz#d60ba2349a51c2cbc5e816dcd831a42029d376bf" + integrity sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ== + dependencies: + "@types/node" "*" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + integrity sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q== + +adm-zip@^0.4.16: + version "0.4.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" + integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== + +aes-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== + +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.12.6: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== + dependencies: + fast-deep-equal "^3.1.3" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.4.1" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg== + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@^4.1.1, ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-escapes@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" + integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.0.0, ansi-styles@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +antlr4@^4.13.1-patch-1: + version "4.13.1-patch-1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1-patch-1.tgz#946176f863f890964a050c4f18c47fd6f7e57602" + integrity sha512-OjFLWWLzDMV9rdFhpvroCWR4ooktNg9/nvVYSA5z28wuVpU36QUNuioR1XLnQtcjVlf8npjyz593PxnU/f/Cow== + +antlr4ts@^0.5.0-alpha.4: + version "0.5.0-alpha.4" + resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" + integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +ast-parents@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" + integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@1.x: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.5.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.10" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.10.tgz#62de58653f8762b5d6f8d9fe30fa75f7b2585a75" + integrity sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ== + dependencies: + safe-buffer "^5.0.1" + +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +blakejs@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" + integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== + +bn.js@4.11.6: + version "4.11.6" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" + integrity sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA== + +bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +boxen@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserify-aes@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +bs58check@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + safe-buffer "^5.1.2" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + dependencies: + "@types/http-cache-semantics" "^4.0.2" + get-stream "^6.0.1" + http-cache-semantics "^4.1.1" + keyv "^4.5.3" + mimic-response "^4.0.0" + normalize-url "^8.0.0" + responselike "^3.0.0" + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.0.0, camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caseless@^0.12.0, caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +cbor@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5" + integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg== + dependencies: + nofilter "^3.1.0" + +chai-as-promised@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.2.tgz#70cd73b74afd519754161386421fb71832c6d041" + integrity sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw== + dependencies: + check-error "^1.0.2" + +chai@^4.2.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + +"charenc@>= 0.0.1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +check-error@^1.0.2, check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@^3.4.0, chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + +cli-table3@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" + integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== + dependencies: + object-assign "^4.1.0" + string-width "^2.1.1" + optionalDependencies: + colors "^1.1.2" + +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +colors@1.4.0, colors@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +command-exists@^1.2.8: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +commander@~12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.6.0, concat-stream@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^8.0.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +"crypt@>= 0.0.1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +death@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/death/-/death-1.1.0.tgz#01aa9c401edd92750514470b8266390c66c67318" + integrity sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w== + +debug@4, debug@^4.1.1, debug@^4.3.5, debug@~4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-eql@^4.0.1, deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + dependencies: + type-detect "^4.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +defer-to-connect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +difflib@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/difflib/-/difflib-0.2.4.tgz#b5e30361a6db023176d562892db85940a718f47e" + integrity sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w== + dependencies: + heap ">= 0.2.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dotenv@^16.3.1: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +elliptic@^6.5.2, elliptic@^6.5.4: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enquirer@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== + dependencies: + ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@1.8.x: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + integrity sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A== + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +esprima@2.7.x, esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + integrity sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + integrity sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eth-gas-reporter@^0.2.25: + version "0.2.27" + resolved "https://registry.yarnpkg.com/eth-gas-reporter/-/eth-gas-reporter-0.2.27.tgz#928de8548a674ed64c7ba0bf5795e63079150d4e" + integrity sha512-femhvoAM7wL0GcI8ozTdxfuBtBFJ9qsyIAsmKVjlWAHUbdnnXHt+lKzz/kmldM5lA9jLuNHGwuIxorNpLbR1Zw== + dependencies: + "@solidity-parser/parser" "^0.14.0" + axios "^1.5.1" + cli-table3 "^0.5.0" + colors "1.4.0" + ethereum-cryptography "^1.0.3" + ethers "^5.7.2" + fs-readdir-recursive "^1.1.0" + lodash "^4.17.14" + markdown-table "^1.1.3" + mocha "^10.2.0" + req-cwd "^2.0.0" + sha1 "^1.1.1" + sync-request "^6.0.0" + +ethereum-bloom-filters@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.1.0.tgz#b3fc1eb789509ee30db0bf99a2988ccacb8d0397" + integrity sha512-J1gDRkLpuGNvWYzWslBQR9cDV4nd4kfvVTE/Wy4Kkm4yb3EYRSlyi0eB/inTsSTTVyA0+HyzHgbr95Fn/Z1fSw== + dependencies: + "@noble/hashes" "^1.4.0" + +ethereum-cryptography@0.1.3, ethereum-cryptography@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" + integrity sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ== + dependencies: + "@types/pbkdf2" "^3.0.0" + "@types/secp256k1" "^4.0.1" + blakejs "^1.1.0" + browserify-aes "^1.2.0" + bs58check "^2.1.2" + create-hash "^1.2.0" + create-hmac "^1.1.7" + hash.js "^1.1.7" + keccak "^3.0.0" + pbkdf2 "^3.0.17" + randombytes "^2.1.0" + safe-buffer "^5.1.2" + scrypt-js "^3.0.0" + secp256k1 "^4.0.1" + setimmediate "^1.0.5" + +ethereum-cryptography@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz#5ccfa183e85fdaf9f9b299a79430c044268c9b3a" + integrity sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw== + dependencies: + "@noble/hashes" "1.2.0" + "@noble/secp256k1" "1.7.1" + "@scure/bip32" "1.1.5" + "@scure/bip39" "1.1.1" + +ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz#58f2810f8e020aecb97de8c8c76147600b0b8ccf" + integrity sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg== + dependencies: + "@noble/curves" "1.4.2" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + +ethereumjs-abi@^0.6.8: + version "0.6.8" + resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" + integrity sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA== + dependencies: + bn.js "^4.11.8" + ethereumjs-util "^6.0.0" + +ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" + integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw== + dependencies: + "@types/bn.js" "^4.11.3" + bn.js "^4.11.0" + create-hash "^1.1.2" + elliptic "^6.5.2" + ethereum-cryptography "^0.1.3" + ethjs-util "0.1.6" + rlp "^2.2.3" + +ethereumjs-util@^7.1.4: + version "7.1.5" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181" + integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== + dependencies: + "@types/bn.js" "^5.1.0" + bn.js "^5.1.2" + create-hash "^1.1.2" + ethereum-cryptography "^0.1.3" + rlp "^2.2.4" + +ethers@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + +ethers@^6.4.0: + version "6.13.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.1.tgz#2b9f9c7455cde9d38b30fe6589972eb083652961" + integrity sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.17.1" + +ethjs-unit@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" + integrity sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw== + dependencies: + bn.js "4.11.6" + number-to-bn "1.7.0" + +ethjs-util@0.1.6, ethjs-util@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" + integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== + dependencies: + is-hex-prefixed "1.0.0" + strip-hex-prefix "1.0.0" + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@~8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.0.3: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== + dependencies: + locate-path "^2.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.12.1, follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== + +form-data@^2.2.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fp-ts@1.19.3: + version "1.19.3" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f" + integrity sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg== + +fp-ts@^1.0.0: + version "1.19.5" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.5.tgz#3da865e585dfa1fdfd51785417357ac50afc520a" + integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== + +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-port@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" + integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== + +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + +ghost-testrpc@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz#c4de9557b1d1ae7b2d20bbe474a91378ca90ce92" + integrity sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ== + dependencies: + chalk "^2.4.2" + node-emoji "^1.10.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + integrity sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.0.3, glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globby@^10.0.1: + version "10.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" + integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" + glob "^7.1.3" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^12.1.0: + version "12.6.1" + resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" + integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + cacheable-lookup "^7.0.0" + cacheable-request "^10.2.8" + decompress-response "^6.0.0" + form-data-encoder "^2.1.2" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^3.0.0" + +graceful-fs@4.2.10: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handlebars@^4.0.1: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +hardhat-gas-reporter@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.10.tgz#ebe5bda5334b5def312747580cd923c2b09aef1b" + integrity sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA== + dependencies: + array-uniq "1.0.3" + eth-gas-reporter "^0.2.25" + sha1 "^1.1.1" + +hardhat@^2.17.2: + version "2.22.6" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.22.6.tgz#d73caece246cd8219a1815554dabc31d400fa035" + integrity sha512-abFEnd9QACwEtSvZZGSmzvw7N3zhQN1cDKz5SLHAupfG24qTHofCjqvD5kT5Wwsq5XOL0ON1Mq5rr4v0XX5ciw== + dependencies: + "@ethersproject/abi" "^5.1.2" + "@metamask/eth-sig-util" "^4.0.0" + "@nomicfoundation/edr" "^0.4.1" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + "@nomicfoundation/solidity-analyzer" "^0.1.0" + "@sentry/node" "^5.18.1" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "^5.1.0" + adm-zip "^0.4.16" + aggregate-error "^3.0.0" + ansi-escapes "^4.3.0" + boxen "^5.1.2" + chalk "^2.4.2" + chokidar "^3.4.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + ethereum-cryptography "^1.0.3" + ethereumjs-abi "^0.6.8" + find-up "^2.1.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + glob "7.2.0" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + keccak "^3.0.2" + lodash "^4.17.11" + mnemonist "^0.38.0" + mocha "^10.0.0" + p-map "^4.0.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + solc "0.8.26" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + tsort "0.0.1" + undici "^5.14.0" + uuid "^8.3.2" + ws "^7.4.6" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +"heap@>= 0.2.0": + version "0.2.7" + resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" + integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +http-basic@^8.1.1: + version "8.1.3" + resolved "https://registry.yarnpkg.com/http-basic/-/http-basic-8.1.3.tgz#a7cabee7526869b9b710136970805b1004261bbf" + integrity sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw== + dependencies: + caseless "^0.12.0" + concat-stream "^1.6.2" + http-response-object "^3.0.1" + parse-cache-control "^1.0.1" + +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-response-object@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-3.0.2.tgz#7f435bb210454e4360d074ef1f989d5ea8aa9810" + integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA== + dependencies: + "@types/node" "^10.0.3" + +http2-wrapper@^2.1.10: + version "2.2.1" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" + integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +husky@^9.0.10: + version "9.0.11" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" + integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^5.1.1, ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +immutable@^4.0.0-rc.12: + version "4.3.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" + integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + +import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +io-ts@1.10.4: + version "1.10.4" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2" + integrity sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g== + dependencies: + fp-ts "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" + integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-hex-prefixed@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" + integrity sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@3.x: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +jsonschema@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" + integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== + +keccak@^3.0.0, keccak@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" + integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== + dependencies: + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + readable-stream "^3.6.0" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +latest-version@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" + integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== + dependencies: + package-json "^8.1.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lilconfig@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lint-staged@^15.2.2: + version "15.2.7" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" + integrity sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw== + dependencies: + chalk "~5.3.0" + commander "~12.1.0" + debug "~4.3.4" + execa "~8.0.1" + lilconfig "~3.1.1" + listr2 "~8.2.1" + micromatch "~4.0.7" + pidtree "~0.6.0" + string-argv "~0.3.2" + yaml "~2.4.2" + +listr2@~8.2.1: + version "8.2.3" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.3.tgz#c494bb89b34329cf900e4e0ae8aeef9081d7d7a5" + integrity sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw== + dependencies: + cli-truncate "^4.0.0" + colorette "^2.0.20" + eventemitter3 "^5.0.1" + log-update "^6.0.0" + rfdc "^1.4.1" + wrap-ansi "^9.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" + integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== + dependencies: + ansi-escapes "^6.2.0" + cli-cursor "^4.0.0" + slice-ansi "^7.0.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lowercase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" + integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + +markdown-table@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.3.tgz#9fcb69bcfdb8717bfd0398c6ec2d93036ef8de60" + integrity sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.2.3, merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micro-ftch@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" + integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== + +micromatch@^4.0.4, micromatch@~4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@0.5.x: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mnemonist@^0.38.0: + version "0.38.5" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" + integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== + dependencies: + obliterator "^2.0.0" + +mocha@^10.0.0, mocha@^10.2.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.6.0.tgz#465fc66c52613088e10018989a3b98d5e11954b9" + integrity sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-addon-api@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" + integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== + +node-emoji@^1.10.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + +node-gyp-build@^4.2.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== + +nofilter@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66" + integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g== + +nopt@3.x: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg== + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" + integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== + +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + +number-to-bn@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/number-to-bn/-/number-to-bn-1.7.0.tgz#bb3623592f7e5f9e0030b1977bd41a0c53fe1ea0" + integrity sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig== + dependencies: + bn.js "4.11.6" + strip-hex-prefix "1.0.0" + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +obliterator@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" + integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== + +once@1.x, once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +ordinal@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ordinal/-/ordinal-1.0.3.tgz#1a3c7726a61728112f50944ad7c35c06ae3a0d4d" + integrity sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ== + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== + dependencies: + p-limit "^1.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== + +package-json@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" + integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== + dependencies: + got "^12.1.0" + registry-auth-token "^5.0.1" + registry-url "^6.0.0" + semver "^7.3.7" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-cache-control@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e" + integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-parse@^1.0.6, path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +pbkdf2@^3.0.17: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +picocolors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pidtree@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +prettier@^2.8.3: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@^6.4.0: + version "6.12.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.3.tgz#e43ce03c8521b9c7fd7f1f13e514e5ca37727754" + integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ== + dependencies: + side-channel "^1.0.6" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +raw-body@^2.4.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +recursive-readdir@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== + dependencies: + minimatch "^3.0.5" + +registry-auth-token@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" + integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== + dependencies: + "@pnpm/npm-conf" "^2.1.0" + +registry-url@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" + integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== + dependencies: + rc "1.2.8" + +req-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/req-cwd/-/req-cwd-2.0.0.tgz#d4082b4d44598036640fb73ddea01ed53db49ebc" + integrity sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ== + dependencies: + req-from "^2.0.0" + +req-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/req-from/-/req-from-2.0.0.tgz#d74188e47f93796f4aa71df6ee35ae689f3e0e70" + integrity sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA== + dependencies: + resolve-from "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-alpn@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@1.1.x: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg== + +resolve@1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +resolve@^1.1.6: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + dependencies: + lowercase-keys "^3.0.0" + +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rlp@^2.2.3, rlp@^2.2.4: + version "2.2.7" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" + integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== + dependencies: + bn.js "^5.2.0" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sc-istanbul@^0.4.5: + version "0.4.6" + resolved "https://registry.yarnpkg.com/sc-istanbul/-/sc-istanbul-0.4.6.tgz#cf6784355ff2076f92d70d59047d71c13703e839" + integrity sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g== + dependencies: + abbrev "1.0.x" + async "1.x" + escodegen "1.8.x" + esprima "2.7.x" + glob "^5.0.15" + handlebars "^4.0.1" + js-yaml "3.x" + mkdirp "0.5.x" + nopt "3.x" + once "1.x" + resolve "1.1.x" + supports-color "^3.1.0" + which "^1.1.1" + wordwrap "^1.0.0" + +scrypt-js@3.0.1, scrypt-js@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" + integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== + +secp256k1@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.4, semver@^7.3.7, semver@^7.5.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +sha1@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" + integrity sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA== + dependencies: + charenc ">= 0.0.1" + crypt ">= 0.0.1" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shelljs@^0.8.3: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + +solc@0.8.26: + version "0.8.26" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" + integrity sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g== + dependencies: + command-exists "^1.2.8" + commander "^8.1.0" + follow-redirects "^1.12.1" + js-sha3 "0.8.0" + memorystream "^0.3.1" + semver "^5.5.0" + tmp "0.0.33" + +solhint-plugin-lido@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/solhint-plugin-lido/-/solhint-plugin-lido-0.0.4.tgz#a1f64970b7d58a94c1dfa98bae1ff83615cc0548" + integrity sha512-c+MsZY8zfVahIekswH04baEymE4foUS4oq62nKodUOEJ9mbkMACDszx7zevlAePW/77EBd2NONZdnRPy9WvpFQ== + +solhint@^4.1.1: + version "4.5.4" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-4.5.4.tgz#171cf33f46c36b8499efe60c0e425f6883a54e50" + integrity sha512-Cu1XiJXub2q1eCr9kkJ9VPv1sGcmj3V7Zb76B0CoezDOB9bu3DxKIFFH7ggCl9fWpEPD6xBmRLfZrYijkVmujQ== + dependencies: + "@solidity-parser/parser" "^0.18.0" + ajv "^6.12.6" + antlr4 "^4.13.1-patch-1" + ast-parents "^0.0.1" + chalk "^4.1.2" + commander "^10.0.0" + cosmiconfig "^8.0.0" + fast-diff "^1.2.0" + glob "^8.0.3" + ignore "^5.2.4" + js-yaml "^4.1.0" + latest-version "^7.0.0" + lodash "^4.17.21" + pluralize "^8.0.0" + semver "^7.5.2" + strip-ansi "^6.0.1" + table "^6.8.1" + text-table "^0.2.0" + optionalDependencies: + prettier "^2.8.3" + +solidity-coverage@^0.8.4: + version "0.8.12" + resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.12.tgz#c4fa2f64eff8ada7a1387b235d6b5b0e6c6985ed" + integrity sha512-8cOB1PtjnjFRqOgwFiD8DaUsYJtVJ6+YdXQtSZDrLGf8cdhhh8xzTtGzVTGeBf15kTv0v7lYPJlV/az7zLEPJw== + dependencies: + "@ethersproject/abi" "^5.0.9" + "@solidity-parser/parser" "^0.18.0" + chalk "^2.4.2" + death "^1.1.0" + difflib "^0.2.4" + fs-extra "^8.1.0" + ghost-testrpc "^0.0.2" + global-modules "^2.0.0" + globby "^10.0.1" + jsonschema "^1.2.4" + lodash "^4.17.21" + mocha "^10.2.0" + node-emoji "^1.10.0" + pify "^4.0.1" + recursive-readdir "^2.2.2" + sc-istanbul "^0.4.5" + semver "^7.3.4" + shelljs "^0.8.3" + web3-utils "^1.3.6" + +source-map-support@^0.5.13: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + integrity sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA== + dependencies: + amdefine ">=0.0.4" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stacktrace-parser@^0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" + integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== + dependencies: + type-fest "^0.7.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string-argv@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" + integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-hex-prefix@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f" + integrity sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A== + dependencies: + is-hex-prefixed "1.0.0" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +supports-color@^3.1.0: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A== + dependencies: + has-flag "^1.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +sync-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/sync-request/-/sync-request-6.1.0.tgz#e96217565b5e50bbffe179868ba75532fb597e68" + integrity sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw== + dependencies: + http-response-object "^3.0.1" + sync-rpc "^1.2.1" + then-request "^6.0.0" + +sync-rpc@^1.2.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/sync-rpc/-/sync-rpc-1.3.6.tgz#b2e8b2550a12ccbc71df8644810529deb68665a7" + integrity sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw== + dependencies: + get-port "^3.1.0" + +table@^6.8.0, table@^6.8.1: + version "6.8.2" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" + integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +then-request@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/then-request/-/then-request-6.0.2.tgz#ec18dd8b5ca43aaee5cb92f7e4c1630e950d4f0c" + integrity sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA== + dependencies: + "@types/concat-stream" "^1.6.0" + "@types/form-data" "0.0.33" + "@types/node" "^8.0.0" + "@types/qs" "^6.2.31" + caseless "~0.12.0" + concat-stream "^1.6.0" + form-data "^2.2.0" + http-basic "^8.1.1" + http-response-object "^3.0.1" + promise "^8.0.0" + qs "^6.4.0" + +tmp@0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsort@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" + integrity sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw== + +tweetnacl-util@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" + integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + +type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +uglify-js@^3.1.4: + version "3.18.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.18.0.tgz#73b576a7e8fda63d2831e293aeead73e0a270deb" + integrity sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici@^5.14.0: + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + dependencies: + "@fastify/busboy" "^2.0.0" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +uri-js@^4.2.2, uri-js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utf8@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +web3-utils@^1.3.6: + version "1.10.4" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.4.tgz#0daee7d6841641655d8b3726baf33b08eda1cbec" + integrity sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A== + dependencies: + "@ethereumjs/util" "^8.1.0" + bn.js "^5.2.1" + ethereum-bloom-filters "^1.0.6" + ethereum-cryptography "^2.1.2" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + +which@^1.1.1, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +ws@^7.4.6: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yaml@~2.4.2: + version "2.4.5" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" + integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==