diff --git a/.gas-snapshot b/.gas-snapshot index efafbe87..ef638edd 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -3,68 +3,69 @@ ArbitrumDAOConstitutionTest:testConstructor() (gas: 259383) ArbitrumDAOConstitutionTest:testMonOwnerCannotSetHash() (gas: 262836) ArbitrumDAOConstitutionTest:testOwnerCanSetHash() (gas: 261148) ArbitrumDAOConstitutionTest:testOwnerCanSetHashTwice() (gas: 263824) -ArbitrumFoundationVestingWalletTest:testBeneficiaryCanSetBeneficiary() (gas: 16331729) -ArbitrumFoundationVestingWalletTest:testMigrateEthToNewWalletWithSlowerVesting() (gas: 19243292) -ArbitrumFoundationVestingWalletTest:testMigrateTokensToNewWalletWithFasterVesting() (gas: 19246635) -ArbitrumFoundationVestingWalletTest:testMigrateTokensToNewWalletWithSlowerVesting() (gas: 19246580) -ArbitrumFoundationVestingWalletTest:testMigrationTargetMustBeContract() (gas: 16335062) -ArbitrumFoundationVestingWalletTest:testOnlyBeneficiaryCanRelease() (gas: 16327044) -ArbitrumFoundationVestingWalletTest:testOnlyOwnerCanMigrate() (gas: 16329393) -ArbitrumFoundationVestingWalletTest:testOwnerCanSetBeneficiary() (gas: 16331812) -ArbitrumFoundationVestingWalletTest:testProperlyInits() (gas: 16337182) -ArbitrumFoundationVestingWalletTest:testRandomAddressCantSetBeneficiary() (gas: 16329292) -ArbitrumFoundationVestingWalletTest:testRelease() (gas: 16448267) +ArbitrumFoundationVestingWalletTest:testBeneficiaryCanSetBeneficiary() (gas: 16332093) +ArbitrumFoundationVestingWalletTest:testMigrateEthToNewWalletWithSlowerVesting() (gas: 19243747) +ArbitrumFoundationVestingWalletTest:testMigrateTokensToNewWalletWithFasterVesting() (gas: 19247090) +ArbitrumFoundationVestingWalletTest:testMigrateTokensToNewWalletWithSlowerVesting() (gas: 19247035) +ArbitrumFoundationVestingWalletTest:testMigrationTargetMustBeContract() (gas: 16335426) +ArbitrumFoundationVestingWalletTest:testOnlyBeneficiaryCanRelease() (gas: 16327408) +ArbitrumFoundationVestingWalletTest:testOnlyOwnerCanMigrate() (gas: 16329757) +ArbitrumFoundationVestingWalletTest:testOwnerCanSetBeneficiary() (gas: 16332176) +ArbitrumFoundationVestingWalletTest:testProperlyInits() (gas: 16337546) +ArbitrumFoundationVestingWalletTest:testRandomAddressCantSetBeneficiary() (gas: 16329656) +ArbitrumFoundationVestingWalletTest:testRelease() (gas: 16448631) ArbitrumVestingWalletFactoryTest:testDeploy() (gas: 4589688) ArbitrumVestingWalletFactoryTest:testOnlyOwnerCanCreateWallets() (gas: 1504286) -ArbitrumVestingWalletTest:testCastVote() (gas: 16201311) -ArbitrumVestingWalletTest:testCastVoteFailsForNonBeneficiary() (gas: 16151068) -ArbitrumVestingWalletTest:testClaim() (gas: 16007495) -ArbitrumVestingWalletTest:testClaimFailsForNonBeneficiary() (gas: 15967682) -ArbitrumVestingWalletTest:testDelegate() (gas: 16080833) -ArbitrumVestingWalletTest:testDelegateFailsForNonBeneficiary() (gas: 16008162) -ArbitrumVestingWalletTest:testDoesDeploy() (gas: 15971069) -ArbitrumVestingWalletTest:testReleaseAffordance() (gas: 16008376) -ArbitrumVestingWalletTest:testVestedAmountStart() (gas: 16074644) -FixedDelegateErc20WalletTest:testInit() (gas: 5822393) -FixedDelegateErc20WalletTest:testInitZeroToken() (gas: 5816623) -FixedDelegateErc20WalletTest:testTransfer() (gas: 5932036) -FixedDelegateErc20WalletTest:testTransferNotOwner() (gas: 5897661) +ArbitrumVestingWalletTest:testCastVote() (gas: 16201584) +ArbitrumVestingWalletTest:testCastVoteFailsForNonBeneficiary() (gas: 16151341) +ArbitrumVestingWalletTest:testClaim() (gas: 16007768) +ArbitrumVestingWalletTest:testClaimFailsForNonBeneficiary() (gas: 15967955) +ArbitrumVestingWalletTest:testDelegate() (gas: 16081106) +ArbitrumVestingWalletTest:testDelegateFailsForNonBeneficiary() (gas: 16008435) +ArbitrumVestingWalletTest:testDoesDeploy() (gas: 15971342) +ArbitrumVestingWalletTest:testReleaseAffordance() (gas: 16008649) +ArbitrumVestingWalletTest:testVestedAmountStart() (gas: 16074917) +E2E:testE2E() (gas: 83632522) +FixedDelegateErc20WalletTest:testInit() (gas: 5822575) +FixedDelegateErc20WalletTest:testInitZeroToken() (gas: 5816805) +FixedDelegateErc20WalletTest:testTransfer() (gas: 5932218) +FixedDelegateErc20WalletTest:testTransferNotOwner() (gas: 5897843) InboxActionsTest:testPauseAndUpauseInbox() (gas: 370341) L1AddressRegistryTest:testAddressRegistryAddress() (gas: 47053) -L1ArbitrumTimelockTest:testCancel() (gas: 4788118) -L1ArbitrumTimelockTest:testCancelFailsBadSender() (gas: 4833005) -L1ArbitrumTimelockTest:testDoesDeploy() (gas: 4736837) -L1ArbitrumTimelockTest:testDoesNotDeployZeroInbox() (gas: 4442863) -L1ArbitrumTimelockTest:testDoesNotDeployZeroL2Timelock() (gas: 4440833) -L1ArbitrumTimelockTest:testExecute() (gas: 4868828) -L1ArbitrumTimelockTest:testExecuteInbox() (gas: 4921047) -L1ArbitrumTimelockTest:testExecuteInboxBatch() (gas: 4964571) -L1ArbitrumTimelockTest:testExecuteInboxInvalidData() (gas: 4889852) -L1ArbitrumTimelockTest:testExecuteInboxNotEnoughVal() (gas: 4909640) -L1ArbitrumTimelockTest:testSchedule() (gas: 4820974) -L1ArbitrumTimelockTest:testScheduleFailsBadL2Timelock() (gas: 4749287) -L1ArbitrumTimelockTest:testScheduleFailsBadSender() (gas: 4744555) -L1ArbitrumTokenTest:testBridgeBurn() (gas: 3395480) -L1ArbitrumTokenTest:testBridgeBurnNotGateway() (gas: 3389520) -L1ArbitrumTokenTest:testBridgeMint() (gas: 3390707) -L1ArbitrumTokenTest:testBridgeMintNotGateway() (gas: 3340945) -L1ArbitrumTokenTest:testInit() (gas: 3355848) -L1ArbitrumTokenTest:testInitZeroGateway() (gas: 3177143) -L1ArbitrumTokenTest:testInitZeroNovaGateway() (gas: 3177210) -L1ArbitrumTokenTest:testInitZeroNovaRouter() (gas: 3177144) -L1ArbitrumTokenTest:testRegisterTokenOnL2() (gas: 4568521) -L1ArbitrumTokenTest:testRegisterTokenOnL2NotEnoughVal() (gas: 4425708) -L1GovernanceFactoryTest:testL1GovernanceFactory() (gas: 10234968) -L1GovernanceFactoryTest:testSetMinDelay() (gas: 10209854) -L1GovernanceFactoryTest:testSetMinDelayRevertsForCoreAddress() (gas: 10262809) +L1ArbitrumTimelockTest:testCancel() (gas: 5324642) +L1ArbitrumTimelockTest:testCancelFailsBadSender() (gas: 5369529) +L1ArbitrumTimelockTest:testDoesDeploy() (gas: 5273077) +L1ArbitrumTimelockTest:testDoesNotDeployZeroInbox() (gas: 4978961) +L1ArbitrumTimelockTest:testDoesNotDeployZeroL2Timelock() (gas: 4976931) +L1ArbitrumTimelockTest:testExecute() (gas: 5405352) +L1ArbitrumTimelockTest:testExecuteInbox() (gas: 5746378) +L1ArbitrumTimelockTest:testExecuteInboxBatch() (gas: 6056741) +L1ArbitrumTimelockTest:testExecuteInboxInvalidData() (gas: 5426399) +L1ArbitrumTimelockTest:testExecuteInboxNotEnoughVal() (gas: 5446210) +L1ArbitrumTimelockTest:testSchedule() (gas: 5357782) +L1ArbitrumTimelockTest:testScheduleFailsBadL2Timelock() (gas: 5286095) +L1ArbitrumTimelockTest:testScheduleFailsBadSender() (gas: 5281079) +L1ArbitrumTokenTest:testBridgeBurn() (gas: 3395571) +L1ArbitrumTokenTest:testBridgeBurnNotGateway() (gas: 3389611) +L1ArbitrumTokenTest:testBridgeMint() (gas: 3390798) +L1ArbitrumTokenTest:testBridgeMintNotGateway() (gas: 3341036) +L1ArbitrumTokenTest:testInit() (gas: 3355939) +L1ArbitrumTokenTest:testInitZeroGateway() (gas: 3177234) +L1ArbitrumTokenTest:testInitZeroNovaGateway() (gas: 3177301) +L1ArbitrumTokenTest:testInitZeroNovaRouter() (gas: 3177235) +L1ArbitrumTokenTest:testRegisterTokenOnL2() (gas: 4568612) +L1ArbitrumTokenTest:testRegisterTokenOnL2NotEnoughVal() (gas: 4425799) +L1GovernanceFactoryTest:testL1GovernanceFactory() (gas: 10771117) +L1GovernanceFactoryTest:testSetMinDelay() (gas: 10746003) +L1GovernanceFactoryTest:testSetMinDelayRevertsForCoreAddress() (gas: 10798958) L2AddressRegistryTest:testAddressRegistryAddress() (gas: 54658) -L2ArbitrumGovernorTest:testCantReinit() (gas: 13669216) -L2ArbitrumGovernorTest:testExecutorPermissions() (gas: 13706210) -L2ArbitrumGovernorTest:testExecutorPermissionsFail() (gas: 13678862) -L2ArbitrumGovernorTest:testPastCirculatingSupply() (gas: 13672965) -L2ArbitrumGovernorTest:testPastCirculatingSupplyExclude() (gas: 13812442) -L2ArbitrumGovernorTest:testPastCirculatingSupplyMint() (gas: 13736945) -L2ArbitrumGovernorTest:testProperlyInitialized() (gas: 13664433) +L2ArbitrumGovernorTest:testCantReinit() (gas: 13669489) +L2ArbitrumGovernorTest:testExecutorPermissions() (gas: 13706483) +L2ArbitrumGovernorTest:testExecutorPermissionsFail() (gas: 13679135) +L2ArbitrumGovernorTest:testPastCirculatingSupply() (gas: 13673238) +L2ArbitrumGovernorTest:testPastCirculatingSupplyExclude() (gas: 13812715) +L2ArbitrumGovernorTest:testPastCirculatingSupplyMint() (gas: 13737218) +L2ArbitrumGovernorTest:testProperlyInitialized() (gas: 13664706) L2ArbitrumTokenTest:testCanBurn() (gas: 4066835) L2ArbitrumTokenTest:testCanMint2Percent() (gas: 4101512) L2ArbitrumTokenTest:testCanMintLessThan2Percent() (gas: 4101514) @@ -83,15 +84,20 @@ L2ArbitrumTokenTest:testDoesNotInitialiseZeroL1Token() (gas: 3800726) L2ArbitrumTokenTest:testDoesNotInitialiseZeroOwner() (gas: 3800739) L2ArbitrumTokenTest:testIsInitialised() (gas: 4072777) L2ArbitrumTokenTest:testNoLogicContractInit() (gas: 2693127) -L2GovernanceFactoryTest:testContractsDeployed() (gas: 28359370) -L2GovernanceFactoryTest:testContractsInitialized() (gas: 28396320) +L2GovernanceFactoryTest:testContractsDeployed() (gas: 28359365) +L2GovernanceFactoryTest:testContractsInitialized() (gas: 28396315) L2GovernanceFactoryTest:testDeploySteps() (gas: 28370874) -L2GovernanceFactoryTest:testProxyAdminOwnership() (gas: 28368380) -L2GovernanceFactoryTest:testRoles() (gas: 28391455) -L2GovernanceFactoryTest:testSanityCheckValues() (gas: 28415663) -L2GovernanceFactoryTest:testSetMinDelay() (gas: 28364376) -L2GovernanceFactoryTest:testSetMinDelayRevertsForCoreAddress() (gas: 28417247) +L2GovernanceFactoryTest:testProxyAdminOwnership() (gas: 28368375) +L2GovernanceFactoryTest:testRoles() (gas: 28391450) +L2GovernanceFactoryTest:testSanityCheckValues() (gas: 28415658) +L2GovernanceFactoryTest:testSetMinDelay() (gas: 28364371) +L2GovernanceFactoryTest:testSetMinDelayRevertsForCoreAddress() (gas: 28417242) L2GovernanceFactoryTest:testUpgraderCanCancel() (gas: 28657360) +L2SecurityCouncilMgmtFactoryTest:testMemberElectionGovDeployment() (gas: 30363122) +L2SecurityCouncilMgmtFactoryTest:testNomineeElectionGovDeployment() (gas: 30367353) +L2SecurityCouncilMgmtFactoryTest:testOnlyOwnerCanDeploy() (gas: 25498765) +L2SecurityCouncilMgmtFactoryTest:testRemovalGovDeployment() (gas: 30365353) +L2SecurityCouncilMgmtFactoryTest:testSecurityCouncilManagerDeployment() (gas: 30384446) OutboxActionsTest:testAddOutbxesAction() (gas: 651591) OutboxActionsTest:testCantAddEOA() (gas: 969161) OutboxActionsTest:testCantReAddOutbox() (gas: 974559) @@ -99,50 +105,139 @@ OutboxActionsTest:testRemoveAllOutboxes() (gas: 693238) OutboxActionsTest:testRemoveOutboxes() (gas: 854205) ProxyUpgradeAndCallActionTest:testUpgrade() (gas: 137095) ProxyUpgradeAndCallActionTest:testUpgradeAndCall() (gas: 143042) +SecurityCouncilManagerTest:testAddMemberAffordances() (gas: 249651) +SecurityCouncilManagerTest:testAddMemberSpecialAddresses() (gas: 20795) +SecurityCouncilManagerTest:testAddMemberToFirstCohort() (gas: 339764) +SecurityCouncilManagerTest:testAddMemberToSecondCohort() (gas: 343060) +SecurityCouncilManagerTest:testAddSC() (gas: 118567) +SecurityCouncilManagerTest:testAddSCAffordances() (gas: 112083) +SecurityCouncilManagerTest:testCantUpdateCohortWithADup() (gas: 123116) +SecurityCouncilManagerTest:testCohortMethods() (gas: 136185) +SecurityCouncilManagerTest:testInitialization() (gas: 193074) +SecurityCouncilManagerTest:testRemoveMember() (gas: 213029) +SecurityCouncilManagerTest:testRemoveMemberAffordances() (gas: 99074) +SecurityCouncilManagerTest:testRemoveSCAffordances() (gas: 81253) +SecurityCouncilManagerTest:testRemoveSeC() (gas: 38309) +SecurityCouncilManagerTest:testReplaceMemberAffordances() (gas: 208560) +SecurityCouncilManagerTest:testReplaceMemberInFirstCohort() (gas: 258788) +SecurityCouncilManagerTest:testReplaceMemberInSecondCohort() (gas: 262305) +SecurityCouncilManagerTest:testRotateMember() (gas: 258792) +SecurityCouncilManagerTest:testUpdateCohortAffordances() (gas: 83026) +SecurityCouncilManagerTest:testUpdateFirstCohort() (gas: 295311) +SecurityCouncilManagerTest:testUpdateRouter() (gas: 76296) +SecurityCouncilManagerTest:testUpdateRouterAffordacnes() (gas: 112379) +SecurityCouncilManagerTest:testUpdateSecondCohort() (gas: 295316) +SecurityCouncilMemberElectionGovernorTest:testCannotUseMoreVotesThanAvailable() (gas: 246997) +SecurityCouncilMemberElectionGovernorTest:testCastBySig() (gas: 302832) +SecurityCouncilMemberElectionGovernorTest:testCastBySigTwice() (gas: 266284) +SecurityCouncilMemberElectionGovernorTest:testCastVoteReverts() (gas: 35277) +SecurityCouncilMemberElectionGovernorTest:testExecute() (gas: 665450) +SecurityCouncilMemberElectionGovernorTest:testForceSupport() (gas: 165349) +SecurityCouncilMemberElectionGovernorTest:testInitReverts() (gas: 4922497) +SecurityCouncilMemberElectionGovernorTest:testInvalidParams() (gas: 165321) +SecurityCouncilMemberElectionGovernorTest:testMiscVotesViews() (gas: 227939) +SecurityCouncilMemberElectionGovernorTest:testNoVoteForNonCompliantNominee() (gas: 123524) +SecurityCouncilMemberElectionGovernorTest:testNoZeroWeightVotes() (gas: 169595) +SecurityCouncilMemberElectionGovernorTest:testOnlyNomineeElectionGovernorCanPropose() (gas: 111038) +SecurityCouncilMemberElectionGovernorTest:testProperInitialization() (gas: 49388) +SecurityCouncilMemberElectionGovernorTest:testProposeReverts() (gas: 32916) +SecurityCouncilMemberElectionGovernorTest:testRelay() (gas: 42229) +SecurityCouncilMemberElectionGovernorTest:testSelectTopNominees(uint256) (runs: 256, μ: 340004, ~: 339432) +SecurityCouncilMemberElectionGovernorTest:testSelectTopNomineesFails() (gas: 273335) +SecurityCouncilMemberElectionGovernorTest:testSetFullWeightDuration() (gas: 34951) +SecurityCouncilMemberElectionGovernorTest:testVotesToWeight() (gas: 152898) +SecurityCouncilMemberRemovalGovernorTest:testInitFails() (gas: 10159193) +SecurityCouncilMemberRemovalGovernorTest:testProposalCreationCallParamRestriction() (gas: 56157) +SecurityCouncilMemberRemovalGovernorTest:testProposalCreationCallRestriction() (gas: 49685) +SecurityCouncilMemberRemovalGovernorTest:testProposalCreationTargetLen() (gas: 35392) +SecurityCouncilMemberRemovalGovernorTest:testProposalCreationTargetRestriction() (gas: 46987) +SecurityCouncilMemberRemovalGovernorTest:testProposalCreationUnexpectedCallDataLen() (gas: 41583) +SecurityCouncilMemberRemovalGovernorTest:testProposalCreationValuesRestriction() (gas: 61908) +SecurityCouncilMemberRemovalGovernorTest:testProposalDoesExpire() (gas: 272525) +SecurityCouncilMemberRemovalGovernorTest:testProposalExpirationDeadline() (gas: 134831) +SecurityCouncilMemberRemovalGovernorTest:testRelay() (gas: 42123) +SecurityCouncilMemberRemovalGovernorTest:testSeparateSelector() (gas: 23536) +SecurityCouncilMemberRemovalGovernorTest:testSetVoteSuccessNumerator() (gas: 30049) +SecurityCouncilMemberRemovalGovernorTest:testSetVoteSuccessNumeratorAffordance() (gas: 47631) +SecurityCouncilMemberRemovalGovernorTest:testSuccessNumeratorInsufficientVotes() (gas: 358327) +SecurityCouncilMemberRemovalGovernorTest:testSuccessNumeratorSufficientVotes() (gas: 361245) +SecurityCouncilMemberRemovalGovernorTest:testSuccessfulProposalAndCantAbstain() (gas: 142674) +SecurityCouncilMemberSyncActionTest:testAddOne() (gas: 7938503) +SecurityCouncilMemberSyncActionTest:testAddOne() (gas: 7939341) +SecurityCouncilMemberSyncActionTest:testCantDropBelowThreshhold() (gas: 7965404) +SecurityCouncilMemberSyncActionTest:testCantDropBelowThreshhold() (gas: 7965411) +SecurityCouncilMemberSyncActionTest:testGetPrevOwner() (gas: 7929385) +SecurityCouncilMemberSyncActionTest:testGetPrevOwner() (gas: 7929385) +SecurityCouncilMemberSyncActionTest:testNonces() (gas: 8229875) +SecurityCouncilMemberSyncActionTest:testNoopUpdate() (gas: 7928439) +SecurityCouncilMemberSyncActionTest:testNoopUpdate() (gas: 7929365) +SecurityCouncilMemberSyncActionTest:testRemoveOne() (gas: 7929685) +SecurityCouncilMemberSyncActionTest:testRemoveOne() (gas: 7930546) +SecurityCouncilMemberSyncActionTest:testUpdateCohort() (gas: 8171934) +SecurityCouncilMemberSyncActionTest:testUpdateCohort() (gas: 8172795) +SecurityCouncilMgmtUtilsTests:testIsInArray() (gas: 2102) +SecurityCouncilNomineeElectionGovernorTest:testAddContender() (gas: 215127) +SecurityCouncilNomineeElectionGovernorTest:testCastBySig() (gas: 318348) +SecurityCouncilNomineeElectionGovernorTest:testCastBySigTwice() (gas: 281704) +SecurityCouncilNomineeElectionGovernorTest:testCastVoteReverts() (gas: 35255) +SecurityCouncilNomineeElectionGovernorTest:testCountVote() (gas: 542695) +SecurityCouncilNomineeElectionGovernorTest:testCreateElection() (gas: 253049) +SecurityCouncilNomineeElectionGovernorTest:testExcludeNominee() (gas: 431856) +SecurityCouncilNomineeElectionGovernorTest:testExecute() (gas: 671552) +SecurityCouncilNomineeElectionGovernorTest:testForceSupport() (gas: 176798) +SecurityCouncilNomineeElectionGovernorTest:testIncludeNominee() (gas: 644396) +SecurityCouncilNomineeElectionGovernorTest:testInvalidInit() (gas: 7095881) +SecurityCouncilNomineeElectionGovernorTest:testProperInitialization() (gas: 78091) +SecurityCouncilNomineeElectionGovernorTest:testProposeFails() (gas: 19744) +SecurityCouncilNomineeElectionGovernorTest:testRelay() (gas: 42365) +SecurityCouncilNomineeElectionGovernorTest:testSetNomineeVetter() (gas: 40019) SequencerActionsTest:testAddAndRemoveSequencer() (gas: 483356) SequencerActionsTest:testCantAddZeroAddress() (gas: 235614) SetInitialGovParamsActionTest:testL1() (gas: 259904) SetInitialGovParamsActionTest:testL2() (gas: 688888) -TokenDistributorTest:testClaim() (gas: 5742653) -TokenDistributorTest:testClaimAndDelegate() (gas: 5850736) -TokenDistributorTest:testClaimAndDelegateFailsForExpired() (gas: 5748153) -TokenDistributorTest:testClaimAndDelegateFailsForWrongSender() (gas: 5803294) -TokenDistributorTest:testClaimAndDelegateFailsWrongNonce() (gas: 5803275) -TokenDistributorTest:testClaimFailsAfterEnd() (gas: 5703944) -TokenDistributorTest:testClaimFailsBeforeStart() (gas: 5703439) -TokenDistributorTest:testClaimFailsForFalseTransfer() (gas: 5686155) -TokenDistributorTest:testClaimFailsForTwice() (gas: 5741413) -TokenDistributorTest:testClaimFailsForUnknown() (gas: 5706020) -TokenDistributorTest:testClaimStartAfterClaimEnd() (gas: 4134747) -TokenDistributorTest:testDoesDeploy() (gas: 5339462) -TokenDistributorTest:testDoesDeployAndDeposit() (gas: 5404492) -TokenDistributorTest:testOldClaimStart() (gas: 4135310) -TokenDistributorTest:testSetRecipients() (gas: 5701854) -TokenDistributorTest:testSetRecipientsFailsNotEnoughDeposit() (gas: 5668719) -TokenDistributorTest:testSetRecipientsFailsNotOwner() (gas: 5420268) -TokenDistributorTest:testSetRecipientsFailsWhenAddingTwice() (gas: 5712897) -TokenDistributorTest:testSetRecipientsFailsWrongAmountCount() (gas: 5421728) -TokenDistributorTest:testSetRecipientsFailsWrongRecipientCount() (gas: 5421957) -TokenDistributorTest:testSetRecipientsTwice() (gas: 6391434) -TokenDistributorTest:testSetSweepReceiver() (gas: 5706171) -TokenDistributorTest:testSetSweepReceiverFailsNullAddress() (gas: 5703790) -TokenDistributorTest:testSetSweepReceiverFailsOwner() (gas: 5704751) -TokenDistributorTest:testSweep() (gas: 5751880) -TokenDistributorTest:testSweepAfterClaim() (gas: 5789863) -TokenDistributorTest:testSweepFailsBeforeClaimPeriodEnd() (gas: 5703524) -TokenDistributorTest:testSweepFailsForFailedTransfer() (gas: 5707223) -TokenDistributorTest:testSweepFailsTwice() (gas: 5750839) -TokenDistributorTest:testWithdraw() (gas: 5741107) -TokenDistributorTest:testWithdrawFailsNotOwner() (gas: 5741129) -TokenDistributorTest:testWithdrawFailsTransfer() (gas: 5705726) -TokenDistributorTest:testZeroDelegateTo() (gas: 4132642) -TokenDistributorTest:testZeroOwner() (gas: 4132555) -TokenDistributorTest:testZeroReceiver() (gas: 4132584) +TokenDistributorTest:testClaim() (gas: 5742744) +TokenDistributorTest:testClaimAndDelegate() (gas: 5850827) +TokenDistributorTest:testClaimAndDelegateFailsForExpired() (gas: 5748244) +TokenDistributorTest:testClaimAndDelegateFailsForWrongSender() (gas: 5803385) +TokenDistributorTest:testClaimAndDelegateFailsWrongNonce() (gas: 5803366) +TokenDistributorTest:testClaimFailsAfterEnd() (gas: 5704035) +TokenDistributorTest:testClaimFailsBeforeStart() (gas: 5703530) +TokenDistributorTest:testClaimFailsForFalseTransfer() (gas: 5686246) +TokenDistributorTest:testClaimFailsForTwice() (gas: 5741504) +TokenDistributorTest:testClaimFailsForUnknown() (gas: 5706111) +TokenDistributorTest:testClaimStartAfterClaimEnd() (gas: 4134838) +TokenDistributorTest:testDoesDeploy() (gas: 5339553) +TokenDistributorTest:testDoesDeployAndDeposit() (gas: 5404583) +TokenDistributorTest:testOldClaimStart() (gas: 4135401) +TokenDistributorTest:testSetRecipients() (gas: 5701945) +TokenDistributorTest:testSetRecipientsFailsNotEnoughDeposit() (gas: 5668810) +TokenDistributorTest:testSetRecipientsFailsNotOwner() (gas: 5420359) +TokenDistributorTest:testSetRecipientsFailsWhenAddingTwice() (gas: 5712988) +TokenDistributorTest:testSetRecipientsFailsWrongAmountCount() (gas: 5421819) +TokenDistributorTest:testSetRecipientsFailsWrongRecipientCount() (gas: 5422048) +TokenDistributorTest:testSetRecipientsTwice() (gas: 6391525) +TokenDistributorTest:testSetSweepReceiver() (gas: 5706262) +TokenDistributorTest:testSetSweepReceiverFailsNullAddress() (gas: 5703881) +TokenDistributorTest:testSetSweepReceiverFailsOwner() (gas: 5704842) +TokenDistributorTest:testSweep() (gas: 5751971) +TokenDistributorTest:testSweepAfterClaim() (gas: 5789954) +TokenDistributorTest:testSweepFailsBeforeClaimPeriodEnd() (gas: 5703615) +TokenDistributorTest:testSweepFailsForFailedTransfer() (gas: 5707314) +TokenDistributorTest:testSweepFailsTwice() (gas: 5750930) +TokenDistributorTest:testWithdraw() (gas: 5741198) +TokenDistributorTest:testWithdrawFailsNotOwner() (gas: 5741220) +TokenDistributorTest:testWithdrawFailsTransfer() (gas: 5705817) +TokenDistributorTest:testZeroDelegateTo() (gas: 4132733) +TokenDistributorTest:testZeroOwner() (gas: 4132646) +TokenDistributorTest:testZeroReceiver() (gas: 4132675) TokenDistributorTest:testZeroToken() (gas: 71889) -UpgradeExecutorTest:testAdminCanChangeExecutor() (gas: 2583710) -UpgradeExecutorTest:testCantExecuteEOA() (gas: 2439630) -UpgradeExecutorTest:testExecute() (gas: 2677904) -UpgradeExecutorTest:testExecuteFailsForAdmin() (gas: 2663523) -UpgradeExecutorTest:testExecuteFailsForNobody() (gas: 2665764) -UpgradeExecutorTest:testInit() (gas: 2427511) -UpgradeExecutorTest:testInitFailsZeroAdmin() (gas: 2288251) \ No newline at end of file +TopNomineesGasTest:testTopNomineesGas() (gas: 4502786) +UpgradeExecRouteBuilderTest:testAIP1Point2() (gas: 1322645) +UpgradeExecRouteBuilderTest:testRouteBuilderErrors() (gas: 1127374) +UpgradeExecutorTest:testAdminCanChangeExecutor() (gas: 2583801) +UpgradeExecutorTest:testCantExecuteEOA() (gas: 2439721) +UpgradeExecutorTest:testExecute() (gas: 2677995) +UpgradeExecutorTest:testExecuteFailsForAdmin() (gas: 2663614) +UpgradeExecutorTest:testExecuteFailsForNobody() (gas: 2665855) +UpgradeExecutorTest:testInit() (gas: 2427602) +UpgradeExecutorTest:testInitFailsZeroAdmin() (gas: 2288342) \ No newline at end of file diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 037420b2..2443a047 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -29,6 +29,32 @@ jobs: - name: Run tests run: make test + test-contract-size: + name: Test contract size + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Setup node/yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + + - name: Install packages + run: yarn + + - name: Run build --sizes + run: FOUNDRY_PROFILE=sec_council_mgmt forge build --sizes + test-gas: name: Test gas runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 325bc090..4f4b9260 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dist/ report/ lcov.info lcov.info.pruned -proposalState.json \ No newline at end of file +types/ +proposalState.json diff --git a/.gitmodules b/.gitmodules index ae953de5..451eb450 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ path = token-bridge-contracts url = https://github.com/OffchainLabs/token-bridge-contracts.git branch = reverse-bridge-2 +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/Vectorized/solady diff --git a/Makefile b/Makefile index 5efeaf13..77bfacf1 100644 --- a/Makefile +++ b/Makefile @@ -9,12 +9,14 @@ install :; yarn build :; forge build coverage :; forge coverage gas :; forge test --gas-report -gas-check :; forge snapshot --check +gas-check :; forge snapshot --check --tolerance 1 snapshot :; forge snapshot test-unit :; forge test -vvv clean :; forge clean fmt :; forge fmt gen-network :; yarn gen:network test : test-unit -test-integration :; yarn test:integration test-action-storage :; ./scripts/test-action-storage.sh +sc-election-test :; FOUNDRY_MATCH_PATH='test/security-council-mgmt/**/*.t.sol' make test +test-integration :; yarn test:integration + diff --git a/README.md b/README.md index 3f5e6a57..b8946914 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ This project contains smart contracts for Arbitrum token and governance. Please * [Overview](./docs/overview.md) * [Proposal lifecycle](./docs/proposal_lifecycle_example.md) * [Governance Action Contracts](./src/gov-action-contracts/README.md) +* [Security Council Elections](./docs/security-council-mgmt.md) * [Security Audit](./audits/trail_of_bits_governance_report_1_6_2023.pdf) * [Gotchas](./docs/gotchas.md) * [Proposal Monitor](./docs/proposalMonitor.md) diff --git a/audits/trail_of_bits_sec_council_elections_report_8_9_2023.pdf b/audits/trail_of_bits_sec_council_elections_report_8_9_2023.pdf new file mode 100644 index 00000000..1619d7e9 Binary files /dev/null and b/audits/trail_of_bits_sec_council_elections_report_8_9_2023.pdf differ diff --git a/docs/gotchas.md b/docs/gotchas.md index 83f88275..e4e675ed 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -15,7 +15,8 @@ There are two L1 proxy admins - one for the governance contracts, once for the g In the both treasury timelock and the DAO treasury can be transfered via treasury gov DAO vote; however, only ARB in the DAO treasury is excluded from the quorum numerator calculation. Thus, the DAO’s ARB should ideally all be stored in the DAO Treasury. (Currently, the sweepReceiver in the TokenDistributor is set to the timelock, not the DAO treasury.) - **L2ArbitrumGovernoer onlyGovernance behavior** Typically, for a timelocked OZ governror, the `onlyGovernance` modifier ensures a call is made from the timelock; in L2ArbitrumGoverner, the _executor() method is overriden such that `onlyGovernance` enforces a call from the governor contract itself. This ensures calls guarded by `onlyGovernance` go through the full core proposal path, as calls from the governor could only be sent via `relay`. See the code comment on `relay` in [L2ArbitrumGoveror](../src/L2ArbitrumGovernor.sol) for more. -- **L2 Proposal Cancelation** -There are two redundant affordances that the security council can use to cancel proposals in the L2 timelock: relaying through core governor, or using its `CANCELLER_ROLE` affordance. Additionally, the later affordances is granted directly to the security council, not the `UpgradeExecutor` (this is inconsistent with how `UpgradeExecutors` are generally used elsewhere.) -- **L1 Proposal Cancelation** - The Security Council — not the L1 `UpgradeExecutor` — has the affordance to cancel proposals in the L1 timelock, inconsistent with how `UpgradeExecutors` are generally used elsewhere. + +- The `UpgradeExecRouteBuilder` contract is immutable; instead of upgrading it, it can be redeployed, in which case any references to its address should be updated as well (currently only referenced in SecurityCouncilManager). +- The `UpgradeExecRouteBuilder`'s l1TimelockMinDelay variable should be equal to the minimum timelock delay on the core L1 timelock. If, for whatever reason, the value on the L1 timelock is ever increased, the UpgradeExecRouteBuilder should be redeployed with the new value accordingly. +- Changes to members of the security council should be initiated via the SecurityCouncilManager, not via calling addOwner/removeOwner on the multisigs directly. This ensures that the security council's two cohorts remain properly tracked. +- Voting abstain on a SecurityCouncilMemberRemovalGovernor proposal is disallowed. diff --git a/docs/overview.md b/docs/overview.md index 55d839f8..a4aada8e 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -151,7 +151,6 @@ You can read more about how the path is encoded in the [proposal lifecycle docum Both governor contracts have the affordance to cancel proposals scheduled in the L2 Timelock. The Security Council can likewise cancel proposals [via calling L2ArbitrumGovernor.relay](src/gov-action-contracts/governance/CancelTimelockOperation.sol). Note that although the core-governor Security Council has the affordance to cancel proposals in the L2 timelock via calling `cancel` directly, for clarity and consistency, it should use the aforementioned `relay` method. ## Future DAO-Governed Chains - When a [new L2 chain is authorized by the DAO](https://docs.arbitrum.foundation/new-arb-chains), the following steps should be carried out for the new chain to become DAO-governed: 1. Deploy a new UpgradeExecutor contract and a new Security Council on the new L2 chain. 1. Initialize the new L2 UpgradeExectutor with the L1 Timelock's aliased addressed and the new Security Council as its executors. diff --git a/docs/security-council-colors.png b/docs/security-council-colors.png new file mode 100644 index 00000000..cc542f5c Binary files /dev/null and b/docs/security-council-colors.png differ diff --git a/docs/security-council-election-flow.png b/docs/security-council-election-flow.png new file mode 100644 index 00000000..baa38ac6 Binary files /dev/null and b/docs/security-council-election-flow.png differ diff --git a/docs/security-council-manager.md b/docs/security-council-manager.md new file mode 100644 index 00000000..4dea1865 --- /dev/null +++ b/docs/security-council-manager.md @@ -0,0 +1,68 @@ +# Security Council Manager as a source of truth + +The Security Council Manager is the source of truth for the membership of all Security Councils on all chains where they are deployed. It contains a registered list of Security Councils, which it will update with any changes to membership. + +All changes to membership should be made through the manager, which will then propagate these changes to the councils. Any changes made directly to the councils will be overwritten the next time the Manager pushes an update. As a reminder the normal flow for an election is: +- **SecurityCouncilNomineeElectionGovernor.createElection** - called at T+0 where T is a multiple of 6 months after the first election date +- **SecurityCouncilNomineeElectionGovernor.execute** - called at T+21 days +- **SecurityCouncilMemberElectionGovernor.execute** - called at T + 42 days +- **ArbitrumTimelock.execute** (on L2) - called at T + 42 days +- **Outbox.executeTransaction** - called at T + 49 days +- **L1ArbitrumTimelock.executeBatch** - called at T + 52 days +- Retryable ticket execution on ArbOne and Nova - also called at T + 52 days + + +## Public functions + +### Replace cohort + +Can only be called by the Member Election Governor. It is used to replace a whole cohort of (6) members. This is the function that will be called for the standard elections that take place every 6 months. + +### Remove member + +Can be called by the Emergency Security Council and the Removal Governor. Is used to remove a member. The Constitution allows members to be removed if 9 of 12 members wish them to be, or if the DAO votes to remove a member and 10% of votable tokens cast a vote, with 5/6 of voted tokens are in favour of removal. + +### Add member + +Can only be called by the Emergency Security Council. Can only be called if the there are less than 12 members in the Security Council because at least one has been removed. + +### Replace member + +Can only be called by the Emergency Security Council. This is a utility function to allow the council to call Remove and Add in the same transaction. Semantically this means that an entity has been removed from the council, and a different one has been added. + +### Rotate member + +Can only be called by the Emergency Security Council. Functionally this is the same as Replace, however semantically it infers different intent. Rotate should be called when a entity wish to replace their address, but it is the same entity that controls the newly added address. + +### Add Security Council + +Can be called by DAO and the emergency security council. Adds an additional security council whose members will be updated by the election system. + +### Remove Security Council + +Can be called by DAO and the emergency security council. Removes a security council from being updated by the election system. + + +## Race conditions +Since the Security Council Manager can be updated from a number of sources race conditions can occur in the updates. Some of these updates are long lived processes (the elections), so care must be taking to avoid these kinds of race conditions. + +Below we'll explain the possible sources of races, and how they're mitigated. + +### Standard elections +Standard elections take place every 6 months, and last 42 days before replacing the cohort in the Manager. The election governors do checks to ensure that a contender will not become a member of both cohorts, and this is then later enforced in Manager. However, whilst the elections are ongoing the membership in the Manager may be manipulated causing the result of the election to conflict (eg by adding a potential nominee to the previous cohort in the manager). This would cause the election execution to revert. + +The election contracts deal with this situation by essentially pausing the election pipeline until it is "unblocked". The election result will wait as an unexecuted proposal in the Member Election Governor; the Nominee Selection Governor will not allow the next election to be created until the previous proposal has executed, therefore meaning that no election results can be propagated to the Manager until the conflict is resolved. + +Since the conflict can only have been created by adding an individual member - something only the Security Council can do - it is then expected that the Security Council should unblock the pipeline by removing the member they added and allowing the election to complete. Therefore should the Security Council call the `rotateMember`, `replaceMember` or `addMember` methods they should ensure that they do not do so for accounts that are contenders or nominees in an ongoing election. In particular: + +> If an address is being added to the previous cohort whilst an election is ongoing it must not be the same as any of the contenders or nominees in that election + +### Removal elections +Elections occuring in the Removal Governor will result in a member being removed. However it could be that by the time the proposal to remove the member completes the member has already been removed by some other means. In this case the removal proposal will block, being unable to execute. + +To address this an expiry has been added to Succeeded removal proposals, such that if a successful removal proposal has not been executed within the expiry period it will transition into an Expired state and will not be able to be executed at a later date. The execute function can be called by anyone, so someone should ensure that this is called on a successful proposal before it expires. + +### Propagation races +After an update occurs in the Manager it is propagated to all registered Security Councils. This propagation goes through a number of steps, including timelocks, withdrawals and potentially retryable transactions. Each of these stages can be executed by anyone, however if one these stages wasn't executed then the update might remain waiting at one of the stages. It's possible that at this point another update could overtake a previous one, the final execution of the updates might then occur out of order. + +To mitigate this the Member Sync Action contract which is the last step in updating the membership on the Security Council Gnosis Safe uses a key value store to store an ordered update nonce that ensure no later update can be made before an earlier one. \ No newline at end of file diff --git a/docs/security-council-mgmt.md b/docs/security-council-mgmt.md new file mode 100644 index 00000000..fab992de --- /dev/null +++ b/docs/security-council-mgmt.md @@ -0,0 +1,199 @@ +# Security Council Election and Membership Management + +The pages documents an overview of the Security Council Election procedure, more detail on some specific components can be found in: +* [Security Council Manager](./security-council-manager.md) +* [Nominee vetting guidelines](./security-council-nominee-vetting.md) + +For background information see sections 3 and 4 of the [Constitution of the Arbitrum DAO](https://docs.arbitrum.foundation/dao-constitution). The election flow also makes use of existing Arbitrum Governance [Arbitrum Governance](https://github.com/ArbitrumFoundation/governance/blob/main/docs/overview.md) contracts - the Timelocks and the Action system - so these are required reading for understanding the system. + +# High-level Election Overview + +The Constitution specifies that membership of the Security Council is split into two cohorts. Every 6 months, all positions in a single cohort are put up for election. + +The election implementation is split into: + +- **Election stages:** The process for selecting and voting for nominees. +- **Update stages:** Installing the newly elected cohort into the Arbitrum smart contracts. + +The election process and update stages are performed via on-chain smart contracts. A brief overview of each stage includes: + +## Election Stages + +Process for selecting and voting for the new cohort of Security council members. + +1. **Nominee Selection (7 days).** Candidates must gain 0.2% of total votable tokens in order to make it to the next step. +2. **Compliance check by Foundation (14 days).** As dictated in the Constitution, the selected nominees must undergo a compliance check to ensure they comply with the legal requirements, service agreements, and additional rules dictated by the Constitution. +3. **Member election (21 days).** Votes are cast and the top 6 nominees are selected. + +## Update Stages + +Process to install the newly elected cohort of Security Council members into the Arbitrum smart contracts. + +1. **Security Council manager update (0 days).** The manager is the source of truth for specifying who are the current council members. It processes the election result and takes note on who will be the new security council members. +2. **L2 timelock + withdrawal + L1 timelock (3 + 7 + 3 days).** All actions that directly affect the core Arbitrum contracts must go through a series of timelocks to protect the right for all users to exit. This is a built-in safety mechanism for users who are unhappy with the approved changes. +3. **Individual council update (0 days).** Once the updates have passed through the relevant timelocks, the Security Council manager can install the security council members. This requires updating 4 Gnosis Safe smart contracts that are controlled by the Security Council members. + +![](./security-council-election-flow.png) + +# Election Stages in detail + +## 1. Nominee selection (7 days) + +This stage consists of handling election timing, candidate registration, candidate endorsement: + +- **Election creation.** Elections can be created by anyone, but only every 6 months. The election alternates between targeting the positions on the two cohorts. Once created, this first stage of the election process lasts for 7 days. +- **Candidate registration.** During these 7 days, any candidate can register, unless they are already a member of the other cohort. Members of the current cohort (the cohort up for election) are allowed to register for re-election. +- **Endorsing candidates.** Delegates can endorse a candidate during this 7 day window. A single delegate can split their vote across multiple candidates. No candidate can accrue more than 0.2% of all votable tokens. +- **Fallback in case of too few candidates.** In the event that fewer than 6 candidates receive a 0.2% endorsement, the Arbitrum Foundation will randomly select members from the outgoing cohort to make up to 6 candidates. + +### Implementation details + +The nominee selection process is implemented by the `SecurityCouncilNomineeElectionGovernor` contract. + +It inherits most of its functionality from the OpenZeppelin Governor contracts, extended for some addition features: + +- Custom counting module to allow delegates to endorse multiple candidates. +- Overridden proposal and execution to make the governor single purpose. + +This governor contract has the following general interface relevant to the first stage: + +- A new proposal is created each election cycle by calling `createElection` +- Contenders can make themselves eligible to receive votes by calling `addContender`. +- Delegates can call `castVoteWithReasonAndParams` or `castVoteWithReasonAndParamsBySig` supplying custom arguments in the params to indicate which candidate they wish to endorse with what weight. + +## 2. Compliance check by the Foundation (14 days) + +The Foundation will be given 14 days to vet the prospective nominees. If they find that a candidate does not meet the compliance check, they can exclude the candidate from progressing to the next stage. The compliance rules are not detailed here, and will instead be published by the Foundation, but note that grounds for exclusion will include greater than 3 members of a given organisation being represented in the nominee set (as described in section 4 of the Constitution). + +For some further details and guidelines see [here](./security-council-nominee-vetting.md). + +### Implementation details + +The foundation can exclude a nominee by calling `excludeNominee` function on the `SecurityCouncilNomineeElectionGovernor` contract. + +If there are less than 6 eligible nominees, then the Foundation will consult with outgoing members of the cohort on whether they will continue in this role for another 12 months. Members of the existing cohort may be selected at random to fill the remaining seats. To fill an empty seat, the Foundation calls `includeNominee` + +The Governor smart contract enforces that at least a 2 week time period be provided to the Foundation to exclude/include nominees by this deadline. If 6 nominees are not selected by this deadline this phase will extend until 6 are included. + +Once the compliance check has completed, anyone can call the `execute` function on the `SecurityCouncilNomineeElectionGovernor` to proceed to the member election stage. + +## 3. Member election (21 days) + +The voting process can begin once a set of compliant candidates have been successfully nominated. + +The voting process is designed to encourage voters to cast their vote early. Their voting power will eventually decay if they do not cast their vote within the first 7 days: + +- **0 - 7 days.** Votes cast will carry weight 1 per token +- **7 - 21 days.** Votes cast will have their weight linearly decreased based on amount of time that has passed since the 7 day point. By the 21st day, each token will carry a weight of 0. + +Additionally, delegates can cast votes for more than one nominee: + +- **Split voting.** delegates can split their tokens across multiple nominees, with 1 token representing 1 vote. + +### Implementation details + +The Security Council member election will take place in a separate `SecurityCouncilMemberElectionGovernor` contract which will also inherit from OpenZeppelin Governor contracts. + +After the 14 day waiting period for the compliance check, anyone can trigger a new member election: + +- Call the execute function in `SecurityCouncilNomineeElectionGovernor` to create a new election proposal for `SecurityCouncilMemberElectionGovernor` + +The `SecurityCouncilMemberElectionGovernor` includes: + +- A custom counting module that allows delegates to split their vote and accounts for the linear decrease in voting weight. +- These additional parameters are supplied as the params argument when calling `castVoteWithReasonAndParams`. +- The custom counting module also checks that the account being voted for is a compliant nominee by checking against the list in the `SecurityCouncilNomineeElectionGovernor`. + +At the end of the 21 days of election: + +- Anyone can call `execute` on the `SecurityCouncilMemberElectionGovernor` contract to initiate the update of top 6 nominees with the most votes into `SecurityCouncilManager`. + +# Update stages in detail + +## 1. Security Council manager update + +The `SecurityCouncilManager` is the entry point for updating council members. It contains the canonical list of security council members, and which cohort they are part of. When a member election completes, the manager updates its local list of the current cohorts then forms cross chain messages to propagate those updates to each of the Security Council Gnosis safes. + +The manager also provides some additional functionality to allow the security council to: + +- **Remove a member.** As described in the Constitution, the council can remove one of its own members. The DAO can also remove a member under special condition described by the Constitution. +- **Add a member.** After removing a member, the council can add a member +- **Address rotation.** As a practical matter, a council member can rotate one of their own keys. This can only be done with the approval of at least 9/12 council members. + +See [Security Council Manager](./security-council-manager.md) as a source of truth for more details on how the Manager operates. + +### Implementation details + +The manager functionality is contained within a custom `SecurityCouncilManager` smart contract. Since the `SecurityCouncilManager` is indirectly able to make calls to the standard `UpgradeExecutor` contracts which have far reaching powers, special care must be take to ensure the manager only makes council member updates. + +Calling the `UpgradeExecutor`s on each of the chains requires navigating withdrawals transactions, timelocks and inboxes, the `SecurityCouncilManager` outsources the calldata creation for these routes to the `UpgradeExecRouteBuilder` contract. + +## 2. Timelocks and withdrawal + +Constitutional DAO proposals all pass through: + +- L2 timelock (3 days), +- L2 → L1 withdrawal (~7 days), +- L1 timelock (3 days). + +You can read more about these stages in the [governance docs](https://github.com/ArbitrumFoundation/governance/blob/main/docs/overview.md#proposal-delays). The purpose of these delays is to ensure that users who wish to withdraw their assets before the proposal is executed will have the time to do so. Changing the Security Council members should also provide this guarantee, so after the election has completed and before the Security Councils are updated the update message also goes through these same stages. The update message will use the existing timelocks to enforce these delays. + +### Implementation details + +The existing governance timelock contracts are used as part of this flow. + +The `SecurityCouncilManager` is given the `PROPOSER` role on the L2 timelock enabling it to create messages that will eventually be received by each `UpgradeExecutor` + +## 3. Individual council updates + +The new Security Council members need to be installed into 4 Gnosis safes: + +- Arbitrum One 9 of 12 Emergency Security Council +- Arbitrum One 7 of 12 Non-Emergency Security Council +- Ethereum 9 of 12 Emergency Security Council +- Nova 9 of 12 Emergency Security Council + +The old cohort of members will be removed, and the new cohort will replace them. + +### Implementation details + +To do this the existing [Upgrade Executor contracts](https://github.com/ArbitrumFoundation/governance/blob/main/docs/overview.md#l1-upgrade-executor) on each chain will be installed as Gnosis Safe modules into the Security Council safes. A custom [Governance Action Contract](https://github.com/ArbitrumFoundation/governance/blob/main/src/gov-action-contracts/README.md) will be used to call the specific `OwnerManager` `addOwnerWithThreshold` and `removeOwner` methods on the Gnosis safes. + +## Additional affordances + +The Constitution also declares some other additional affordances to certain parties + +### Removal Governor +The DAO can vote to remove a member prior to the end of their term, as long as 10% of possible votes are cast in favour and 5/6 of cast votes are in favour. This is implemented as a governor with correct quorum and proposal passed thresholds. + +It accepts proposals in the same format that normal governors do, however it overrides the propose function to check that these proposals are only executing a remove member function on the manager. This governor has the rights to call `removeMember` on the `SecurityCouncilManager`. + +Voting and proposing can occur using the standard governance UIs. + +### 9/12 Security Council + +The Security Council can remove a member prior to the end of their term, if 9 of 12 members agree. The 9 of 12 council has the rights to call `removeMember` on the `SecurityCouncilManager`. + +The Security Council can also add a member once one has been removed if 9 of 12 members agree and if there are less than 12 members currently on the council. The 9 of 12 council is be given the rights to call `addMember` on the `SecurityCouncilManager`. + +### Overall diagram +Below is a diagram showing the interaction between the different components described above: +![](./security-council-colors.png) + +### Block periods +The constitution specifies time periods in days and weeks, however in the implementation block numbers are used as a proxy for this. In the event of an L1 block time change the contracts here, and in general governance, would need to be updated to reflect the time periods again. + +## Settings of interest +Some of the following settings are updateable, others are presently not, see the in-code function documentation for how/if to update these settings and who can update them. The Constitution dictates the values of many of the settings and so in many cases will also need to be updated. + +| Contract | Property | Notes | +| --- | --- | --- | +| SecurityCouncilManager | securityCouncils | If new security councils are created, either on existing chains or on new ones, their information should be added here so that they can receive membership updates | +| SecurityCouncilManager | router | The router is used to locate Upgrade Executors on other chains, and form calldata for accessing them. The router itself is immutable, so if Upgrade Executors change locations, or new ones are added on other chains then a new UpgradeExecRouteBuilder will need to be created with the correct settings and its address updated on the Manager | +| SecurityCouncilManager | cohortSize | There is no method for changing cohort size, but if this value needs to change it will be updated on Manager, or a new Manager deployed. One consideration here is that some operations are O(n^2) in the cohort size, so before setting a new cohort size these operations should be checked to see if they can still be executed. All usages of cohort size should also be checked. The threshold in the multisigs should also be considered when changing the cohort size. | +| SecurityCouncilMemberElection GovernorCountingUpgradeable | fullWeightDuration | Sets the duration for which votes have full weight in during the member election phase. | +| SecurityCouncilMember RemovalGovernor | voteSuccessNumerator | Determines the for/against ratio that can cause a removal proposal to pass. | +| SecurityCouncilNominee ElectionGovernor | nomineeVetter | The address that can call includeNominee and excludeNominee. As mentioned above a multisig should be set to this address as changing it requires a Constiutional proposal. | +| SecurityCouncilNominee ElectionGovernor | quorumNumeratorValue | There is no method for updating the quorum numerator value, doing so would require upgrading the contract. This value dictates the maximum possible number of nominees (currently 500 due to numerator being 20 and denominator being 10000), so when changing this number the length of time it takes to vet these nominees needs to be considered. Also the contracts do some O(n) operations, so these need to be checked to see if they’ll still execute with the new value | + + diff --git a/docs/security-council-nominee-vetting.md b/docs/security-council-nominee-vetting.md new file mode 100644 index 00000000..55aeb565 --- /dev/null +++ b/docs/security-council-nominee-vetting.md @@ -0,0 +1,31 @@ +# Nominee vetting guidelines + +7 days after an election has begun there is a 14 day vetting period to allow the Foundation to conduct compliance checks on each of the nominees that have received 0.2% of votable tokens. They can then exclude nominees who fail these checks, and include nominees if fewer than 6 nominees have been selected in the Nominee Election phase. The Member Election Governor authorises a specific address that is controlled by the Foundation to do these actions. + +### Foundation vetting security best practice + +The address authorised to exclude and include addresses should be a multisig that allows for address rotation for the following reasons: + +- Inclusion and exclusion is a powerful role that could be used to manipulate elections. An m-of-n multisig will help to guard against key compromise +- To change the vetting address in the member election governor requires a Constitutional proposal, or Security Council emergency action, so the Foundation should use a multisig to manage their own key rotation rather than being able to rotate the authorised address. + +### Excluding addresses + +7 days after an election has begun there is a 14 day vetting period to allow the Foundation to conduct compliance checks on each of the nominees that have received 0.2% of votable tokens. Amongst other things the Foundation should consider: + +- Does the entity behind the address conform to all legal requirements that the Foundation has of Security Council members? +- Is the entity a member of an organisation that if all nominees were elected would result in having more than 3 members of that organisation in the Security Council? As an example, if there are already 2 members of an org in the other cohort then only 1 member can be selected as a nominee for the current election. +- Is the address already a member of the opposite cohort? The contracts check that this was not the case at the time of creation, but manipulation in the Manager could cause this to be violated at a later time. +- Is the owner of the address able to create a signature of all chains where a Security Council is located that will be accepted by a Gnosis Safe? + +If any of the above are false the Foundation should call `excludeAddress` to stop that address from proceeding to the next stage. + +### Including addresses + +During the 14 day vetting period the Foundation should include new addresses if the number of nominees that achieved a threshold of 0.2% of votable tokens and have not been excluded is less than 6. They should include new addresses until there are a total of 6 nominees that will progress to the next stage. New addresses should be selected in the following order, if each stage does not yield enough included addresses to make up to 6 nominees then the Foundation should progress to the next stage: + +1. Members from the outgoing cohort that fulfil the requirements to not be excluded should be chosen at random. +2. Contenders who did not receive 0.2% threshold of votes should be included, in order of votes descending +3. If there still aren’t 6 nominees the Foundation may include any address they see fit + +In the event that the Foundation does not fill these spots the DAO or Security Council will need to take action to remedy the situation. \ No newline at end of file diff --git a/files/mainnet/scmDeployment.json b/files/mainnet/scmDeployment.json new file mode 100644 index 00000000..82e894ce --- /dev/null +++ b/files/mainnet/scmDeployment.json @@ -0,0 +1,33 @@ +{ + "activationActionContracts": { + "1": "0x22EC545357162C342F643bDdb2eD4c3FB6B42eb0", + "42161": "0x1015c1Ae166C4C39D18a1151b7029bAC1530c9aa", + "42170": "0x22EC545357162C342F643bDdb2eD4c3FB6B42eb0" + }, + "keyValueStores": { + "1": "0xd343Fd9ba453D3AD0f868c24734808FB73f5F52B", + "42161": "0xd343Fd9ba453D3AD0f868c24734808FB73f5F52B", + "42170": "0xd343Fd9ba453D3AD0f868c24734808FB73f5F52B" + }, + "securityCouncilMemberSyncActions": { + "1": "0x9BF7b8884Fa381a45f8CB2525905fb36C996297a", + "42161": "0x9BF7b8884Fa381a45f8CB2525905fb36C996297a", + "42170": "0x9BF7b8884Fa381a45f8CB2525905fb36C996297a" + }, + "emergencyGnosisSafes": { + "1": "0xF06E95eF589D9c38af242a8AAee8375f14023F85", + "42161": "0x423552c0F05baCCac5Bfa91C6dCF1dc53a0A1641", + "42170": "0xc232ee726E3C51B86778BB4dBe61C52cC07A60F3" + }, + "nonEmergencyGnosisSafe": "0xADd68bCb0f66878aB9D37a447C7b9067C5dfa941", + "nomineeElectionGovernor": "0x8a1cDA8dee421cD06023470608605934c16A05a0", + "nomineeElectionGovernorLogic": "0x8436A1bc9f9f9EB0cF1B51942C5657b60A40CCDD", + "memberElectionGovernor": "0x467923B9AE90BDB36BA88eCA11604D45F13b712C", + "memberElectionGovernorLogic": "0xbD5016a7946a82b938242573ED63ccB8962A4acB", + "securityCouncilManager": "0xD509E5f5aEe2A205F554f36E8a7d56094494eDFC", + "securityCouncilManagerLogic": "0x468dA0eE5570Bdb1Dd81bFd925BAf028A93Dce64", + "securityCouncilMemberRemoverGov": "0x6f3a242cA91A119F872f0073BC14BC8a74a315Ad", + "securityCouncilMemberRemoverGovLogic": "0x2F06643fc2CC18585Ae790b546388F0DE4Ec6635", + "upgradeExecRouteBuilder": "0x7481716f05E315Fc4C4a64E56DcD9bc1D6F24C0a", + "l2SecurityCouncilMgmtFactory": "0xe8e5DC1793d6fe39452DdCB90d12997FA39dE1de" +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 8a25cf7a..1e790cbe 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,3 +1,4 @@ +# when changing optimizer settings, make sure to also change settings in hardhat.config.ts [profile.default] src = 'src' out = 'out' @@ -5,6 +6,10 @@ libs = ['lib'] optimizer = true optimizer_runs = 20000 via_ir = false +solc_version = '0.8.16' + +[profile.sec_council_mgmt] +optimizer_runs = 1500 [fmt] number_underscore = 'thousands' diff --git a/hardhat.config.ts b/hardhat.config.ts index 555fce47..19b50a66 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,16 +1,31 @@ import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; +import "@nomicfoundation/hardhat-foundry"; -const config: HardhatUserConfig = { - solidity: { +// when changing optimizer settings, make sure to also change settings in foundry.toml +const solidityProfiles = { + default: { version: "0.8.16", settings: { optimizer: { enabled: true, runs: 20000 }, - }, + } }, + sec_council_mgmt: { + version: "0.8.16", + settings: { + optimizer: { + enabled: true, + runs: 1500 + }, + } + } +} + +const config: HardhatUserConfig = { + solidity: solidityProfiles[process.env.FOUNDRY_PROFILE || "default"] || solidityProfiles.default, paths: { sources: "./src", tests: "./test-ts", diff --git a/lib/solady b/lib/solady new file mode 160000 index 00000000..7175c21f --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 7175c21f95255dc7711ce84cc32080a41864abd6 diff --git a/package.json b/package.json index 42378f58..146ee40e 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,18 @@ "build": "yarn build:governance && yarn build:token-bridge-types", "build:governance": "hardhat compile", "build:token-bridge-types": "cd token-bridge-contracts && yarn && yarn build", + "build:sc-mgmt": "FOUNDRY_PROFILE=sec_council_mgmt hardhat compile", + "build:gnosis-safe-types": "typechain --target ethers-v5 scripts/security-council-mgmt-deployment/safe-abis/*.json", "gen:network": "ts-node ./scripts/genNetwork.ts", "gen:nova:network": "ts-node ./scripts/genNetworkNova.ts", "gen:recipients": "ts-node ./scripts/genRecipients.ts", "deploy:governance": "ts-node ./scripts/governanceDeployer.ts", "deploy:note-store": "ts-node ./scripts/deployNoteStore.ts", "deploy:vested-wallets": "ts-node ./scripts/vestedWalletsDeployer.ts", + "deploy:dummy-elections": "yarn build:sc-mgmt && ts-node scripts/security-council-mgmt-deployment/deployDummyElectionsContracts.ts", "verify:vested-wallets": "ts-node ./scripts/vestedWalletsDeploymentVerifier.ts", "verify:governance": "ts-node ./scripts/governanceDeploymentVerifier.ts", + "deploy:sc-mgmt": "yarn build:sc-mgmt && yarn build:gnosis-safe-types && ts-node ./scripts/security-council-mgmt-deployment/main.ts", "allocate:tokens": "ts-node ./scripts/tokenAllocator.ts", "allocate:dao:tokens": "ts-node ./scripts/tokenDaoAllocator.ts", "verify:distribution:partial": "FULL_TOKEN_VERIFY=false ts-node ./scripts/tokenDistributionVerifier.ts", @@ -34,8 +38,9 @@ "format:sol": "forge fmt", "coverage:filtered-report": "forge coverage --report lcov && lcov --remove ./lcov.info -o ./lcov.info.pruned 'node_modules' 'test' 'hardhatTest' 'gov-action-contracts'", "coverage:htmlreport": "rm -rf ./report && genhtml ./lcov.info.pruned -o report --branch-coverage", - "coverage:open-report": "open ./report/index.html", + "coverage:open-report": "node -e 'const opn = process.platform === \"darwin\" ? \"open\" : \"xdg-open\"; require(\"child_process\").exec(`${opn} ./report/index.html`)'", "coverage:report": "yarn coverage:filtered-report && yarn coverage:htmlreport && yarn coverage:open-report", + "coverage:refresh": "yarn coverage:filtered-report && yarn coverage:htmlreport", "propmon:ui": "cd src-ts && http-server . -p 8080", "propmon:service": "ts-node ./src-ts/proposalMonitorCli.ts --jsonOutputLocation ./src-ts/propMonUi/proposalState.json --l1RpcUrl $ETH_RPC --govChainRpcUrl https://arb1.arbitrum.io/rpc --novaRpcUrl https://nova.arbitrum.io/rpc --coreGovernorAddress 0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9 --treasuryGovernorAddress 0x789fC99093B09aD01C34DC7251D0C89ce743e5a4 --sevenTwelveCouncil 0x895c9fc6bcf06e553b54A9fE11D948D67a9B76FA --pollingIntervalSeconds 1", "propmon": "yarn propmon:service & yarn propmon:ui" @@ -45,10 +50,13 @@ "@ethersproject/abi": "^5.7.0", "@ethersproject/providers": "^5.7.2", "@nomicfoundation/hardhat-chai-matchers": "^1.0.4", + "@nomicfoundation/hardhat-foundry": "^1.0.2", "@nomicfoundation/hardhat-network-helpers": "^1.0.6", "@nomicfoundation/hardhat-toolbox": "^2.0.0", "@nomiclabs/hardhat-ethers": "^2.2.0", "@nomiclabs/hardhat-etherscan": "^3.1.1", + "@safe-global/protocol-kit": "^1.2.0", + "@safe-global/safe-core-sdk-types": "^2.2.0", "@typechain/ethers-v5": "^10.1.0", "@typechain/hardhat": "^6.1.3", "@types/mocha": "^10.0.0", @@ -56,7 +64,7 @@ "chai": "^4.3.6", "dotenv": "^16.0.3", "ethers": "^5.7.2", - "hardhat": "^2.12.0", + "hardhat": "^2.12.6", "hardhat-gas-reporter": "^1.0.9", "http-server": "^14.1.1", "solidity-coverage": "^0.8.2", @@ -67,6 +75,7 @@ "dependencies": { "@arbitrum/nitro-contracts": "1.0.1", "@arbitrum/token-bridge-contracts": "1.0.0-beta.0", + "@gnosis.pm/safe-contracts": "1.3.0", "@openzeppelin/contracts": "4.7.3", "@openzeppelin/contracts-upgradeable": "4.7.3", "@types/yargs": "^17.0.17", diff --git a/remappings.txt b/remappings.txt index 2f1b4b72..bf258e90 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,8 +1,9 @@ -ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ +solady/=lib/solady/src/ @arbitrum/token-bridge-contracts/=node_modules/@arbitrum/token-bridge-contracts @arbitrum/nitro-contracts/=node_modules/@arbitrum/nitro-contracts/ @openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@gnosis.pm/safe-contracts/=node_modules/@gnosis.pm/safe-contracts/ \ No newline at end of file diff --git a/scripts/minimalContractVerifier.ts b/scripts/minimalContractVerifier.ts new file mode 100644 index 00000000..1187889c --- /dev/null +++ b/scripts/minimalContractVerifier.ts @@ -0,0 +1,50 @@ +import { AbiCoder } from "@ethersproject/abi"; +import { ContractFactory } from "ethers"; +import { exec } from "child_process"; + +export type ContractVerificationConfig = { + factory: ContractFactory, + contractName: string, + chainId: number, + address: string, + constructorArgs: any[], + foundryProfile: string, + etherscanApiKey: string, +}; + +export async function verifyContracts(configs: ContractVerificationConfig[], delay: number = 1000) { + for (const config of configs) { + console.log(`Verifying ${config.address} (${config.contractName}) on chain ${config.chainId}...`); + if (configs.length > 1) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } + await verifyContract(config); + } +} + +export function verifyContract(config: ContractVerificationConfig) { + const encodedConstructorArgs = new AbiCoder().encode(config.factory.interface.deploy.inputs, config.constructorArgs); + let command = `FOUNDRY_PROFILE=${config.foundryProfile} forge verify-contract --watch --chain-id ${config.chainId} --etherscan-api-key ${config.etherscanApiKey}`; + + if (config.constructorArgs.length > 0) { + command += ` --constructor-args ${encodedConstructorArgs}`; + } + + command += ` ${config.address} ${config.contractName}`; + + return new Promise((resolve, reject) => { + exec(command, (err: Error | null, stdout: string, stderr: string) => { + if (err) { + reject([err, stderr]); + } + else { + // could also extract the GUID from stdout and return it instead + resolve(stdout) + } + }); + }); +} + +// function extractGuid(stdout: string) { +// return stdout.split("\n").find((line) => line.trim().startsWith("GUID:"))?.split("GUID:")[1].trim().replace('`', '').replace('`', ''); +// } \ No newline at end of file diff --git a/scripts/proposals/AIP12/generateProposalData.ts b/scripts/proposals/AIP12/generateProposalData.ts index ab7e1a44..dc06cff4 100644 --- a/scripts/proposals/AIP12/generateProposalData.ts +++ b/scripts/proposals/AIP12/generateProposalData.ts @@ -1,6 +1,6 @@ import { generateArbSysArgs } from "../../genGoverningChainTargetedProposalArgs"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { CoreGovPropposal } from "../coreGovProposalInterface"; +import { SingleActionCoreGovProposal } from "../coreGovProposalInterface"; import fs from "fs"; import dotenv from "dotenv"; dotenv.config(); @@ -57,7 +57,7 @@ const main = async () => { actionAddress, description ); - const proposal: CoreGovPropposal = { + const proposal: SingleActionCoreGovProposal = { actionChainID: chainId, actionAddress, description, diff --git a/scripts/proposals/AIP4/generateProposalData.ts b/scripts/proposals/AIP4/generateProposalData.ts index 120a669b..5b9a5d78 100644 --- a/scripts/proposals/AIP4/generateProposalData.ts +++ b/scripts/proposals/AIP4/generateProposalData.ts @@ -1,6 +1,6 @@ import { generateArbSysArgs } from "../../genGoverningChainTargetedProposalArgs"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { CoreGovPropposal } from "../coreGovProposalInterface"; +import { SingleActionCoreGovProposal } from "../coreGovProposalInterface"; import fs from "fs"; import dotenv from "dotenv"; dotenv.config(); @@ -88,7 +88,7 @@ const main = async () => { actionAddress, description ); - const proposal: CoreGovPropposal = { + const proposal: SingleActionCoreGovProposal = { actionChainID: chainId, actionAddress, description, diff --git a/scripts/proposals/AIP6/data/42161-AIP6-data.json b/scripts/proposals/AIP6/data/42161-AIP6-data.json new file mode 100644 index 00000000..c6648a9d --- /dev/null +++ b/scripts/proposals/AIP6/data/42161-AIP6-data.json @@ -0,0 +1,17 @@ +{ + "actionChainIds": [ + 1, + 42161, + 42170 + ], + "actionAddresses": [ + "0x22EC545357162C342F643bDdb2eD4c3FB6B42eb0", + "0x1015c1Ae166C4C39D18a1151b7029bAC1530c9aa", + "0x22EC545357162C342F643bDdb2eD4c3FB6B42eb0" + ], + "description": "\nAIP 6: Activate Security Council Elections\nCategory: Constitutional - Process\nAs part of its governance, the Arbitrum DAO incorporates a Security Council that can take certain emergency and non-emergency actions:\nSection 3 of the Constitution describes this council in more detail.\nInitial Setup Transparency Report details the current members of the council.\nSection 4 on the Constitution outlines an election process that will replace six security council members every six months.\nThe first election to replace the first cohort of Security Council Members is expected to begin on the 15th September.\nExecutive summary\nThis vote relates to the enactment of the Arbitrum Security Council elections.\nA smart contract system has been developed to enable on-chain voting. That system is final and has undergone multiple security audits.\nThis vote is meant to enable the smart contract system that will allow for the elections, as well as temperature check with the DAO.\nA successful vote will lead to an on-chain proposal that updates the constitution text with the new sections below, as well as the new Security Council election system being activated within the broader Arbitrum governance architecture.\nImplementation Status\nThe code representing the proposed architecture is final and available for full review in: https://github.com/arbitrumfoundation/governance/tree/949b303a8bc27d6b763d434e1ee15b6c87a765cd. \nThe work has already been audited by Trail of Bits and the identified issues have been patched, the full audit report is available in the codebase. \nA Code4Rena audit competition has also been completed for the election system.\n\nBackground\nAn overview of the election process as described by the Constitution alongside the process for how to enact a code change to Arbitrum’s smart contract suite:\nConstitution of the Arbitrum DAO (esp. sections 3 and 4) - As noted above, this proposal seeks to enact what is already described in the Constitution.\nDAO Governance Architecture - The proposed architecture makes use of a number of existing governance components.\nAn understanding of the following smart contract suites will help the reader evaluate this proposal as it re-uses several components:\nOpen Zeppelin Governor - The proposed architecture inherits a number of OpenZeppelin contracts.\nGnosis Modules - Each of the security councils is a Gnosis Safe, and updating members of the security council is handled via adding Gnosis modules.\n\nHigh-level Election Overview\nThe Constitution specifies that membership of the Security Council is split into two cohorts. Every 6 months, all positions in a single cohort are put up for election.\nThe proposed election implementation, which must adhere to the specification laid out in the Constitution, is split into:\nElection stages: The process for selecting and voting for nominees.\nUpdate stages: Installing the newly elected cohort into the Arbitrum smart contracts.\nThe election process and update stages are performed via on-chain smart contracts. A brief overview of each stage includes:\nElection Stages\nProcess for selecting and voting for the new cohort of Security council members.\nNominee selection (7 days). Candidates must gain 0.2% of total votable tokens in order to make it to the next step.\nCompliance check by Foundation (14 days). As dictated in the Constitution, the selected nominees must undergo a compliance check to ensure they comply with the legal requirements, service agreements, and additional rules dictated by the Constitution.\nMember election (21 days). Votes are cast and the top 6 nominees are selected.\nUpdate Stages\nProcess to install the newly elected cohort of Security Council members into the Arbitrum smart contracts.\nSecurity Council manager update (0 days). The manager is the source of truth for specifying who are the current council members. It processes the election result and takes note on who will be the new security council members.\nL2 timelock + withdrawal + L1 timelock (3 + 7 + 3 days). All actions that directly affect the core Arbitrum contracts must go through a series of timelocks to protect the right for all users to exit. This is a built-in safety mechanism for users who are unhappy with the approved changes.\nIndividual council update (0 days). Once the updates have passed through the relevant timelocks, the Security Council manager can install the security council members. This requires updating 4 Gnosis Safe smart contracts that are controlled by the Security Council members.\n\nElection Stages in detail\n1. Nominee selection (7 days)\nThis stage consists of handling election timing, candidate registration, candidate endorsement:\nElection creation. Elections can be created by anyone, but only every 6 months. The election alternates between targeting the positions on the two cohorts. Once created, this first stage of the election process lasts for 7 days.\nCandidate registration. During these 7 days, any candidate can register, unless they are already a member of the other cohort. Members of the current cohort (the cohort up for election) are allowed to register for re-election.\nEndorsing candidates. Delegates can endorse a candidate during this 7 day window. A single delegate can split their vote across multiple candidates. No candidate can accrue more than 0.2% of all votable tokens.\nFallback in case of too few candidates. In the event that fewer than 6 candidates receive a 0.2% endorsement, outgoing members of the cohort up for election will be selected to make up to 6 candidates.\nImplementation details\nThe nominee selection process is implemented by the SecurityCouncilNomineeElectionGovernor contract.\nIt inherits most of its functionality from the Open Zeppelin Governor contracts and we have extended it with an extra feature:\nCustom counting module to allow delegates to endorse multiple candidates.\nThe governor contract has the following characteristics:\nA new proposal is created each election cycle, with an identifier unique to that election cycle.\nCandidates can the put themselves forward by calling addContender.\nDelegates can call castVoteWithReasonAndParams, supplying custom arguments in the params to indicate which candidate they wish to endorse with what weight.\n2. Compliance check by the Foundation(14 days)\nThe Foundation will be given 14 days to vet the prospective nominees. If they find that a candidate does not meet the compliance check, they can exclude the candidate from progressing to the next stage. Note that grounds for exclusion could include greater than 3 members of a given organization being represented in the nominee set (as described in section 4 of the Constitution).\nImplementation details\nThe foundation can exclude a nominee by:\nCalling a custom excludeNominee function on the same SecurityCouncilNomineeElectionGovernor contract.\nThe Governor smart contract enforces the 2 week time period and the Foundation must exclude nominees by this deadline.\nOnce the compliance check has completed:\nAnyone can call the execute function on the SecurityCouncilNomineeElectionGovernor to proceed to the member election stage.\nIf there are less than 6 eligible nominees, then the Foundation will consult with outgoing members of the cohort on whether they will continue in this role for another 12 months. Members of the existing cohort may be selected at random to fill the remaining seats.\n3. Member election (21 days)\nThe voting process can begin once a set of compliant candidates have been successfully nominated.\nThe voting process is designed to encourage voters to cast their vote early. Their voting power will eventually decay if they do not cast their vote within the first 7 days:\n0 - 7 days. Votes cast will carry weight 1 per token\n7 - 21 days. Votes cast will have their weight linearly decreased based on the amount of time that has passed since the 7 day point. By the 21st day, each token will carry a weight of 0.\nAdditionally, delegates can cast votes for more than one nominee:\nSplit voting. delegates can split their tokens across multiple nominees, with 1 token representing 1 vote.\nImplementation details\nThe Security Council member election will take place in a separate SecurityCouncilMemberElectionGovernor contract which will also inherit from Open Zeppelin Governor contracts.\nAfter the 14 day waiting period for the compliance check, anyone can trigger a new member election:\nCall the execute function in SecurityCouncilNomineeElectionGovernor to deploy a new election proposal for SecurityCouncilMemberElectionGovernor\nThe SecurityCouncilMemberElectionGovernor includes:\nA custom counting module that allows delegates to split their vote and accounts for the linear decrease in voting weight.\nThese additional parameters are supplied as the params argument when calling castVoteWithReasonAndParams.\nThe custom counting module also checks that the nominee being voted is a compliant one by checking against the compliant nominee list in the SecurityCouncilNomineeElectionGovernor.\nAt the end of the 21 days of election:\nAnyone can call the execute function on the SecurityCouncilMemberElectionGovernor contract to initiate the update of top 6 nominees with the most votes into SecurityCouncilManager.\n\nUpdate stages in detail\n1. Security Council manager update\nThe security council manager is a contract which contains the canonical list of security council members, and which cohort they are part of. When a member election completes, the manager updates its local list of the current cohorts then forms cross chain messages to propagate those updates to each of the Security Council Gnosis safes.\nThe manager also provides some additional functionality to allow the security council to:\nRemove a member: As described in the Constitution, the council can remove one of its own members. The DAO can also remove a member under special conditions described by the Constitution.\nAdd a member: After removing a member, the council can add a member\nAddress rotation: As a practical matter, a council member can rotate one of their own keys. This can only be done with the approval of at least 9/12 council members.\nImplementation details\nThe manager functionality is contained within a custom SecurityCouncilManager smart contract. Since the SecurityCouncilManager is indirectly able to make calls to the standard UpgradeExecutor contracts which have far reaching powers, special care must be take to ensure the manager only makes council member updates.\nCalling the UpgradeExecutors on each of the chains requires navigating withdrawals transactions, timelocks and inboxes, the SecurityCouncilManager outsources the calldata creation for these routes to a UpgradeExecRouteBuilder contract.\n2. Timelocks and withdrawal\nConstitutional DAO proposals all pass through:\nL2 timelock (3 days),\nL2 → L1 withdrawal (~7 days),\nL1 timelock (3 days).\nYou can read more about these stages in the governance docs. The purpose of these delays is to ensure that users wishing to withdraw their assets before the proposal is executed will have the time to do so. Changing the Security Council members should also provide this guarantee, so after the election has completed and before the Security Councils are updated the update message also goes through these same stages. The update message will use the existing timelocks to enforce these delays.\nImplementation details\nThe existing governance timelock contracts are used as part of this flow.\nThe SecurityCouncilManager is given the PROPOSER role on the L2 timelock enabling it to create messages that will eventually be received by each UpgradeExecutor.\n3. Individual council updates\nThe new Security Council members need to be installed into 4 Gnosis safes:\nArbitrum One 9 of 12 Emergency Security Council\nArbitrum One 7 of 12 Non-Emergency Security Council\nEthereum 9 of 12 Emergency Security Council\nNova 9 of 12 Emergency Security Council\nThe old cohort of members will be removed, and the new cohort will replace them.\nImplementation details\nTo do this the existing Upgrade Executor contracts on each chain will be installed as Gnosis Safe modules into the Security Council safes. A custom Governance Action Contract will be used to call the specific OwnerManager addOwnerWithThreshold and removeOwner methods on the Gnosis safes.\nAdditional affordances\nThe Constitution also declares some other additional affordances to certain parties\nThe DAO can vote to remove a member prior to the end of their term, as long as 10% of possible votes are cast in favor and 5/6 of cast votes are in favor. This will be implemented as a governor with correct quorum and proposal passed thresholds. This governor will be given the rights to call removeMember on the SecurityCouncilManager.\nThe Security Council can remove a member prior to the end of their term, if 9 of 12 members agree. The 9 of 12 council will be given the rights to call removeMember on the SecurityCouncilManager.\nThe Security Council can add a member once one has been removed, if 9 of 12 members agree and if there are less than 12 members currently on the council. The 9 of 12 council will be given the rights to call addMember on the SecurityCouncilManager.\n\nConstitution Updates\nThe proposed implementation mostly satisfies the specification outlined by the Arbitrum Constitution. There are some minor changes that are required to the Constitution’s text to take into account the time it takes to install new candidates and to support compliance procedures set out by the Arbitrum Foundation.\nNote, the final wording for how to update the Constitution will be provided in a later revision. We simply want to notify the requirement that the text needs to be changed. At this stage, our request for feedback is focused on the implementation details of the smart contract suite.\nUpdate timeline for the election\nThe Section 4 of the Constitution contains the text:\nFrom T until T+7 days: Any DAO member may declare their candidacy for the Security Council; provided that a current Security Council member in one cohort may not be a candidate for a seat in the other cohort. To the extent that there are more than six candidates, each eligible candidate must be supported by pledged votes representing at least 0.2% of all Votable Tokens. In the event that fewer than six candidates are supported by pledged votes representing at least 0.2% of all Votable Tokens, the current Security Council members whose seats are up for election may become candidates (as randomly selected out of their Cohort) until there are 6 candidates.\nFrom T+7 days until T+28 days: Each DAO member or delegate may vote for any declared candidate. Each token may be cast for one candidate. Votes cast before T+14 days will have 100% weight. Votes cast between T+14 days and T+28 days will have weight based on the time of casting, decreasing linearly with time, with 100% weight at T+14 days, decreasing linearly to 0% weight at T+28 days.\nAt T+28 days: The 6 candidates who have received the most votes are elected and immediately join the Council, replacing the Cohort that was up for re-election.\nWe need to make three changes to the Arbitrum Constitution: \nNew timeline. A dedicated compliance process must be included between the nominee selection and member election phases. This will shift the timeline of events and the total election will now last at least 42 days alongside additional time to install the newly elected Security Council members via the on-chain governance smart contracts. \nLess than 6 eligible nominees. The Arbitrum Foundation has the authority to add new nominees during the Compliance stage if there are less than 6 eligible nominees. \nInstallation time. We need to remove the phrase ‘immediately join the Council’ to take into account the on-chain governance process for installing the newly elected candidates. For example, the various time locks to protect a user’s right to exit Arbitrum during the upgrade and the time it takes to send an L2 -> L1 message. \nWith the above in mind, we propose an update to Section 4 of the Constitution with the following text:\nNominee selection (T until T+7 days): Any DAO member may declare their candidacy for the Security Council; provided that a current Security Council member in one cohort may not be a candidate for a seat in the other cohort. To the extent that there are more than six candidates, each eligible candidate must be supported by pledged votes representing at least 0.2% of all Votable Tokens.\nCompliance process (T+7 until T+21 days): All candidates will cooperate with The Arbitrum Foundation and complete the compliance process. The Arbitrum Foundation is responsible for removing any candidates that fail the compliance process. In the event that fewer than six candidates are supported by pledged votes representing at least 0.2% of all Votable Tokens, the current Security Council members whose seats are up for election may become candidates (as randomly selected out of their Cohort) until there are 6 candidates.\nMember election (T+21 until T+42 days): Each DAO member or delegate may vote for any declared candidate. Each token may be cast for one candidate. Votes cast before T+28 days will have 100% weight. Votes cast between T+28 days and T+42 days will have weight based on the time of casting, decreasing linearly with time, with 100% weight at T+28 days, decreasing linearly to 0% weight at T+42 days.\nAt T+42 days: The process for replacing the cohort of Security Council members with the 6 candidates who received the most votes will be activated. The installation process must be executed via the on-chain governance smart contracts and it may take several days until the new Security Council members are installed. \nCompliance checks by the foundation.\nThe Constitution contains the text:\n“Prior to the next Security Council election, The Arbitrum Foundation shall establish and set forth more detailed procedures and guidelines regarding the election process for the Security Council, which may include, but aren’t limited to, a candidate intake process in order to comply with Cayman Islands laws, a standard template for candidates to complete for purposes of their public nominations and other processes to ensure an orderly, fair and transparent election.”\nThe text gives an affordance to the Foundation to conduct a compliance check on the potential nominees. We propose to make this check an explicit stage of 14 days between Nominee Selection and Member Election to allow the Foundation to conduct these checks. Details of compliance checks will be provided by the Arbitrum Foundation at a later date.\nWe propose to update the Constitution with the following text: \nThe Arbitrum Foundation is allocated 14 days for the Compliance process and it should be executed between the Nominee selection and Member election. The Arbitrum Foundation has flexibility to update its compliance policy for every new election. This is required to allow The Arbitrum Foundation to comply with Cayman Island laws. Furthermore, The Arbitrum Foundation maintains the right to issue new procedures and guidelines for off-chain components of the Security Council election. All efforts should be made by The Arbitrum Foundation to ensure an orderly, fair, and transparent election. \nSecurity Council cannot re-appoint a member who was removed by the DAO.\nThe Constitution contains the text:\nThe seats of Security Council members who have been removed prior to the end of their respective terms shall remain unfilled until the next election that such seats are up for appointment, unless otherwise replaced prior to such next election by a vote of at least 9 of the Security Council members, in which case such seat shall be up for appointment at the next such election. \nThe text focuses on how to replace a security council member who was removed by a vote from the DAO or 9/12 of the current security council members. We plan to update the Constitution to remove an edge-case that allows the Security Council to re-appoint a member who was removed by a DAO vote. \nWe propose to update the Constitution with the following text: \nThe seats of Security Council members who have been removed prior to the end of their respective terms shall remain unfilled until the next election that such seats are up for appointment, unless otherwise replaced prior to such next election by a vote of at least 9 of the Security Council members, in which case such seat shall be up for appointment at the next such election. The Security Council may not re-appoint a member removed and they must be re-elected via the election voting system. \nFinal update to the Constitution text.\nFor completeness, the amended text for Section 4 of the Arbitrum Constitution, including the changes from AIP-4: \nThe Security Council has 12 members, who are divided into two Cohorts of 6 members.\nThe initial Security Council Cohorts were determined by randomly splitting the 12 members into two 6-member cohorts - 6 members in the 'First Cohort' and 6 members in the 'Second Cohort'. The members of the initial Security Council Cohorts are detailed in a transparency report here.\nThe first Security Council election is scheduled to begin on the 15th September 2023 or the earliest possible date. The election can only begin upon the availability of an on-chain election process that is approved and installed by the Arbitrum DAO. This first election replaces the 'First Cohort'. The next election replaces the 'Second Cohort' and so forth.\nThe date chosen for the first election will form the basis for all future elections. Every election should begin 6 months after the previous election has started and it will replace its respective cohort of 6 members.\nAll Security Council members are expected to serve their term until the election is complete and the new Security Council members are installed.\nThe following timeline governs an election that starts at time T:\nNominee selection (T until T+7 days): Any DAO member may declare their candidacy for the Security Council; provided that a current Security Council member in one cohort may not be a candidate for a seat in the other cohort. To the extent that there are more than six candidates, each eligible candidate must be supported by pledged votes representing at least 0.2% of all Votable Tokens.\nCompliance process (T+7 until T+21 days): All candidates will cooperate with the Arbitrum Foundation and complete the compliance process. The Arbitrum Foundation is responsible for removing any candidates that fail the compliance process. In the event that fewer than six candidates are supported by pledged votes representing at least 0.2% of all Votable Tokens, the current Security Council members whose seats are up for election may become candidates (as randomly selected out of their Cohort) until there are 6 candidates.\nMember election (T+21 until T+42 days): Each DAO member or delegate may vote for any declared candidate. Each token may be cast for one candidate. Votes cast before T+14 days will have 100% weight. Votes cast between T+21 days and T+35 days will have weight based on the time of casting, decreasing linearly with time, with 100% weight at T+21 days, decreasing linearly to 0% weight at T+42 days.\nAt T+42 days: The process for replacing the cohort of security council members with the 6 candidates who received the most votes will be activated. The installation process must be executed via the on-chain governance smart contracts and it may take several days until the new security council members are installed. \nThe Arbitrum Foundation is allocated 14 days for the Compliance process and it should be executed between the Nominee selection and Member election. The Arbitrum Foundation has flexibility to update its compliance policy for every new election. This is required to allow The Arbitrum Foundation to comply with Cayman Island laws. Furthermore, The Arbitrum Foundation maintains the right to issue new procedures and guidelines for off-chain components of the Security Council election. All efforts should be made by The Arbitrum Foundation to ensure an orderly, fair, and transparent election. \nAs a matter of best practice for maintaining an independent Security Council, no single organization should be overly represented in the Security Council. In particular, there should not be more than 3 candidates associated with a single entity or group of entities being elected to the Security Council, thereby ensuring that there will be no single entity or group of entities able to control or even veto a Security Council vote.\nFurthermore, no candidate with conflicts of interest that would prevent them from acting in the best interests of the ArbitrumDAO, Governed Chains and/or The Arbitrum Foundation should be elected to the Security Council. Potential conflicts of interest could be, but are not limited to, affiliations with direct Arbitrum competitors, proven histories of exploiting projects and others.\nThe DAO may approve and implement a Constitutional AIP to change the rules governing future Security Council elections, but the AIP process may not be used to intervene in an ongoing election.\nSecurity Council members may only be removed prior to the end of their terms under two conditions:\nAt least 10% of all Votable Tokens have casted votes \"in favor\" of removal and at least 5/6 (83.33%) of all casted votes are \"in favor\" of removal; or\nAt least 9 of the Security Council members vote in favor of removal.\nThe seats of Security Council members who have been removed prior to the end of their respective terms shall remain unfilled until the next election that such seats are up for appointment, unless otherwise replaced prior to such next election by a vote of at least 9 of the Security Council members, in which case such seat shall be up for appointment at the next such election. The Security Council may not re-appoint a removed member and they can only be re-elected via the election voting system. \n", + "arbSysSendTxToL1Args": { + "l1Timelock": "0xE6841D92B0C345144506576eC13ECf5103aC7f49", + "calldata": "0x8f2a0bb000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000030000000000000000000000003fffbadaf827559da092217e474760e2b2c3cedd000000000000000000000000a723c008e76e379c55599d2e4d93879beafda79c000000000000000000000000a723c008e76e379c55599d2e4d93879beafda79c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000000841cff79cd00000000000000000000000022ec545357162c342f643bddb2ed4c3fb6b42eb000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004b147f40c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000004dbd4fc535ac27206064b68ffcf827b0a60bab3f000000000000000000000000cf57572261c7c2bcf21ffd220ea7d1a27d40a82700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000841cff79cd0000000000000000000000001015c1ae166c4c39d18a1151b7029bac1530c9aa00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004b147f40c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000c4448b71118c9071bcb9734a0eac55d18a15394900000000000000000000000086a02dd71363c440b21f4c0e5b2ad01ffe1a748200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000841cff79cd00000000000000000000000022ec545357162c342f643bddb2ed4c3fb6b42eb000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004b147f40c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } +} \ No newline at end of file diff --git a/scripts/proposals/AIP6/generateProposalData.ts b/scripts/proposals/AIP6/generateProposalData.ts new file mode 100644 index 00000000..5c89a01b --- /dev/null +++ b/scripts/proposals/AIP6/generateProposalData.ts @@ -0,0 +1,219 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { promises as fs } from "fs"; +import { assertDefined } from "../../security-council-mgmt-deployment/utils"; // todo: move this somewhere else +import { SecurityCouncilManagementDeploymentResult } from "../../security-council-mgmt-deployment/types"; +import { buildProposal } from "../buildProposal"; +import dotenv from "dotenv"; +dotenv.config(); + +const description = ` +AIP 6: Activate Security Council Elections +Category: Constitutional - Process +As part of its governance, the Arbitrum DAO incorporates a Security Council that can take certain emergency and non-emergency actions: +Section 3 of the Constitution describes this council in more detail. +Initial Setup Transparency Report details the current members of the council. +Section 4 on the Constitution outlines an election process that will replace six security council members every six months. +The first election to replace the first cohort of Security Council Members is expected to begin on the 15th September. +Executive summary +This vote relates to the enactment of the Arbitrum Security Council elections. +A smart contract system has been developed to enable on-chain voting. That system is final and has undergone multiple security audits. +This vote is meant to enable the smart contract system that will allow for the elections, as well as temperature check with the DAO. +A successful vote will lead to an on-chain proposal that updates the constitution text with the new sections below, as well as the new Security Council election system being activated within the broader Arbitrum governance architecture. +Implementation Status +The code representing the proposed architecture is final and available for full review in: https://github.com/arbitrumfoundation/governance/tree/949b303a8bc27d6b763d434e1ee15b6c87a765cd. +The work has already been audited by Trail of Bits and the identified issues have been patched, the full audit report is available in the codebase. +A Code4Rena audit competition has also been completed for the election system. + +Background +An overview of the election process as described by the Constitution alongside the process for how to enact a code change to Arbitrum’s smart contract suite: +Constitution of the Arbitrum DAO (esp. sections 3 and 4) - As noted above, this proposal seeks to enact what is already described in the Constitution. +DAO Governance Architecture - The proposed architecture makes use of a number of existing governance components. +An understanding of the following smart contract suites will help the reader evaluate this proposal as it re-uses several components: +Open Zeppelin Governor - The proposed architecture inherits a number of OpenZeppelin contracts. +Gnosis Modules - Each of the security councils is a Gnosis Safe, and updating members of the security council is handled via adding Gnosis modules. + +High-level Election Overview +The Constitution specifies that membership of the Security Council is split into two cohorts. Every 6 months, all positions in a single cohort are put up for election. +The proposed election implementation, which must adhere to the specification laid out in the Constitution, is split into: +Election stages: The process for selecting and voting for nominees. +Update stages: Installing the newly elected cohort into the Arbitrum smart contracts. +The election process and update stages are performed via on-chain smart contracts. A brief overview of each stage includes: +Election Stages +Process for selecting and voting for the new cohort of Security council members. +Nominee selection (7 days). Candidates must gain 0.2% of total votable tokens in order to make it to the next step. +Compliance check by Foundation (14 days). As dictated in the Constitution, the selected nominees must undergo a compliance check to ensure they comply with the legal requirements, service agreements, and additional rules dictated by the Constitution. +Member election (21 days). Votes are cast and the top 6 nominees are selected. +Update Stages +Process to install the newly elected cohort of Security Council members into the Arbitrum smart contracts. +Security Council manager update (0 days). The manager is the source of truth for specifying who are the current council members. It processes the election result and takes note on who will be the new security council members. +L2 timelock + withdrawal + L1 timelock (3 + 7 + 3 days). All actions that directly affect the core Arbitrum contracts must go through a series of timelocks to protect the right for all users to exit. This is a built-in safety mechanism for users who are unhappy with the approved changes. +Individual council update (0 days). Once the updates have passed through the relevant timelocks, the Security Council manager can install the security council members. This requires updating 4 Gnosis Safe smart contracts that are controlled by the Security Council members. + +Election Stages in detail +1. Nominee selection (7 days) +This stage consists of handling election timing, candidate registration, candidate endorsement: +Election creation. Elections can be created by anyone, but only every 6 months. The election alternates between targeting the positions on the two cohorts. Once created, this first stage of the election process lasts for 7 days. +Candidate registration. During these 7 days, any candidate can register, unless they are already a member of the other cohort. Members of the current cohort (the cohort up for election) are allowed to register for re-election. +Endorsing candidates. Delegates can endorse a candidate during this 7 day window. A single delegate can split their vote across multiple candidates. No candidate can accrue more than 0.2% of all votable tokens. +Fallback in case of too few candidates. In the event that fewer than 6 candidates receive a 0.2% endorsement, outgoing members of the cohort up for election will be selected to make up to 6 candidates. +Implementation details +The nominee selection process is implemented by the SecurityCouncilNomineeElectionGovernor contract. +It inherits most of its functionality from the Open Zeppelin Governor contracts and we have extended it with an extra feature: +Custom counting module to allow delegates to endorse multiple candidates. +The governor contract has the following characteristics: +A new proposal is created each election cycle, with an identifier unique to that election cycle. +Candidates can the put themselves forward by calling addContender. +Delegates can call castVoteWithReasonAndParams, supplying custom arguments in the params to indicate which candidate they wish to endorse with what weight. +2. Compliance check by the Foundation(14 days) +The Foundation will be given 14 days to vet the prospective nominees. If they find that a candidate does not meet the compliance check, they can exclude the candidate from progressing to the next stage. Note that grounds for exclusion could include greater than 3 members of a given organization being represented in the nominee set (as described in section 4 of the Constitution). +Implementation details +The foundation can exclude a nominee by: +Calling a custom excludeNominee function on the same SecurityCouncilNomineeElectionGovernor contract. +The Governor smart contract enforces the 2 week time period and the Foundation must exclude nominees by this deadline. +Once the compliance check has completed: +Anyone can call the execute function on the SecurityCouncilNomineeElectionGovernor to proceed to the member election stage. +If there are less than 6 eligible nominees, then the Foundation will consult with outgoing members of the cohort on whether they will continue in this role for another 12 months. Members of the existing cohort may be selected at random to fill the remaining seats. +3. Member election (21 days) +The voting process can begin once a set of compliant candidates have been successfully nominated. +The voting process is designed to encourage voters to cast their vote early. Their voting power will eventually decay if they do not cast their vote within the first 7 days: +0 - 7 days. Votes cast will carry weight 1 per token +7 - 21 days. Votes cast will have their weight linearly decreased based on the amount of time that has passed since the 7 day point. By the 21st day, each token will carry a weight of 0. +Additionally, delegates can cast votes for more than one nominee: +Split voting. delegates can split their tokens across multiple nominees, with 1 token representing 1 vote. +Implementation details +The Security Council member election will take place in a separate SecurityCouncilMemberElectionGovernor contract which will also inherit from Open Zeppelin Governor contracts. +After the 14 day waiting period for the compliance check, anyone can trigger a new member election: +Call the execute function in SecurityCouncilNomineeElectionGovernor to deploy a new election proposal for SecurityCouncilMemberElectionGovernor +The SecurityCouncilMemberElectionGovernor includes: +A custom counting module that allows delegates to split their vote and accounts for the linear decrease in voting weight. +These additional parameters are supplied as the params argument when calling castVoteWithReasonAndParams. +The custom counting module also checks that the nominee being voted is a compliant one by checking against the compliant nominee list in the SecurityCouncilNomineeElectionGovernor. +At the end of the 21 days of election: +Anyone can call the execute function on the SecurityCouncilMemberElectionGovernor contract to initiate the update of top 6 nominees with the most votes into SecurityCouncilManager. + +Update stages in detail +1. Security Council manager update +The security council manager is a contract which contains the canonical list of security council members, and which cohort they are part of. When a member election completes, the manager updates its local list of the current cohorts then forms cross chain messages to propagate those updates to each of the Security Council Gnosis safes. +The manager also provides some additional functionality to allow the security council to: +Remove a member: As described in the Constitution, the council can remove one of its own members. The DAO can also remove a member under special conditions described by the Constitution. +Add a member: After removing a member, the council can add a member +Address rotation: As a practical matter, a council member can rotate one of their own keys. This can only be done with the approval of at least 9/12 council members. +Implementation details +The manager functionality is contained within a custom SecurityCouncilManager smart contract. Since the SecurityCouncilManager is indirectly able to make calls to the standard UpgradeExecutor contracts which have far reaching powers, special care must be take to ensure the manager only makes council member updates. +Calling the UpgradeExecutors on each of the chains requires navigating withdrawals transactions, timelocks and inboxes, the SecurityCouncilManager outsources the calldata creation for these routes to a UpgradeExecRouteBuilder contract. +2. Timelocks and withdrawal +Constitutional DAO proposals all pass through: +L2 timelock (3 days), +L2 → L1 withdrawal (~7 days), +L1 timelock (3 days). +You can read more about these stages in the governance docs. The purpose of these delays is to ensure that users wishing to withdraw their assets before the proposal is executed will have the time to do so. Changing the Security Council members should also provide this guarantee, so after the election has completed and before the Security Councils are updated the update message also goes through these same stages. The update message will use the existing timelocks to enforce these delays. +Implementation details +The existing governance timelock contracts are used as part of this flow. +The SecurityCouncilManager is given the PROPOSER role on the L2 timelock enabling it to create messages that will eventually be received by each UpgradeExecutor. +3. Individual council updates +The new Security Council members need to be installed into 4 Gnosis safes: +Arbitrum One 9 of 12 Emergency Security Council +Arbitrum One 7 of 12 Non-Emergency Security Council +Ethereum 9 of 12 Emergency Security Council +Nova 9 of 12 Emergency Security Council +The old cohort of members will be removed, and the new cohort will replace them. +Implementation details +To do this the existing Upgrade Executor contracts on each chain will be installed as Gnosis Safe modules into the Security Council safes. A custom Governance Action Contract will be used to call the specific OwnerManager addOwnerWithThreshold and removeOwner methods on the Gnosis safes. +Additional affordances +The Constitution also declares some other additional affordances to certain parties +The DAO can vote to remove a member prior to the end of their term, as long as 10% of possible votes are cast in favor and 5/6 of cast votes are in favor. This will be implemented as a governor with correct quorum and proposal passed thresholds. This governor will be given the rights to call removeMember on the SecurityCouncilManager. +The Security Council can remove a member prior to the end of their term, if 9 of 12 members agree. The 9 of 12 council will be given the rights to call removeMember on the SecurityCouncilManager. +The Security Council can add a member once one has been removed, if 9 of 12 members agree and if there are less than 12 members currently on the council. The 9 of 12 council will be given the rights to call addMember on the SecurityCouncilManager. + +Constitution Updates +The proposed implementation mostly satisfies the specification outlined by the Arbitrum Constitution. There are some minor changes that are required to the Constitution’s text to take into account the time it takes to install new candidates and to support compliance procedures set out by the Arbitrum Foundation. +Note, the final wording for how to update the Constitution will be provided in a later revision. We simply want to notify the requirement that the text needs to be changed. At this stage, our request for feedback is focused on the implementation details of the smart contract suite. +Update timeline for the election +The Section 4 of the Constitution contains the text: +From T until T+7 days: Any DAO member may declare their candidacy for the Security Council; provided that a current Security Council member in one cohort may not be a candidate for a seat in the other cohort. To the extent that there are more than six candidates, each eligible candidate must be supported by pledged votes representing at least 0.2% of all Votable Tokens. In the event that fewer than six candidates are supported by pledged votes representing at least 0.2% of all Votable Tokens, the current Security Council members whose seats are up for election may become candidates (as randomly selected out of their Cohort) until there are 6 candidates. +From T+7 days until T+28 days: Each DAO member or delegate may vote for any declared candidate. Each token may be cast for one candidate. Votes cast before T+14 days will have 100% weight. Votes cast between T+14 days and T+28 days will have weight based on the time of casting, decreasing linearly with time, with 100% weight at T+14 days, decreasing linearly to 0% weight at T+28 days. +At T+28 days: The 6 candidates who have received the most votes are elected and immediately join the Council, replacing the Cohort that was up for re-election. +We need to make three changes to the Arbitrum Constitution: +New timeline. A dedicated compliance process must be included between the nominee selection and member election phases. This will shift the timeline of events and the total election will now last at least 42 days alongside additional time to install the newly elected Security Council members via the on-chain governance smart contracts. +Less than 6 eligible nominees. The Arbitrum Foundation has the authority to add new nominees during the Compliance stage if there are less than 6 eligible nominees. +Installation time. We need to remove the phrase ‘immediately join the Council’ to take into account the on-chain governance process for installing the newly elected candidates. For example, the various time locks to protect a user’s right to exit Arbitrum during the upgrade and the time it takes to send an L2 -> L1 message. +With the above in mind, we propose an update to Section 4 of the Constitution with the following text: +Nominee selection (T until T+7 days): Any DAO member may declare their candidacy for the Security Council; provided that a current Security Council member in one cohort may not be a candidate for a seat in the other cohort. To the extent that there are more than six candidates, each eligible candidate must be supported by pledged votes representing at least 0.2% of all Votable Tokens. +Compliance process (T+7 until T+21 days): All candidates will cooperate with The Arbitrum Foundation and complete the compliance process. The Arbitrum Foundation is responsible for removing any candidates that fail the compliance process. In the event that fewer than six candidates are supported by pledged votes representing at least 0.2% of all Votable Tokens, the current Security Council members whose seats are up for election may become candidates (as randomly selected out of their Cohort) until there are 6 candidates. +Member election (T+21 until T+42 days): Each DAO member or delegate may vote for any declared candidate. Each token may be cast for one candidate. Votes cast before T+28 days will have 100% weight. Votes cast between T+28 days and T+42 days will have weight based on the time of casting, decreasing linearly with time, with 100% weight at T+28 days, decreasing linearly to 0% weight at T+42 days. +At T+42 days: The process for replacing the cohort of Security Council members with the 6 candidates who received the most votes will be activated. The installation process must be executed via the on-chain governance smart contracts and it may take several days until the new Security Council members are installed. +Compliance checks by the foundation. +The Constitution contains the text: +“Prior to the next Security Council election, The Arbitrum Foundation shall establish and set forth more detailed procedures and guidelines regarding the election process for the Security Council, which may include, but aren’t limited to, a candidate intake process in order to comply with Cayman Islands laws, a standard template for candidates to complete for purposes of their public nominations and other processes to ensure an orderly, fair and transparent election.” +The text gives an affordance to the Foundation to conduct a compliance check on the potential nominees. We propose to make this check an explicit stage of 14 days between Nominee Selection and Member Election to allow the Foundation to conduct these checks. Details of compliance checks will be provided by the Arbitrum Foundation at a later date. +We propose to update the Constitution with the following text: +The Arbitrum Foundation is allocated 14 days for the Compliance process and it should be executed between the Nominee selection and Member election. The Arbitrum Foundation has flexibility to update its compliance policy for every new election. This is required to allow The Arbitrum Foundation to comply with Cayman Island laws. Furthermore, The Arbitrum Foundation maintains the right to issue new procedures and guidelines for off-chain components of the Security Council election. All efforts should be made by The Arbitrum Foundation to ensure an orderly, fair, and transparent election. +Security Council cannot re-appoint a member who was removed by the DAO. +The Constitution contains the text: +The seats of Security Council members who have been removed prior to the end of their respective terms shall remain unfilled until the next election that such seats are up for appointment, unless otherwise replaced prior to such next election by a vote of at least 9 of the Security Council members, in which case such seat shall be up for appointment at the next such election. +The text focuses on how to replace a security council member who was removed by a vote from the DAO or 9/12 of the current security council members. We plan to update the Constitution to remove an edge-case that allows the Security Council to re-appoint a member who was removed by a DAO vote. +We propose to update the Constitution with the following text: +The seats of Security Council members who have been removed prior to the end of their respective terms shall remain unfilled until the next election that such seats are up for appointment, unless otherwise replaced prior to such next election by a vote of at least 9 of the Security Council members, in which case such seat shall be up for appointment at the next such election. The Security Council may not re-appoint a member removed and they must be re-elected via the election voting system. +Final update to the Constitution text. +For completeness, the amended text for Section 4 of the Arbitrum Constitution, including the changes from AIP-4: +The Security Council has 12 members, who are divided into two Cohorts of 6 members. +The initial Security Council Cohorts were determined by randomly splitting the 12 members into two 6-member cohorts - 6 members in the 'First Cohort' and 6 members in the 'Second Cohort'. The members of the initial Security Council Cohorts are detailed in a transparency report here. +The first Security Council election is scheduled to begin on the 15th September 2023 or the earliest possible date. The election can only begin upon the availability of an on-chain election process that is approved and installed by the Arbitrum DAO. This first election replaces the 'First Cohort'. The next election replaces the 'Second Cohort' and so forth. +The date chosen for the first election will form the basis for all future elections. Every election should begin 6 months after the previous election has started and it will replace its respective cohort of 6 members. +All Security Council members are expected to serve their term until the election is complete and the new Security Council members are installed. +The following timeline governs an election that starts at time T: +Nominee selection (T until T+7 days): Any DAO member may declare their candidacy for the Security Council; provided that a current Security Council member in one cohort may not be a candidate for a seat in the other cohort. To the extent that there are more than six candidates, each eligible candidate must be supported by pledged votes representing at least 0.2% of all Votable Tokens. +Compliance process (T+7 until T+21 days): All candidates will cooperate with the Arbitrum Foundation and complete the compliance process. The Arbitrum Foundation is responsible for removing any candidates that fail the compliance process. In the event that fewer than six candidates are supported by pledged votes representing at least 0.2% of all Votable Tokens, the current Security Council members whose seats are up for election may become candidates (as randomly selected out of their Cohort) until there are 6 candidates. +Member election (T+21 until T+42 days): Each DAO member or delegate may vote for any declared candidate. Each token may be cast for one candidate. Votes cast before T+14 days will have 100% weight. Votes cast between T+21 days and T+35 days will have weight based on the time of casting, decreasing linearly with time, with 100% weight at T+21 days, decreasing linearly to 0% weight at T+42 days. +At T+42 days: The process for replacing the cohort of security council members with the 6 candidates who received the most votes will be activated. The installation process must be executed via the on-chain governance smart contracts and it may take several days until the new security council members are installed. +The Arbitrum Foundation is allocated 14 days for the Compliance process and it should be executed between the Nominee selection and Member election. The Arbitrum Foundation has flexibility to update its compliance policy for every new election. This is required to allow The Arbitrum Foundation to comply with Cayman Island laws. Furthermore, The Arbitrum Foundation maintains the right to issue new procedures and guidelines for off-chain components of the Security Council election. All efforts should be made by The Arbitrum Foundation to ensure an orderly, fair, and transparent election. +As a matter of best practice for maintaining an independent Security Council, no single organization should be overly represented in the Security Council. In particular, there should not be more than 3 candidates associated with a single entity or group of entities being elected to the Security Council, thereby ensuring that there will be no single entity or group of entities able to control or even veto a Security Council vote. +Furthermore, no candidate with conflicts of interest that would prevent them from acting in the best interests of the ArbitrumDAO, Governed Chains and/or The Arbitrum Foundation should be elected to the Security Council. Potential conflicts of interest could be, but are not limited to, affiliations with direct Arbitrum competitors, proven histories of exploiting projects and others. +The DAO may approve and implement a Constitutional AIP to change the rules governing future Security Council elections, but the AIP process may not be used to intervene in an ongoing election. +Security Council members may only be removed prior to the end of their terms under two conditions: +At least 10% of all Votable Tokens have casted votes "in favor" of removal and at least 5/6 (83.33%) of all casted votes are "in favor" of removal; or +At least 9 of the Security Council members vote in favor of removal. +The seats of Security Council members who have been removed prior to the end of their respective terms shall remain unfilled until the next election that such seats are up for appointment, unless otherwise replaced prior to such next election by a vote of at least 9 of the Security Council members, in which case such seat shall be up for appointment at the next such election. The Security Council may not re-appoint a removed member and they can only be re-elected via the election voting system. +`; + +async function main() { + const provider = new JsonRpcProvider(assertDefined(process.env.ARB_URL, "ARB_URL is undefined")); + + const chainId = (await provider.getNetwork()).chainId; + + let scmDeploymentPath: string; + if (chainId === 42161) { + scmDeploymentPath = "files/mainnet/scmDeployment.json"; + } else if (chainId === 421613) { + scmDeploymentPath = "files/goerli/scmDeployment.json"; + } else { + throw new Error(`Unknown chainId ${chainId}`); + } + + const scmDeployment = JSON.parse( + (await fs.readFile(scmDeploymentPath)).toString() + ) as SecurityCouncilManagementDeploymentResult; + const actions = scmDeployment.activationActionContracts; + + const chainIds = Object.keys(actions).map((k) => parseInt(k)); + const actionAddresses = chainIds.map((chainId) => actions[chainId]); + + const proposal = await buildProposal( + description, + provider, + scmDeployment.upgradeExecRouteBuilder, + chainIds, + actionAddresses + ); + + const path = `${__dirname}/data/${chainId}-AIP6-data.json`; + await fs.mkdir(`${__dirname}/data`, { recursive: true }); + await fs.writeFile(path, JSON.stringify(proposal, null, 2)); + console.log("Wrote proposal data to", path); + console.log(proposal); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/proposals/buildProposal.ts b/scripts/proposals/buildProposal.ts new file mode 100644 index 00000000..ed1bab7f --- /dev/null +++ b/scripts/proposals/buildProposal.ts @@ -0,0 +1,98 @@ +import { Provider } from "@ethersproject/providers"; +import { CoreGovProposal } from "./coreGovProposalInterface"; +import { ArbSys__factory, UpgradeExecRouteBuilder__factory } from "../../typechain-types"; +import { BigNumberish, BytesLike, ethers } from "ethers"; + +async function _buildProposal( + description: string, + provider: Provider, + routeBuilderAddress: string, + actionChainIds: number[], + actionAddresses: string[], + actionValues: BigNumberish[] | undefined, + actionDatas: BytesLike[] | undefined, + predecessor: BytesLike | undefined, + timelockSalt: BytesLike | undefined +): Promise { + const routeBuilder = UpgradeExecRouteBuilder__factory.connect( + routeBuilderAddress, + provider + ); + + let calldata; + + if (actionValues && actionDatas && predecessor && timelockSalt) { + [, calldata] = await routeBuilder.createActionRouteData( + actionChainIds, + actionAddresses, + actionValues, + actionDatas, + predecessor, + timelockSalt + ); + } + else { + [, calldata] = await routeBuilder.createActionRouteDataWithDefaults( + actionChainIds, + actionAddresses, + timelockSalt || ethers.constants.HashZero, + ); + } + + const decoded = ArbSys__factory.createInterface().decodeFunctionData("sendTxToL1", calldata); + + return { + actionChainIds, + actionAddresses, + description, + arbSysSendTxToL1Args: { + l1Timelock: decoded[0], + calldata: decoded[1], + }, + }; +} + +export function buildProposalCustom( + description: string, + provider: Provider, + routeBuilderAddress: string, + actionChainIds: number[], + actionAddresses: string[], + actionValues: BigNumberish[], + actionDatas: BytesLike[], + predecessor: BytesLike, + timelockSalt: BytesLike +): Promise { + return _buildProposal( + description, + provider, + routeBuilderAddress, + actionChainIds, + actionAddresses, + actionValues, + actionDatas, + predecessor, + timelockSalt + ); +} + +export function buildProposal( + description: string, + provider: Provider, + routeBuilderAddress: string, + actionChainIds: number[], + actionAddresses: string[], + timelockSalt: BytesLike | undefined = undefined +): Promise { + return _buildProposal( + description, + provider, + routeBuilderAddress, + actionChainIds, + actionAddresses, + undefined, + undefined, + undefined, + timelockSalt + ) +} diff --git a/scripts/proposals/coreGovProposalInterface.ts b/scripts/proposals/coreGovProposalInterface.ts index eea43317..7046473b 100644 --- a/scripts/proposals/coreGovProposalInterface.ts +++ b/scripts/proposals/coreGovProposalInterface.ts @@ -1,4 +1,4 @@ -export interface CoreGovPropposal { +export interface SingleActionCoreGovProposal { actionChainID: number; actionAddress: string; description: string; @@ -6,4 +6,14 @@ export interface CoreGovPropposal { l1Timelock: string; calldata: string } +} + +export interface CoreGovProposal { + actionChainIds: number[]; + actionAddresses: string[]; + description: string; + arbSysSendTxToL1Args: { + l1Timelock: string; + calldata: string; + }, } \ No newline at end of file diff --git a/scripts/security-council-mgmt-deployment/configs/arbgoerli.ts b/scripts/security-council-mgmt-deployment/configs/arbgoerli.ts new file mode 100644 index 00000000..3246d515 --- /dev/null +++ b/scripts/security-council-mgmt-deployment/configs/arbgoerli.ts @@ -0,0 +1,62 @@ +import { ethers } from "ethers"; +import { DeploymentConfig } from "../types"; +import { assertDefined, blocks, readDeployedContracts } from "../utils"; + +const deployedContracts = readDeployedContracts("./files/goerli/deployedContracts.json"); + +const config: DeploymentConfig = { + ...deployedContracts, + emergencySignerThreshold: 9, + nonEmergencySignerThreshold: 7, + removalGovVotingDelay: blocks(3, 'days'), + removalGovVotingPeriod: blocks(14, 'days'), + removalGovQuorumNumerator: 1000, // 10% + removalGovProposalThreshold: ethers.utils.parseEther("1000000"), + removalGovVoteSuccessNumerator: 8333, // 83.33% + removalGovMinPeriodAfterQuorum: blocks(2, 'days'), + removalProposalExpirationBlocks: blocks(14, 'days'), + firstNominationStartDate: { + year: 2023, + month: 9, + day: 15, + hour: 12, // todo + }, + nomineeVettingDuration: blocks(14, 'days'), + nomineeVetter: "0x000000000000000000000000000000000000dead", // todo + nomineeQuorumNumerator: 20, // 0.2% + nomineeVotingPeriod: blocks(7, 'days'), + memberVotingPeriod: blocks(21, 'days'), + fullWeightDuration: blocks(7, 'days'), + firstCohort: [ + "0x526C0DA9970E7331d171f86AeD28FAFB5D8A49EF", + "0xf8e1492255d9428c2Fc20A98A1DeB1215C8ffEfd", + "0x0E5011001cF9c89b0259BC3B050785067495eBf5", + "0x8688515028955734350067695939423222009623", + "0x6e77068823f9D0fE98F80764c21Ec294e4d96AdB", + "0x8e6247239CBeB3Eaf9d9a691D01A67e2A9Fea3C5", + ], + secondCohort: [ + "0x566a07C3c932aE6AF74d77c29e5c30D8B1853710", + "0x5280406912EB8Ec677Df66C326BE48f938DC2e44", + "0x0275b3D54a5dDbf8205A75984796eFE8b7357Bae", + "0x5A1FD562271aAC2Dadb51BAAb7760b949D9D81dF", + "0xf6B6F07862A02C85628B3A9688beae07fEA9C863", + "0x475816ca2a31D601B4e336f5c2418A67978aBf09", + ], + govChain: { + chainID: 421613, + rpcUrl: assertDefined(process.env.ARB_URL, "ARB_URL is undefined"), + privateKey: assertDefined(process.env.ARB_KEY, "ARB_KEY is undefined"), + prevEmergencySecurityCouncil: "0x1c08879f8d07b1ff6d88ae0d1b4df6f1fecaff6c", + prevNonEmergencySecurityCouncil: "0x62Df147e5A7811F3b9329dA618A020358F5eEb5d", + }, + hostChain: { + chainID: 5, + rpcUrl: assertDefined(process.env.ETH_URL, "ETH_URL is undefined"), + privateKey: assertDefined(process.env.ETH_KEY, "ETH_KEY is undefined"), + prevEmergencySecurityCouncil: "0x073b9C5C1bb56cc17ca23f33D4dAd36C4AE80AeE", + }, + governedChains: [] +}; + +export default config; \ No newline at end of file diff --git a/scripts/security-council-mgmt-deployment/configs/mainnet.ts b/scripts/security-council-mgmt-deployment/configs/mainnet.ts new file mode 100644 index 00000000..45bb7015 --- /dev/null +++ b/scripts/security-council-mgmt-deployment/configs/mainnet.ts @@ -0,0 +1,73 @@ +import { ethers } from "ethers"; +import { DeploymentConfig } from "../types"; +import { assertDefined, blocks, readDeployedContracts } from "../utils"; +import dotenv from "dotenv"; +dotenv.config(); + +const deployedContracts = readDeployedContracts("./files/mainnet/deployedContracts.json"); + +const config: DeploymentConfig = { + ...deployedContracts, + emergencySignerThreshold: 9, + nonEmergencySignerThreshold: 7, + removalGovVotingDelay: blocks(3, 'days'), + removalGovVotingPeriod: blocks(14, 'days'), + removalGovQuorumNumerator: 1000, // 10% + removalGovProposalThreshold: ethers.utils.parseEther("1000000"), + removalGovVoteSuccessNumerator: 8333, // 83.33% + removalGovMinPeriodAfterQuorum: blocks(2, 'days'), + removalProposalExpirationBlocks: blocks(14, 'days'), + firstNominationStartDate: { + year: 2023, + month: 9, + day: 15, + hour: 12, + }, + nomineeVettingDuration: blocks(14, 'days'), + nomineeVetter: "0xc610984d9C96a7CE54Bcd335CEee9b0e3874380C", + nomineeQuorumNumerator: 20, // 0.2% + nomineeVotingPeriod: blocks(7, 'days'), + memberVotingPeriod: blocks(21, 'days'), + fullWeightDuration: blocks(7, 'days'), + firstCohort: [ + "0x526C0DA9970E7331d171f86AeD28FAFB5D8A49EF", + "0xf8e1492255d9428c2Fc20A98A1DeB1215C8ffEfd", + "0x0E5011001cF9c89b0259BC3B050785067495eBf5", + "0x8688515028955734350067695939423222009623", + "0x88910996671162953E89DdcE5C8137f9077da217", + "0x8e6247239CBeB3Eaf9d9a691D01A67e2A9Fea3C5", + ], + secondCohort: [ + "0x566a07C3c932aE6AF74d77c29e5c30D8B1853710", + "0x5280406912EB8Ec677Df66C326BE48f938DC2e44", + "0x0275b3D54a5dDbf8205A75984796eFE8b7357Bae", + "0x5A1FD562271aAC2Dadb51BAAb7760b949D9D81dF", + "0xf6B6F07862A02C85628B3A9688beae07fEA9C863", + "0x475816ca2a31D601B4e336f5c2418A67978aBf09", + ], + govChain: { + chainID: 42161, + rpcUrl: assertDefined(process.env.ARB_URL, "ARB_URL is undefined"), + privateKey: assertDefined(process.env.ARB_KEY, "ARB_KEY is undefined"), + prevEmergencySecurityCouncil: "0x3568A44b3E72F5B17a0E14E53fdB7366B3B7Ad13", + prevNonEmergencySecurityCouncil: "0x895c9fc6bcf06e553b54A9fE11D948D67a9B76FA", + }, + hostChain: { + chainID: 1, + rpcUrl: assertDefined(process.env.ETH_URL, "ETH_URL is undefined"), + privateKey: assertDefined(process.env.ETH_KEY, "ETH_KEY is undefined"), + prevEmergencySecurityCouncil: "0x3666a60ff589873ced457a9a8a0aA6F83D708767", + }, + governedChains: [ + { + chainID: 42170, + rpcUrl: assertDefined(process.env.NOVA_URL, "NOVA_URL is undefined"), + privateKey: assertDefined(process.env.NOVA_KEY, "NOVA_KEY is undefined"), + // @ts-ignore + upExecLocation: deployedContracts.novaUpgradeExecutorProxy, + prevEmergencySecurityCouncil: "0x3cA27a792C64a3a81417499AA53786A41812B2cd", + } + ] +}; + +export default config; diff --git a/scripts/security-council-mgmt-deployment/deployContracts.ts b/scripts/security-council-mgmt-deployment/deployContracts.ts new file mode 100644 index 00000000..5ad83cbe --- /dev/null +++ b/scripts/security-council-mgmt-deployment/deployContracts.ts @@ -0,0 +1,390 @@ +import { + L2SecurityCouncilMgmtFactory__factory, + SecurityCouncilMemberSyncAction__factory, + L1ArbitrumTimelock__factory, + KeyValueStore__factory, + SecurityCouncilNomineeElectionGovernor__factory, + SecurityCouncilMemberElectionGovernor__factory, + SecurityCouncilManager__factory, + SecurityCouncilMemberRemovalGovernor__factory, + GovernanceChainSCMgmtActivationAction__factory, + L1SCMgmtActivationAction__factory, + NonGovernanceChainSCMgmtActivationAction__factory, +} from "../../typechain-types"; +import { + DeployParamsStruct, + ContractsDeployedEventObject, + ContractImplementationsStruct, +} from "../../typechain-types/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory"; +import { GnosisSafeL2__factory } from "../../types/ethers-contracts/factories/GnosisSafeL2__factory"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { getL2Network } from "@arbitrum/sdk"; +import { BigNumber, Wallet, constants, ethers } from "ethers"; +import { DeploymentConfig, ChainConfig, SecurityCouncilManagementDeploymentResult, GovernedChainConfig } from "./types"; +import { randomNonce } from "./utils"; +import { SafeFactory, EthersAdapter } from '@safe-global/protocol-kit' + +function getSigner(chain: ChainConfig): Wallet { + return new Wallet(chain.privateKey, new JsonRpcProvider(chain.rpcUrl)); +} + +// deploy gnosis safe with a module and set of owners +async function deployGnosisSafe( + owners: string[], + threshold: number, + module: string, + nonce: number, + signer: Wallet +) { + const ethAdapter = new EthersAdapter({ ethers, signerOrProvider: signer }) + const safeFactory = await SafeFactory.create({ ethAdapter }) + + const safeSdk = await safeFactory.deploySafe({ + safeAccountConfig: { + owners: [...owners, signer.address], + threshold: 1 + }, + saltNonce: nonce.toString() + }); + + let addDeployerAsModuleTx = await safeSdk.createTransaction({ + safeTransactionData: { + to: await safeSdk.getAddress(), + value: "0", + data: (await safeSdk.createEnableModuleTx(signer.address)).data.data, + } + }); + addDeployerAsModuleTx = await safeSdk.signTransaction(addDeployerAsModuleTx); + + const addDeployerAsModuleTxResult = await safeSdk.executeTransaction(addDeployerAsModuleTx); + await addDeployerAsModuleTxResult.transactionResponse?.wait(); + + // make sure the deployer is a module + if (!await safeSdk.isModuleEnabled(signer.address)) { + throw new Error("Deployer is not a module"); + } + + // remove the deployer as an owner and set threshold + let removeDeployerAsOwnerTx = await safeSdk.createTransaction({ + safeTransactionData: { + to: await safeSdk.getAddress(), + value: "0", + data: (await safeSdk.createRemoveOwnerTx({ ownerAddress: signer.address, threshold })).data.data, + } + }); + removeDeployerAsOwnerTx = await safeSdk.signTransaction(removeDeployerAsOwnerTx); + + const removeDeployerAsOwnerTxResult = await safeSdk.executeTransaction(removeDeployerAsOwnerTx); + await removeDeployerAsOwnerTxResult.transactionResponse?.wait(); + + // make sure the deployer is not an owner + if (await safeSdk.isOwner(signer.address)) { + throw new Error("Deployer is still an owner"); + } + + // make sure the threshold is correct + if (await safeSdk.getThreshold() !== threshold) { + throw new Error("Threshold is not correct"); + } + + const safe = GnosisSafeL2__factory.connect(await safeSdk.getAddress(), signer); + + // add the intended module + const addModuleTx = await safe.execTransactionFromModule( + safe.address, + 0, + (await safeSdk.createEnableModuleTx(module)).data.data, + 0 // operation = call + ); + await addModuleTx.wait(); + + // make sure the module is enabled + if (!await safeSdk.isModuleEnabled(module)) { + throw new Error("Module is not enabled"); + } + + // remove the deployer as a module + const removeDeployerAsModuleTx = await safe.execTransactionFromModule( + safe.address, + 0, + (await safeSdk.createDisableModuleTx(signer.address)).data.data, + 0 // operation = call + ); + await removeDeployerAsModuleTx.wait(); + + // make sure the deployer is not a module + if (await safeSdk.isModuleEnabled(signer.address)) { + throw new Error("Deployer is still a module"); + } + + return safe.address; +} + +async function checkChainConfigs(chains: ChainConfig[]) { + for (const chain of chains) { + const signer = getSigner(chain); + const address = await signer.getAddress(); + const balance = await signer.getBalance(); + if (balance.eq(constants.Zero)) throw new Error(`Signer ${address} on ${chain.chainID} not funded`); + if (chain.chainID !== (await signer.getChainId())) throw new Error(`RPC for ${address} on chain ${chain.chainID} returned wrong chain id`); + } +} + +export async function deployContracts(config: DeploymentConfig): Promise { + // steps: + // 1. deploy action contracts and key value stores + // 2. deploy gnosis safes + // 3. deploy contract implementations + // 4. build deploy params for factory.deploy() + // 5. deploy sc mgmt factory + // 6. call factory.deploy() + + const allChains = [ + config.hostChain, + config.govChain, + ...config.governedChains + ]; + + console.log("Checking chain configs..."); + await checkChainConfigs(allChains); + + const govChainSigner = getSigner(config.govChain); + const hostChainSigner = getSigner(config.hostChain); + + // 1. deploy action contracts and key value stores + console.log("Deploying action contracts and key value stores..."); + + const keyValueStores: SecurityCouncilManagementDeploymentResult["keyValueStores"] = {}; + const securityCouncilMemberSyncActions: SecurityCouncilManagementDeploymentResult["securityCouncilMemberSyncActions"] = {}; + + for (const chain of allChains) { + const signer = getSigner(chain); + + console.log(`\tDeploying KeyValueStore to chain ${chain.chainID}...`); + const kvStore = await new KeyValueStore__factory(signer).deploy(); + await kvStore.deployed(); + + console.log(`\tDeploying SecurityCouncilMemberSyncAction to chain ${chain.chainID}...`); + + const action = await new SecurityCouncilMemberSyncAction__factory(signer).deploy(kvStore.address); + await action.deployed(); + + keyValueStores[chain.chainID] = kvStore.address; + securityCouncilMemberSyncActions[chain.chainID] = action.address; + } + + // 2. deploy gnosis safes + console.log("Deploying gnosis safes..."); + + const owners = await Promise.all([...config.firstCohort, ...config.secondCohort]); + const emergencyGnosisSafes: SecurityCouncilManagementDeploymentResult["emergencyGnosisSafes"] = {}; + + console.log(`\tDeploying non-emergency Gnosis Safe to chain ${config.govChain.chainID}...`); + + const nonEmergencyGnosisSafe = await deployGnosisSafe( + owners, + config.nonEmergencySignerThreshold, + config.l2Executor, + randomNonce(), + govChainSigner + ); + + for (const chain of allChains) { + const signer = getSigner(chain); + + let executor; + switch (chain.chainID) { + case config.hostChain.chainID: + executor = config.l1Executor; + break; + case config.govChain.chainID: + executor = config.l2Executor; + break; + default: + executor = (chain as GovernedChainConfig).upExecLocation; + } + + console.log(`\tDeploying emergency Gnosis Safe to chain ${chain.chainID}...`); + const safe = await deployGnosisSafe( + owners, + config.emergencySignerThreshold, + executor, + randomNonce(), + signer + ); + + emergencyGnosisSafes[chain.chainID] = safe; + } + + // 3. deploy contract implementations + console.log("Deploying contract implementations..."); + + console.log(`\tDeploying SecurityCouncilNomineeElectionGovernor to chain ${config.govChain.chainID}...`); + const nomineeElectionGovernor = await new SecurityCouncilNomineeElectionGovernor__factory(govChainSigner).deploy(); + await nomineeElectionGovernor.deployed(); + + console.log(`\tDeploying SecurityCouncilMemberElectionGovernor to chain ${config.govChain.chainID}...`); + const memberElectionGovernor = await new SecurityCouncilMemberElectionGovernor__factory(govChainSigner).deploy(); + await memberElectionGovernor.deployed(); + + console.log(`\tDeploying SecurityCouncilManager to chain ${config.govChain.chainID}...`); + const securityCouncilManager = await new SecurityCouncilManager__factory(govChainSigner).deploy(); + await securityCouncilManager.deployed(); + + console.log(`\tDeploying SecurityCouncilMemberRemovalGovernor to chain ${config.govChain.chainID}...`); + const securityCouncilMemberRemoverGov = await new SecurityCouncilMemberRemovalGovernor__factory(govChainSigner).deploy(); + await securityCouncilMemberRemoverGov.deployed(); + + // finished object + const contractImplementations: ContractImplementationsStruct = { + nomineeElectionGovernor: nomineeElectionGovernor.address, + memberElectionGovernor: memberElectionGovernor.address, + securityCouncilManager: securityCouncilManager.address, + securityCouncilMemberRemoverGov: securityCouncilMemberRemoverGov.address, + }; + + // 4. build deploy params for factory.deploy() + console.log("Building deploy params for factory.deploy()..."); + const deployParams: DeployParamsStruct = { + ...config, + upgradeExecutors: [ + { + // (L1) host chain executor + chainId: config.hostChain.chainID, + location: { + inbox: constants.AddressZero, + upgradeExecutor: config.l1Executor, + }, + }, + { + // (L2) gov chain executor + chainId: config.govChain.chainID, + location: { + inbox: (await getL2Network(config.govChain.chainID)).ethBridge.inbox, + upgradeExecutor: config.l2Executor, + }, + }, + // (L2) governed chain executors + ...await Promise.all(config.governedChains.map(async (chain) => { + return { + chainId: chain.chainID, + location: { + inbox: (await getL2Network(chain.chainID)).ethBridge.inbox, + upgradeExecutor: chain.upExecLocation, + } + } + })) + ], + govChainEmergencySecurityCouncil: emergencyGnosisSafes[config.govChain.chainID], + l1ArbitrumTimelock: config.l1Timelock, + l2CoreGovTimelock: config.l2CoreTimelock, + govChainProxyAdmin: config.l2ProxyAdmin, + l2UpgradeExecutor: config.l2Executor, + arbToken: config.l2Token, + l1TimelockMinDelay: L1ArbitrumTimelock__factory.connect(config.l1Timelock, hostChainSigner).getMinDelay(), + securityCouncils: [ + // emergency councils + ...allChains.map((chain) => { + return { + securityCouncil: emergencyGnosisSafes[chain.chainID], + chainId: chain.chainID, + updateAction: securityCouncilMemberSyncActions[chain.chainID], + } + }), + // non-emergency council + { + securityCouncil: nonEmergencyGnosisSafe, + chainId: config.govChain.chainID, + updateAction: securityCouncilMemberSyncActions[config.govChain.chainID] + } + ] + }; + // some additional config checks + if(BigNumber.from(await config.firstNominationStartDate.day).toNumber() > 28 + && BigNumber.from(await config.firstNominationStartDate.month).toNumber() === 8) { + // Next election would be on undefined date in february + throw new Error("Invalid date for first nomination start date. Please choose a date that is not the 29th, 30th, or 31st of August.") + } + if(new Set(await Promise.all((config.firstCohort).concat(config.secondCohort))).size !== 12) { + throw new Error("Invalid cohort. Please ensure that all addresses are unique.") + } + + // 5. deploy sc mgmt factory + console.log("Deploying sc mgmt factory..."); + const l2SecurityCouncilMgmtFactory = await new L2SecurityCouncilMgmtFactory__factory(govChainSigner).deploy(); + await l2SecurityCouncilMgmtFactory.deployed(); + + // 6. call factory.deploy() + console.log("Calling factory.deploy()..."); + const deployTx = await l2SecurityCouncilMgmtFactory.connect(govChainSigner).deploy(deployParams, contractImplementations); + const deployReceipt = await deployTx.wait(); + + const deployEvent = deployReceipt.events?.filter( + (e) => e.topics[0] === l2SecurityCouncilMgmtFactory.interface.getEventTopic("ContractsDeployed") + )[0].args as unknown as ContractsDeployedEventObject; + + if (!deployEvent) throw new Error("No contracts deployed event"); + + // 7. deploy activation action contracts + console.log("Deploying activation action contracts..."); + const activationActionContracts: SecurityCouncilManagementDeploymentResult["activationActionContracts"] = {}; + + // 7a. deploy activation action contract to gov chain + console.log(`\tDeploying activation action contract to governance chain ${config.govChain.chainID}...`); + const govChainActivationAction = await new GovernanceChainSCMgmtActivationAction__factory(govChainSigner).deploy( + emergencyGnosisSafes[config.govChain.chainID], + nonEmergencyGnosisSafe, + config.govChain.prevEmergencySecurityCouncil, + config.govChain.prevNonEmergencySecurityCouncil, + config.emergencySignerThreshold, + config.nonEmergencySignerThreshold, + deployEvent.deployedContracts.securityCouncilManager, + config.l2AddressRegistry + ); + await govChainActivationAction.deployed(); + activationActionContracts[config.govChain.chainID] = govChainActivationAction.address; + + // 7b. deploy activation action contract to host chain + console.log(`\tDeploying activation action contract to host chain ${config.hostChain.chainID}...`); + const hostChainActivationAction = await new L1SCMgmtActivationAction__factory(hostChainSigner).deploy( + emergencyGnosisSafes[config.hostChain.chainID], + config.hostChain.prevEmergencySecurityCouncil, + config.emergencySignerThreshold, + config.l1Executor, + config.l1Timelock + ); + await hostChainActivationAction.deployed(); + activationActionContracts[config.hostChain.chainID] = hostChainActivationAction.address; + + // 7c. deploy activation action contract to governed chains + for (const chain of config.governedChains) { + console.log(`\tDeploying activation action contract to governed chain ${chain.chainID}...`); + const signer = getSigner(chain); + const activationAction = await new NonGovernanceChainSCMgmtActivationAction__factory(signer).deploy( + emergencyGnosisSafes[chain.chainID], + chain.prevEmergencySecurityCouncil, + config.emergencySignerThreshold, + chain.upExecLocation + ); + await activationAction.deployed(); + activationActionContracts[chain.chainID] = activationAction.address; + } + + return { + activationActionContracts, + keyValueStores, + securityCouncilMemberSyncActions, + emergencyGnosisSafes, + nonEmergencyGnosisSafe, + nomineeElectionGovernor: deployEvent.deployedContracts.nomineeElectionGovernor, + nomineeElectionGovernorLogic: await contractImplementations.nomineeElectionGovernor, + memberElectionGovernor: deployEvent.deployedContracts.memberElectionGovernor, + memberElectionGovernorLogic: await contractImplementations.memberElectionGovernor, + securityCouncilManager: deployEvent.deployedContracts.securityCouncilManager, + securityCouncilManagerLogic: await contractImplementations.securityCouncilManager, + securityCouncilMemberRemoverGov: deployEvent.deployedContracts.securityCouncilMemberRemoverGov, + securityCouncilMemberRemoverGovLogic: await contractImplementations.securityCouncilMemberRemoverGov, + upgradeExecRouteBuilder: deployEvent.deployedContracts.upgradeExecRouteBuilder, + l2SecurityCouncilMgmtFactory: l2SecurityCouncilMgmtFactory.address, + } +} diff --git a/scripts/security-council-mgmt-deployment/deployDummyElectionsContracts.ts b/scripts/security-council-mgmt-deployment/deployDummyElectionsContracts.ts new file mode 100644 index 00000000..64a7c71b --- /dev/null +++ b/scripts/security-council-mgmt-deployment/deployDummyElectionsContracts.ts @@ -0,0 +1,161 @@ +// not sure how to handle config, so just going to hardcode in an object for now + +import { Wallet, ethers } from "ethers"; +import { L2ArbitrumToken__factory, L2SecurityCouncilMgmtFactory__factory, SecurityCouncilManager__factory, SecurityCouncilMemberElectionGovernor__factory, SecurityCouncilMemberRemovalGovernor__factory, SecurityCouncilNomineeElectionGovernor__factory } from "../../typechain-types"; +import { DeployParamsStruct } from "../../typechain-types/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory"; +import { TransparentUpgradeableProxy__factory } from "@arbitrum/sdk/dist/lib/abi/factories/TransparentUpgradeableProxy__factory"; +import { getNamedObjectItems } from "./utils"; + +// env vars: +// RPC_URL (optional, defaults to http://localhost:8545) +// PRIVATE_KEY + +type DeployParamsPartial = Omit< + DeployParamsStruct, + "govChainEmergencySecurityCouncil" | + "l2CoreGovTimelock" | + "govChainProxyAdmin" | + "arbToken" | + "l1ArbitrumTimelock" | + "l2UpgradeExecutor" | + "firstNominationStartDate" | + "securityCouncils" | + "upgradeExecutors" +>; + +const zxDead = "0x000000000000000000000000000000000000dead"; + +const partialDeployParams: DeployParamsPartial = { + secondCohort: [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000005", + "0x0000000000000000000000000000000000000006", + ], + firstCohort: [ + "0x000000000000000000000000000000000000000a", + "0x000000000000000000000000000000000000000b", + "0x000000000000000000000000000000000000000c", + "0x000000000000000000000000000000000000000d", + "0x000000000000000000000000000000000000000e", + "0x000000000000000000000000000000000000000f", + ], + l1TimelockMinDelay: 0, + removalGovVotingDelay: 0, + removalGovVotingPeriod: minutes(10), + removalGovQuorumNumerator: 1000, // 10% + removalGovProposalThreshold: ethers.utils.parseEther("1000000"), // 1 million tokens + removalGovVoteSuccessNumerator: 8333, // todo: check this + removalGovMinPeriodAfterQuorum: minutes(3), + removalProposalExpirationBlocks: 100, // number of blocks after which a successful removal proposal expires + nomineeVettingDuration: minutes(10), + nomineeVetter: zxDead, + nomineeQuorumNumerator: 20, // 0.2% + nomineeVotingPeriod: minutes(10), + memberVotingPeriod: minutes(10), + fullWeightDuration: minutes(5), +} + +function minutes(n: number) { + return Math.round(n * 60); +} + +function deployNoopContract(signer: Wallet) { + // returns 1 + const bytecode = "60088060093d393df360015f5260205ff3"; + return new ethers.ContractFactory([], bytecode, signer).deploy(); +} + +function deployDummyGnosisSafe(signer: Wallet) { + const bytecode = "608060405234801561001057600080fd5b50610154806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632f54bf6e1461003b578063a0e67e2b14610064575b600080fd5b61004f6100493660046100a1565b50600190565b60405190151581526020015b60405180910390f35b61006c610079565b60405161005b91906100d1565b60408051600c8082526101a08201909252606091602082016101808036833701905050905090565b6000602082840312156100b357600080fd5b81356001600160a01b03811681146100ca57600080fd5b9392505050565b6020808252825182820181905260009190848201906040850190845b818110156101125783516001600160a01b0316835292840192918401916001016100ed565b5090969550505050505056fea26469706673582212200450c5c9306adc3deb1c0f327c8ca37c917cad674222ad7536b032eb3c0c3e5c64736f6c63430008100033"; + return new ethers.ContractFactory([], bytecode, signer).deploy(); +} + +async function deployToken(signer: Wallet) { + const impl = await new L2ArbitrumToken__factory(signer).deploy(); + const proxy = await new TransparentUpgradeableProxy__factory(signer).deploy(impl.address, zxDead, "0x"); + + const token = L2ArbitrumToken__factory.connect(proxy.address, signer); + await token.initialize(zxDead, ethers.utils.parseEther("10000000000"), signer.address); + + return token; +} + +function makeStartDateStruct(startDate: Date) { + return { + year: startDate.getUTCFullYear(), + month: startDate.getUTCMonth() + 1, + day: startDate.getUTCDate(), + hour: startDate.getUTCHours(), + } +} + +async function makeFullDeployParams(partialDeployParams: DeployParamsPartial, signer: Wallet): Promise { + const noop = await deployNoopContract(signer); + const gnosisSafe = await deployDummyGnosisSafe(signer); + + const date = new Date(new Date().getTime() + 60 * 60 * 1000); // add 1 hour to the current time + + return { + ...partialDeployParams, + govChainEmergencySecurityCouncil: gnosisSafe.address, + l2CoreGovTimelock: noop.address, + govChainProxyAdmin: noop.address, + arbToken: (await deployToken(signer)).address, + l1ArbitrumTimelock: noop.address, + l2UpgradeExecutor: noop.address, + firstNominationStartDate: makeStartDateStruct(date), + securityCouncils: [], + upgradeExecutors: [], + } +} + +async function deployImplementationsForFactory(signer: Wallet) { + return { + nomineeElectionGovernor: (await new SecurityCouncilNomineeElectionGovernor__factory(signer).deploy()).address, + memberElectionGovernor: (await new SecurityCouncilMemberElectionGovernor__factory(signer).deploy()).address, + securityCouncilManager: (await new SecurityCouncilManager__factory(signer).deploy()).address, + securityCouncilMemberRemoverGov: (await new SecurityCouncilMemberRemovalGovernor__factory(signer).deploy()).address, + } +} + +async function main() { + if (!process.env.PRIVATE_KEY) throw new Error("need PRIVATE_KEY"); + + console.log("RPC_URL:", process.env.RPC_URL || "http://localhost:8545"); + + const signer = new ethers.Wallet(process.env.PRIVATE_KEY, new ethers.providers.JsonRpcProvider(process.env.RPC_URL)); + + const implementations = await deployImplementationsForFactory(signer); + + // deploy the actual factory contract + const factory = await new L2SecurityCouncilMgmtFactory__factory(signer).deploy(); + + // make full deploy params + const fullDeployParams = await makeFullDeployParams(partialDeployParams, signer); + + // call the factory's deploy function + const deployTx = await factory.deploy( + fullDeployParams, + implementations, + ); + + const deployReceipt = await deployTx.wait(); + + // get ContractsDeployed event + const event = deployReceipt.events?.find((e) => e.event === "ContractsDeployed"); + + const namedItems = getNamedObjectItems(event?.args?.deployedContracts); + + console.log({ + ...namedItems, + arbToken: fullDeployParams.arbToken, + }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/security-council-mgmt-deployment/main.ts b/scripts/security-council-mgmt-deployment/main.ts new file mode 100644 index 00000000..4a7403dc --- /dev/null +++ b/scripts/security-council-mgmt-deployment/main.ts @@ -0,0 +1,57 @@ +import yargs from "yargs"; +import mainnetConfig from "./configs/mainnet"; +import goerliConfig from "./configs/arbgoerli"; +import { deployContracts } from "./deployContracts"; + +import {promises as fs} from "fs"; + +/** + * To run: + * + * set params in configs/ file + * + * Set the following env vars: + * ARB_KEY (pk for funded address on governance chain) + * ETH_KEY (pk for funding address on l1) + * ARB_URL (gov chain RPC URL) + * ETH_ULL (l1 RPC URL) + * + * For mainnet only: + * NOVA_KEY + * NOVA_URL + * + * run: + * yarn deploy:sc-mgmt --network mainnet | goerli + * + */ +const options = yargs(process.argv.slice(2)) + .options({ + network: { type: "string", demandOption: true }, + }) + .parseSync() as { + network: string; +}; + +const main = async () => { + let config; + switch (options.network) { + case "mainnet": + config = mainnetConfig; + break; + case "goerli": + config = goerliConfig; + break; + default: + throw new Error(`Unsupported network: ${options.network}`); + } + + const deployment = await deployContracts(config); + + await fs.writeFile(`./files/${options.network}/scmDeployment.json`, JSON.stringify(deployment, null, 2)); + + return deployment; +}; + +main().then(() => { + console.log("Deployment done"); +}); diff --git a/scripts/security-council-mgmt-deployment/safe-abis/GnosisSafeL2.json b/scripts/security-council-mgmt-deployment/safe-abis/GnosisSafeL2.json new file mode 100644 index 00000000..692b238a --- /dev/null +++ b/scripts/security-council-mgmt-deployment/safe-abis/GnosisSafeL2.json @@ -0,0 +1,1138 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "AddedOwner", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "approvedHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ApproveHash", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "handler", + "type": "address" + } + ], + "name": "ChangedFallbackHandler", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "guard", + "type": "address" + } + ], + "name": "ChangedGuard", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + } + ], + "name": "ChangedThreshold", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "DisabledModule", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "EnabledModule", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "payment", + "type": "uint256" + } + ], + "name": "ExecutionFailure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "ExecutionFromModuleFailure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "ExecutionFromModuleSuccess", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "payment", + "type": "uint256" + } + ], + "name": "ExecutionSuccess", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "RemovedOwner", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "module", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "SafeModuleTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "address payable", + "name": "refundReceiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "additionalInfo", + "type": "bytes" + } + ], + "name": "SafeMultiSigTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SafeReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "initiator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "owners", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "initializer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "fallbackHandler", + "type": "address" + } + ], + "name": "SafeSetup", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "msgHash", + "type": "bytes32" + } + ], + "name": "SignMsg", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "addOwnerWithThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hashToApprove", + "type": "bytes32" + } + ], + "name": "approveHash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "approvedHashes", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "changeThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "dataHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "requiredSignatures", + "type": "uint256" + } + ], + "name": "checkNSignatures", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "dataHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + } + ], + "name": "checkSignatures", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "prevModule", + "type": "address" + }, + { + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "disableModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "domainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "enableModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "internalType": "address", + "name": "refundReceiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + } + ], + "name": "encodeTransactionData", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "internalType": "address payable", + "name": "refundReceiver", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + } + ], + "name": "execTransaction", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "execTransactionFromModule", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "execTransactionFromModuleReturnData", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "start", + "type": "address" + }, + { + "internalType": "uint256", + "name": "pageSize", + "type": "uint256" + } + ], + "name": "getModulesPaginated", + "outputs": [ + { + "internalType": "address[]", + "name": "array", + "type": "address[]" + }, + { + "internalType": "address", + "name": "next", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOwners", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "getStorageAt", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "internalType": "address", + "name": "refundReceiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + } + ], + "name": "getTransactionHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "isModuleEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "isOwner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "prevOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "removeOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "requiredTxGas", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "handler", + "type": "address" + } + ], + "name": "setFallbackHandler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guard", + "type": "address" + } + ], + "name": "setGuard", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_owners", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "address", + "name": "fallbackHandler", + "type": "address" + }, + { + "internalType": "address", + "name": "paymentToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "payment", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "paymentReceiver", + "type": "address" + } + ], + "name": "setup", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "signedMessages", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "targetContract", + "type": "address" + }, + { + "internalType": "bytes", + "name": "calldataPayload", + "type": "bytes" + } + ], + "name": "simulateAndRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "prevOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "swapOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/scripts/security-council-mgmt-deployment/safe-abis/GnosisSafeProxyFactory.json b/scripts/security-council-mgmt-deployment/safe-abis/GnosisSafeProxyFactory.json new file mode 100644 index 00000000..4e405b66 --- /dev/null +++ b/scripts/security-council-mgmt-deployment/safe-abis/GnosisSafeProxyFactory.json @@ -0,0 +1,163 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract GnosisSafeProxy", + "name": "proxy", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "singleton", + "type": "address" + } + ], + "name": "ProxyCreation", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_singleton", + "type": "address" + }, + { + "internalType": "bytes", + "name": "initializer", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "saltNonce", + "type": "uint256" + } + ], + "name": "calculateCreateProxyWithNonceAddress", + "outputs": [ + { + "internalType": "contract GnosisSafeProxy", + "name": "proxy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "singleton", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "createProxy", + "outputs": [ + { + "internalType": "contract GnosisSafeProxy", + "name": "proxy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_singleton", + "type": "address" + }, + { + "internalType": "bytes", + "name": "initializer", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "saltNonce", + "type": "uint256" + }, + { + "internalType": "contract IProxyCreationCallback", + "name": "callback", + "type": "address" + } + ], + "name": "createProxyWithCallback", + "outputs": [ + { + "internalType": "contract GnosisSafeProxy", + "name": "proxy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_singleton", + "type": "address" + }, + { + "internalType": "bytes", + "name": "initializer", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "saltNonce", + "type": "uint256" + } + ], + "name": "createProxyWithNonce", + "outputs": [ + { + "internalType": "contract GnosisSafeProxy", + "name": "proxy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proxyCreationCode", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "proxyRuntimeCode", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/scripts/security-council-mgmt-deployment/types.ts b/scripts/security-council-mgmt-deployment/types.ts new file mode 100644 index 00000000..825121b5 --- /dev/null +++ b/scripts/security-council-mgmt-deployment/types.ts @@ -0,0 +1,86 @@ +import { DeployedContracts } from "../../src-ts/types"; +import { DeployParamsStruct } from "../../typechain-types/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory"; +import { Signer } from "ethers"; + +export interface SecurityCouncilAndChainID { + securityCouncilAddress: string; + chainID: number; +} + +export interface ChainIDs { + govChainID: number, + l1ChainID: number, +} + +export type ChainConfig = { + chainID: number; + rpcUrl: string; + privateKey: string; + prevEmergencySecurityCouncil: string; +} + +export type GovernedChainConfig = ChainConfig & { + upExecLocation: string; +} + +export type GovernanceChainConfig = ChainConfig & { + prevNonEmergencySecurityCouncil: string; +} + +export type DeploymentConfig = + DeployedContracts & + Pick< + DeployParamsStruct, + 'removalGovVotingDelay' | + 'removalGovVotingPeriod' | + 'removalGovQuorumNumerator' | + 'removalGovProposalThreshold' | + 'removalGovVoteSuccessNumerator' | + 'removalGovMinPeriodAfterQuorum' | + 'removalProposalExpirationBlocks' | + 'firstNominationStartDate' | + 'nomineeVettingDuration' | + 'nomineeVetter' | + 'nomineeQuorumNumerator' | + 'nomineeVotingPeriod' | + 'memberVotingPeriod' | + 'fullWeightDuration' | + 'firstCohort' | + 'secondCohort' + > & { + emergencySignerThreshold: number; + nonEmergencySignerThreshold: number; + /** i.e. ArbOne */ + govChain: GovernanceChainConfig; + /** i.e. Ethereum L1 */ + hostChain: ChainConfig; + /** i.e. [Nova], governedChains DOES NOT include the governance chain (i.e. ArbOne) */ + governedChains: GovernedChainConfig[]; + }; + +export interface ChainIDToConnectedSigner { + [key: number]: Signer; +} + +export type SecurityCouncilManagementDeploymentResult = { + keyValueStores: {[key: number]: string}; + securityCouncilMemberSyncActions: {[key: number]: string}; + + emergencyGnosisSafes: {[key: number]: string}; + nonEmergencyGnosisSafe: string; + + nomineeElectionGovernor: string; + nomineeElectionGovernorLogic: string; + memberElectionGovernor: string; + memberElectionGovernorLogic: string; + securityCouncilManager: string; + securityCouncilManagerLogic: string; + securityCouncilMemberRemoverGov: string; + securityCouncilMemberRemoverGovLogic: string; + + upgradeExecRouteBuilder: string; + + activationActionContracts: {[key: number]: string}; + + l2SecurityCouncilMgmtFactory: string; +}; \ No newline at end of file diff --git a/scripts/security-council-mgmt-deployment/utils.ts b/scripts/security-council-mgmt-deployment/utils.ts new file mode 100644 index 00000000..723e2ff7 --- /dev/null +++ b/scripts/security-council-mgmt-deployment/utils.ts @@ -0,0 +1,38 @@ +import * as fs from "fs"; +import { DeployedContracts } from "../../src-ts/types"; + +export function blocks(num: number, unit: 'hours' | 'days', blockTime = 12) { + let seconds; + switch (unit) { + case 'hours': + seconds = num * 60 * 60; + break; + case 'days': + seconds = num * 24 * 60 * 60; + break; + default: + throw new Error(`invalid unit: ${unit}`); + } + return Math.floor(seconds / blockTime); +} + +export function assertDefined(val: T | undefined, msg = "value is undefined"): T { + if (val === undefined) { + throw new Error(msg); + } + return val; +} + +export function readDeployedContracts(path: string) { + return JSON.parse( + fs.readFileSync(path).toString() + ) as DeployedContracts; +} + +export function randomNonce() { + return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); +} + +export function getNamedObjectItems(obj: {[s: string]: unknown} | ArrayLike) { + return Object.entries(obj).reduce((acc, [key, value]) => isNaN(Number(key)) ? {...acc, [key]: value} : acc, {}); +} \ No newline at end of file diff --git a/scripts/security-council-mgmt-deployment/verifyContracts.ts b/scripts/security-council-mgmt-deployment/verifyContracts.ts new file mode 100644 index 00000000..cacf149c --- /dev/null +++ b/scripts/security-council-mgmt-deployment/verifyContracts.ts @@ -0,0 +1,252 @@ +import { GovernanceChainSCMgmtActivationAction__factory, ISecurityCouncilMemberElectionGovernor__factory, KeyValueStore__factory, L1ArbitrumTimelock__factory, L1SCMgmtActivationAction__factory, L2SecurityCouncilMgmtFactory__factory, NonGovernanceChainSCMgmtActivationAction__factory, SecurityCouncilManager__factory, SecurityCouncilMemberElectionGovernor__factory, SecurityCouncilMemberRemovalGovernor__factory, SecurityCouncilMemberSyncAction__factory, SecurityCouncilNomineeElectionGovernor__factory, TransparentUpgradeableProxy__factory, UpgradeExecRouteBuilder__factory } from "../../typechain-types"; +import { ContractVerificationConfig, verifyContracts } from "../minimalContractVerifier"; +import { promises as fs} from "fs"; +import { SecurityCouncilManagementDeploymentResult } from "./types"; +import mainnetConfig from "./configs/mainnet"; +import goerliConfig from "./configs/arbgoerli"; +import { assertDefined } from "./utils"; +import { getL2Network } from "@arbitrum/sdk"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ethers } from "ethers"; + +const TESTNET = true; + +const apiKeys = { + eth: assertDefined(process.env.ETHERSCAN_API_KEY, "ETHERSCAN_API_KEY undefined"), + arb: assertDefined(process.env.ARBISCAN_API_KEY, "ARBISCAN_API_KEY undefined"), + nova: assertDefined(process.env.NOVA_ARBISCAN_API_KEY, "NOVA_ARBISCAN_API_KEY undefined") +}; + +const chainIdToApiKey: {[key: number]: string} = { + 1: apiKeys.eth, + 5: apiKeys.eth, + 42161: apiKeys.arb, + 421613: apiKeys.arb, + 42170: apiKeys.nova +}; + +async function main() { + const path = TESTNET ? "files/goerli/scmDeployment.json" : "files/mainnet/scmDeployment.json"; + const scmDeployment = JSON.parse((await fs.readFile(path)).toString()) as SecurityCouncilManagementDeploymentResult; + const deploymentConfig = TESTNET ? goerliConfig : mainnetConfig; + + // going in order of scmDeployment + + // first do activation contracts + const activationContractConfigs: ContractVerificationConfig[] = [ + { + // gov chain activation contract + factory: new GovernanceChainSCMgmtActivationAction__factory(), + contractName: "GovernanceChainSCMgmtActivationAction", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.activationActionContracts[deploymentConfig.govChain.chainID], + constructorArgs: [ + scmDeployment.emergencyGnosisSafes[deploymentConfig.govChain.chainID], + scmDeployment.nonEmergencyGnosisSafe, + deploymentConfig.govChain.prevEmergencySecurityCouncil, + deploymentConfig.govChain.prevNonEmergencySecurityCouncil, + deploymentConfig.emergencySignerThreshold, + deploymentConfig.nonEmergencySignerThreshold, + scmDeployment.securityCouncilManager, + deploymentConfig.l2AddressRegistry + ], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }, + { + // host chain activation contract + factory: new L1SCMgmtActivationAction__factory(), + contractName: "L1SCMgmtActivationAction", + chainId: deploymentConfig.hostChain.chainID, + address: scmDeployment.activationActionContracts[deploymentConfig.hostChain.chainID], + constructorArgs: [ + scmDeployment.emergencyGnosisSafes[deploymentConfig.hostChain.chainID], + deploymentConfig.hostChain.prevEmergencySecurityCouncil, + deploymentConfig.emergencySignerThreshold, + deploymentConfig.l1Executor, + deploymentConfig.l1Timelock + ], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.hostChain.chainID] + }, + // governed chains activation contracts + ...deploymentConfig.governedChains.map((chain) => ({ + factory: new NonGovernanceChainSCMgmtActivationAction__factory(), + contractName: "NonGovernanceChainSCMgmtActivationAction", + chainId: chain.chainID, + address: scmDeployment.activationActionContracts[chain.chainID], + constructorArgs: [ + scmDeployment.emergencyGnosisSafes[chain.chainID], + chain.prevEmergencySecurityCouncil, + deploymentConfig.emergencySignerThreshold, + chain.upExecLocation + ], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[chain.chainID] + })) + ]; + + // key value stores + const keyValueStoreConfigs: ContractVerificationConfig[] = Object.keys(scmDeployment.keyValueStores).map((chainIdStr) => { + const chainId = parseInt(chainIdStr); + return { + factory: new KeyValueStore__factory(), + contractName: "KeyValueStore", + chainId, + address: scmDeployment.keyValueStores[chainId], + constructorArgs: [], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[chainId] + } + }); + + // member sync actions + const memberSyncActionConfigs: ContractVerificationConfig[] = Object.keys(scmDeployment.securityCouncilMemberSyncActions).map((chainIdStr) => { + const chainId = parseInt(chainIdStr); + return { + factory: new SecurityCouncilMemberSyncAction__factory(), + contractName: "SecurityCouncilMemberSyncAction", + chainId, + address: scmDeployment.securityCouncilMemberSyncActions[chainId], + constructorArgs: [ + scmDeployment.keyValueStores[chainId] + ], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[chainId] + } + }); + + // skip gnosis safes + + // nominee election gov logic + const nomineeElectionGovConfig: ContractVerificationConfig = { + factory: new SecurityCouncilNomineeElectionGovernor__factory(), + contractName: "SecurityCouncilNomineeElectionGovernor", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.nomineeElectionGovernorLogic, + constructorArgs: [], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + // nominee election gov proxy (only need to verify one proxy since they all have same deployed bytecode) + const nomineeElectionGovProxyConfig: ContractVerificationConfig = { + factory: new TransparentUpgradeableProxy__factory(), + contractName: "TransparentUpgradeableProxy", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.nomineeElectionGovernor, + constructorArgs: [ + scmDeployment.nomineeElectionGovernorLogic, + deploymentConfig.l2ProxyAdmin, + [] + ], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + // member election gov logic + const memberElectionGovConfig: ContractVerificationConfig = { + factory: new SecurityCouncilMemberElectionGovernor__factory(), + contractName: "SecurityCouncilMemberElectionGovernor", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.memberElectionGovernorLogic, + constructorArgs: [], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + // manager logic + const managerConfig: ContractVerificationConfig = { + factory: new SecurityCouncilManager__factory(), + contractName: "SecurityCouncilManager", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.securityCouncilManagerLogic, + constructorArgs: [], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + // removal gov logic + const removalGovConfig: ContractVerificationConfig = { + factory: new SecurityCouncilMemberRemovalGovernor__factory(), + contractName: "SecurityCouncilMemberRemovalGovernor", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.securityCouncilMemberRemoverGovLogic, + constructorArgs: [], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + // route builder + const routeBuilderConfig: ContractVerificationConfig = { + factory: new UpgradeExecRouteBuilder__factory(), + contractName: "UpgradeExecRouteBuilder", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.upgradeExecRouteBuilder, + constructorArgs: [ + [ + { + // (L1) host chain executor + chainId: deploymentConfig.hostChain.chainID, + location: { + inbox: ethers.constants.AddressZero, + upgradeExecutor: deploymentConfig.l1Executor, + }, + }, + { + // (L2) gov chain executor + chainId: deploymentConfig.govChain.chainID, + location: { + inbox: (await getL2Network(deploymentConfig.govChain.chainID)).ethBridge.inbox, + upgradeExecutor: deploymentConfig.l2Executor, + }, + }, + // (L2) governed chain executors + ...await Promise.all(deploymentConfig.governedChains.map(async (chain) => { + return { + chainId: chain.chainID, + location: { + inbox: (await getL2Network(chain.chainID)).ethBridge.inbox, + upgradeExecutor: chain.upExecLocation, + } + } + })) + ], + deploymentConfig.l1Timelock, + await L1ArbitrumTimelock__factory.connect(deploymentConfig.l1Timelock, new JsonRpcProvider(assertDefined(process.env.ETH_URL))).getMinDelay() + ], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + // factory + const factoryConfig: ContractVerificationConfig = { + factory: new L2SecurityCouncilMgmtFactory__factory(), + contractName: "L2SecurityCouncilMgmtFactory", + chainId: deploymentConfig.govChain.chainID, + address: scmDeployment.l2SecurityCouncilMgmtFactory, + constructorArgs: [], + foundryProfile: "sec_council_mgmt", + etherscanApiKey: chainIdToApiKey[deploymentConfig.govChain.chainID] + }; + + const allConfigs = [ + ...activationContractConfigs, + ...keyValueStoreConfigs, + ...memberSyncActionConfigs, + nomineeElectionGovConfig, + nomineeElectionGovProxyConfig, + memberElectionGovConfig, + managerConfig, + removalGovConfig, + factoryConfig, + routeBuilderConfig + ]; + + await verifyContracts(allConfigs); +} + +main().then(() => console.log("Done.")).catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/src/L2ArbitrumGovernor.sol b/src/L2ArbitrumGovernor.sol index 743a792a..f08e2cb1 100644 --- a/src/L2ArbitrumGovernor.sol +++ b/src/L2ArbitrumGovernor.sol @@ -33,7 +33,7 @@ contract L2ArbitrumGovernor is /// @notice address for which votes will not be counted toward quorum /// @dev A portion of the Arbitrum tokens will be held by entities (eg the treasury) that /// are not eligible to vote. However, even if their voting/delegation is restricted their - /// tokens will still count towards the total supply, and will therefore affect the quorom. + /// tokens will still count towards the total supply, and will therefore affect the quorum. /// Restricted addresses should be forced to delegate their votes to this special exclude /// addresses which is not counted when calculating quorum /// Example address that should be excluded: DAO treasury, foundation, unclaimed tokens, diff --git a/src/UpgradeExecRouteBuilder.sol b/src/UpgradeExecRouteBuilder.sol new file mode 100644 index 00000000..b1f7f6ee --- /dev/null +++ b/src/UpgradeExecRouteBuilder.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; +import "./UpgradeExecutor.sol"; +import "./L1ArbitrumTimelock.sol"; +import "./security-council-mgmt/Common.sol"; + +interface DefaultGovAction { + function perform() external; +} + +/// @notice The location of an upgrade executor, relative to the host chain. +/// Inbox is set to address(0) if the upgrade executor is on the host chain. +/// Inbox is set to the address of the inbox of another Arbitrum chain if the upgrade executor is +/// is not on the host chain. +struct UpExecLocation { + address inbox; // Inbox should be set to address(0) to signify that the upgrade executor is on the L1/host chain + address upgradeExecutor; +} + +struct ChainAndUpExecLocation { + uint256 chainId; + UpExecLocation location; +} + +/// @notice Builds calldata to target the execution of action contracts in upgrade executors that exist on other chains. +/// Routes target an upgrade executor which is either on the host chain, or can be accessed via the inbox. +/// So routes are of two possible forms: +/// 1. Withdrawal => L1Timelock => UpgradeExecutor +/// 2. Withdrawal => L1Timelock => Inbox => UpgradeExecutor +/// @dev This contract makes the following assumptions: +/// * It is deployed on an L2 - more specifically it has access to an ArbSys which allows it to make withdrawal +/// transactions to a host chain +/// * It can only target one upgrade executor per chain +/// * The upgrade executors being targeted are either on the host chain, or are Arbitrum chains reachable +/// via inboxes on the host chain +/// * There exists a L1 timelock on the host chain +contract UpgradeExecRouteBuilder { + error UpgadeExecDoesntExist(uint256 chainId); + error UpgradeExecAlreadyExists(uint256 chindId); + error ParamLengthMismatch(uint256 len1, uint256 len2); + error EmptyActionBytesData(bytes[]); + + /// @notice The magic value used by the L1 timelock to indicate that a retryable ticket should be created + /// See L1ArbitrumTimelock for more details + address public constant RETRYABLE_TICKET_MAGIC = 0xa723C008e76E379c55599D2E4d93879BeaFDa79C; + /// @notice Default args for creating a proposal, used by createProposalWithDefaulArgs and createProposalBatchWithDefaultArgs + /// Default is function selector for a perform function with no args: 'function perform() external' + bytes public constant DEFAULT_GOV_ACTION_CALLDATA = + abi.encodeWithSelector(DefaultGovAction.perform.selector); + uint256 public constant DEFAULT_VALUE = 0; + /// @notice Default predecessor used when calling the L1 timelock + bytes32 public constant DEFAULT_PREDECESSOR = bytes32(0); + + /// @notice Address of the L1 timelock targeted by this route builder + address public immutable l1TimelockAddr; + /// @notice The minimum delay of the L1 timelock targeted by this route builder + /// @dev If the min delay for this timelock changes then a new route builder will need to be deployed + uint256 public immutable l1TimelockMinDelay; + /// @notice Upgrade Executor locations for each chain (chainId => location) + mapping(uint256 => UpExecLocation) public upExecLocations; + + /// @param _upgradeExecutors Locations of the upgrade executors on each chain + /// @param _l1ArbitrumTimelock Address of the core gov L1 timelock + /// @param _l1TimelockMinDelay Minimum delay for L1 timelock + constructor( + ChainAndUpExecLocation[] memory _upgradeExecutors, + address _l1ArbitrumTimelock, + uint256 _l1TimelockMinDelay + ) { + if (_l1ArbitrumTimelock == address(0)) { + revert ZeroAddress(); + } + + for (uint256 i = 0; i < _upgradeExecutors.length; i++) { + ChainAndUpExecLocation memory chainAndUpExecLocation = _upgradeExecutors[i]; + if (chainAndUpExecLocation.location.upgradeExecutor == address(0)) { + revert ZeroAddress(); + } + if (upExecLocationExists(chainAndUpExecLocation.chainId)) { + revert UpgradeExecAlreadyExists(chainAndUpExecLocation.chainId); + } + upExecLocations[chainAndUpExecLocation.chainId] = chainAndUpExecLocation.location; + } + + l1TimelockAddr = _l1ArbitrumTimelock; + l1TimelockMinDelay = _l1TimelockMinDelay; + } + + /// @notice Check if an upgrade executor exists for the supplied chain id + /// @param _chainId ChainId for target UpExecLocation + function upExecLocationExists(uint256 _chainId) public view returns (bool) { + return upExecLocations[_chainId].upgradeExecutor != address(0); + } + + /// @notice Creates the to address and calldata to be called to execute a route to a batch of action contracts. + /// See Governance Action Contracts for more details. + /// @param chainIds Chain ids containing the actions to be called + /// @param actionAddresses Addresses of the action contracts to be called + /// @param actionValues Values to call the action contracts with + /// @param actionDatas Call data to call the action contracts with + /// @param predecessor A predecessor value for the l1 timelock operation + /// @param timelockSalt A salt for the l1 timelock operation + function createActionRouteData( + uint256[] memory chainIds, + address[] memory actionAddresses, + uint256[] memory actionValues, + bytes[] memory actionDatas, + bytes32 predecessor, + bytes32 timelockSalt + ) public view returns (address, bytes memory) { + if (chainIds.length != actionAddresses.length) { + revert ParamLengthMismatch(chainIds.length, actionAddresses.length); + } + if (chainIds.length != actionValues.length) { + revert ParamLengthMismatch(chainIds.length, actionValues.length); + } + if (chainIds.length != actionDatas.length) { + revert ParamLengthMismatch(chainIds.length, actionDatas.length); + } + + address[] memory schedTargets = new address[](chainIds.length); + uint256[] memory schedValues = new uint256[](chainIds.length); + bytes[] memory schedData = new bytes[](chainIds.length); + + // for each chain create calldata that targets the upgrade executor + // from the l1 timelock + for (uint256 i = 0; i < chainIds.length; i++) { + UpExecLocation memory upExecLocation = upExecLocations[chainIds[i]]; + if (upExecLocation.upgradeExecutor == address(0)) { + revert UpgadeExecDoesntExist(chainIds[i]); + } + if (actionDatas[i].length == 0) { + revert EmptyActionBytesData(actionDatas); + } + + bytes memory executorData = abi.encodeWithSelector( + UpgradeExecutor.execute.selector, actionAddresses[i], actionDatas[i] + ); + + // for L1, inbox is set to address(0): + if (upExecLocation.inbox == address(0)) { + schedTargets[i] = upExecLocation.upgradeExecutor; + schedValues[i] = actionValues[i]; + schedData[i] = executorData; + } else { + // For L2 actions, magic is top level target, and value and calldata are encoded in payload + schedTargets[i] = RETRYABLE_TICKET_MAGIC; + schedValues[i] = 0; + schedData[i] = abi.encode( + upExecLocation.inbox, + upExecLocation.upgradeExecutor, + actionValues[i], + 0, + 0, + executorData + ); + } + } + + // batch those calls to execute from the l1 timelock + bytes memory timelockCallData = abi.encodeWithSelector( + L1ArbitrumTimelock.scheduleBatch.selector, + schedTargets, + schedValues, + schedData, + predecessor, + timelockSalt, + l1TimelockMinDelay + ); + + // create a message to initiate a withdrawal to the L1 timelock + return ( + address(100), + abi.encodeWithSelector(ArbSys.sendTxToL1.selector, l1TimelockAddr, timelockCallData) + ); + } + + /// @notice Creates the to address and calldata to be called to execute a route to a batch of action contracts. + /// Uses common defaults for value, calldata and predecessor. + /// See Governance Action Contracts for more details. + /// @param chainIds Chain ids containing the actions to be called + /// @param actionAddresses Addresses of the action contracts to be called + /// @param timelockSalt A salt for the l1 timelock operation + function createActionRouteDataWithDefaults( + uint256[] memory chainIds, + address[] memory actionAddresses, + bytes32 timelockSalt + ) public view returns (address, bytes memory) { + uint256[] memory values = new uint256[](chainIds.length); + bytes[] memory actionDatas = new bytes[](chainIds.length); + for (uint256 i = 0; i < chainIds.length; i++) { + actionDatas[i] = DEFAULT_GOV_ACTION_CALLDATA; + values[i] = DEFAULT_VALUE; + } + return createActionRouteData( + chainIds, actionAddresses, values, actionDatas, DEFAULT_PREDECESSOR, timelockSalt + ); + } +} diff --git a/src/gov-action-contracts/AIPs/AIP1Point1Target.sol b/src/gov-action-contracts/AIPs/AIP1Point1Target.sol index ee7ccf4c..3a1bb156 100644 --- a/src/gov-action-contracts/AIPs/AIP1Point1Target.sol +++ b/src/gov-action-contracts/AIPs/AIP1Point1Target.sol @@ -7,8 +7,8 @@ pragma solidity 0.8.16; /// and for bookkeeping. /// @dev note that this is not a "Gov Action" contract and thus does not conform to that standard. contract AIP1Point1Target { - address immutable public treasuryTimelock; - address immutable public arbitrumFoundationWallet; + address public immutable treasuryTimelock; + address public immutable arbitrumFoundationWallet; bool public passed = false; diff --git a/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/GovernanceChainSCMgmtActivationAction.sol b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/GovernanceChainSCMgmtActivationAction.sol new file mode 100644 index 00000000..956ce437 --- /dev/null +++ b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/GovernanceChainSCMgmtActivationAction.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../../security-council-mgmt/interfaces/IGnosisSafe.sol"; +import "../../address-registries/L2AddressRegistryInterfaces.sol"; +import "./SecurityCouncilMgmtUpgradeLib.sol"; +import "../../../interfaces/IArbitrumDAOConstitution.sol"; +import "../../../interfaces/IUpgradeExecutor.sol"; +import "../../../interfaces/ICoreTimelock.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +contract GovernanceChainSCMgmtActivationAction { + IGnosisSafe public immutable newEmergencySecurityCouncil; + IGnosisSafe public immutable newNonEmergencySecurityCouncil; + + IGnosisSafe public immutable prevEmergencySecurityCouncil; + IGnosisSafe public immutable prevNonEmergencySecurityCouncil; + + uint256 public immutable emergencySecurityCouncilThreshold; + uint256 public immutable nonEmergencySecurityCouncilThreshold; + + address public immutable securityCouncilManager; + IL2AddressRegistry public immutable l2AddressRegistry; + + bytes32 public constant newConstitutionHash = + 0x60acde40ad14f4ecdb1bea0704d1e3889264fb029231c9016352c670703b35d6; + + constructor( + IGnosisSafe _newEmergencySecurityCouncil, + IGnosisSafe _newNonEmergencySecurityCouncil, + IGnosisSafe _prevEmergencySecurityCouncil, + IGnosisSafe _prevNonEmergencySecurityCouncil, + uint256 _emergencySecurityCouncilThreshold, + uint256 _nonEmergencySecurityCouncilThreshold, + address _securityCouncilManager, + IL2AddressRegistry _l2AddressRegistry + ) { + newEmergencySecurityCouncil = _newEmergencySecurityCouncil; + newNonEmergencySecurityCouncil = _newNonEmergencySecurityCouncil; + + prevEmergencySecurityCouncil = _prevEmergencySecurityCouncil; + prevNonEmergencySecurityCouncil = _prevNonEmergencySecurityCouncil; + + emergencySecurityCouncilThreshold = _emergencySecurityCouncilThreshold; + nonEmergencySecurityCouncilThreshold = _nonEmergencySecurityCouncilThreshold; + + securityCouncilManager = _securityCouncilManager; + l2AddressRegistry = _l2AddressRegistry; + } + + function perform() external { + IUpgradeExecutor upgradeExecutor = IUpgradeExecutor(l2AddressRegistry.coreGov().owner()); + + // swap in new emergency security council + SecurityCouncilMgmtUpgradeLib.replaceEmergencySecurityCouncil({ + _prevSecurityCouncil: prevEmergencySecurityCouncil, + _newSecurityCouncil: newEmergencySecurityCouncil, + _threshold: emergencySecurityCouncilThreshold, + _upgradeExecutor: upgradeExecutor + }); + + // swap in new nonEmergency security council + SecurityCouncilMgmtUpgradeLib.requireSafesEquivalent( + prevNonEmergencySecurityCouncil, + newNonEmergencySecurityCouncil, + nonEmergencySecurityCouncilThreshold + ); + + ICoreTimelock l2CoreGovTimelock = + ICoreTimelock(address(l2AddressRegistry.coreGovTimelock())); + + bytes32 TIMELOCK_PROPOSAL_ROLE = l2CoreGovTimelock.PROPOSER_ROLE(); + bytes32 TIMELOCK_CANCELLER_ROLE = l2CoreGovTimelock.CANCELLER_ROLE(); + + require( + l2CoreGovTimelock.hasRole( + TIMELOCK_PROPOSAL_ROLE, address(prevNonEmergencySecurityCouncil) + ), + "GovernanceChainSCMgmtActivationAction: prev nonemergency council doesn't have proposal role" + ); + require( + !l2CoreGovTimelock.hasRole( + TIMELOCK_PROPOSAL_ROLE, address(newNonEmergencySecurityCouncil) + ), + "GovernanceChainSCMgmtActivationAction: new nonemergency council already has proposal role" + ); + + l2CoreGovTimelock.revokeRole( + TIMELOCK_PROPOSAL_ROLE, address(prevNonEmergencySecurityCouncil) + ); + + l2CoreGovTimelock.grantRole(TIMELOCK_PROPOSAL_ROLE, address(newNonEmergencySecurityCouncil)); + + // give timelock access to manager + require( + Address.isContract(securityCouncilManager), + "GovernanceChainSCMgmtActivationAction: manager address isn't a contract" + ); + + require( + !l2CoreGovTimelock.hasRole(TIMELOCK_PROPOSAL_ROLE, securityCouncilManager), + "GovernanceChainSCMgmtActivationAction: securityCouncilManager already has proposal role" + ); + l2CoreGovTimelock.grantRole(TIMELOCK_PROPOSAL_ROLE, securityCouncilManager); + + // revoke old security council cancel role; it is unnecessary to grant it to explicitly grant it to new security council since the security council can already cancel via the core governor's relay method. + require( + l2CoreGovTimelock.hasRole( + TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil) + ), + "GovernanceChainSCMgmtActivationAction: prev emergency security council should have cancellor role" + ); + + l2CoreGovTimelock.revokeRole(TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil)); + + // confirm updates + bytes32 EXECUTOR_ROLE = upgradeExecutor.EXECUTOR_ROLE(); + require( + upgradeExecutor.hasRole(EXECUTOR_ROLE, address(newEmergencySecurityCouncil)), + "NonGovernanceChainSCMgmtActivationAction: new emergency security council not set" + ); + require( + !upgradeExecutor.hasRole(EXECUTOR_ROLE, address(prevEmergencySecurityCouncil)), + "NonGovernanceChainSCMgmtActivationAction: prev emergency security council still set" + ); + + require( + !l2CoreGovTimelock.hasRole( + TIMELOCK_PROPOSAL_ROLE, address(prevNonEmergencySecurityCouncil) + ), + "GovernanceChainSCMgmtActivationAction: prev nonemergency council still has proposal role" + ); + require( + l2CoreGovTimelock.hasRole( + TIMELOCK_PROPOSAL_ROLE, address(newNonEmergencySecurityCouncil) + ), + "GovernanceChainSCMgmtActivationAction: new nonemergency doesn't have proposal role" + ); + + require( + l2CoreGovTimelock.hasRole(TIMELOCK_PROPOSAL_ROLE, securityCouncilManager), + "GovernanceChainSCMgmtActivationAction: securityCouncilManager doesn't have proposal role" + ); + require( + !l2CoreGovTimelock.hasRole( + TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil) + ), + "GovernanceChainSCMgmtActivationAction: prev emergency security council still has cancellor role" + ); + IArbitrumDAOConstitution arbitrumDaoConstitution = + l2AddressRegistry.arbitrumDAOConstitution(); + arbitrumDaoConstitution.setConstitutionHash(newConstitutionHash); + require( + arbitrumDaoConstitution.constitutionHash() == newConstitutionHash, + "GovernanceChainSCMgmtActivationAction: new constitution hash not set" + ); + } +} diff --git a/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/L1SCMgmtActivationAction.sol b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/L1SCMgmtActivationAction.sol new file mode 100644 index 00000000..602e149c --- /dev/null +++ b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/L1SCMgmtActivationAction.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../../security-council-mgmt/interfaces/IGnosisSafe.sol"; +import "../../../interfaces/IUpgradeExecutor.sol"; +import "../../../interfaces/ICoreTimelock.sol"; +import "./SecurityCouncilMgmtUpgradeLib.sol"; + +contract L1SCMgmtActivationAction { + IGnosisSafe public immutable newEmergencySecurityCouncil; + IGnosisSafe public immutable prevEmergencySecurityCouncil; + uint256 public immutable emergencySecurityCouncilThreshold; + IUpgradeExecutor public immutable l1UpgradeExecutor; + ICoreTimelock public immutable l1Timelock; + + constructor( + IGnosisSafe _newEmergencySecurityCouncil, + IGnosisSafe _prevEmergencySecurityCouncil, + uint256 _emergencySecurityCouncilThreshold, + IUpgradeExecutor _l1UpgradeExecutor, + ICoreTimelock _l1Timelock + ) { + newEmergencySecurityCouncil = _newEmergencySecurityCouncil; + prevEmergencySecurityCouncil = _prevEmergencySecurityCouncil; + emergencySecurityCouncilThreshold = _emergencySecurityCouncilThreshold; + l1UpgradeExecutor = _l1UpgradeExecutor; + l1Timelock = _l1Timelock; + } + + function perform() external { + // swap in new emergency security council + SecurityCouncilMgmtUpgradeLib.replaceEmergencySecurityCouncil({ + _prevSecurityCouncil: prevEmergencySecurityCouncil, + _newSecurityCouncil: newEmergencySecurityCouncil, + _threshold: emergencySecurityCouncilThreshold, + _upgradeExecutor: l1UpgradeExecutor + }); + + // swap in new emergency security council canceller role + bytes32 TIMELOCK_CANCELLER_ROLE = l1Timelock.CANCELLER_ROLE(); + require( + l1Timelock.hasRole(TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil)), + "GovernanceChainSCMgmtActivationAction: prev emergency security council should have cancellor role" + ); + require( + !l1Timelock.hasRole(TIMELOCK_CANCELLER_ROLE, address(l1UpgradeExecutor)), + "GovernanceChainSCMgmtActivationAction: l1UpgradeExecutor already has cancellor role" + ); + + l1Timelock.revokeRole(TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil)); + l1Timelock.grantRole(TIMELOCK_CANCELLER_ROLE, address(l1UpgradeExecutor)); + + // confirm updates + require( + l1Timelock.hasRole(TIMELOCK_CANCELLER_ROLE, address(l1UpgradeExecutor)), + "GovernanceChainSCMgmtActivationAction: l1UpgradeExecutor canceller role not set" + ); + require( + !l1Timelock.hasRole(TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil)), + "GovernanceChainSCMgmtActivationAction: prevEmergencySecurityCouncil canceller role not revoked" + ); + } +} diff --git a/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/NonGovernanceChainSCMgmtActivationAction.sol b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/NonGovernanceChainSCMgmtActivationAction.sol new file mode 100644 index 00000000..d3c48f05 --- /dev/null +++ b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/NonGovernanceChainSCMgmtActivationAction.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../../security-council-mgmt/interfaces/IGnosisSafe.sol"; +import "./SecurityCouncilMgmtUpgradeLib.sol"; + +contract NonGovernanceChainSCMgmtActivationAction { + IGnosisSafe public immutable newEmergencySecurityCouncil; + IGnosisSafe public immutable prevEmergencySecurityCouncil; + uint256 public immutable emergencySecurityCouncilThreshold; + IUpgradeExecutor public immutable upgradeExecutor; + + constructor( + IGnosisSafe _newEmergencySecurityCouncil, + IGnosisSafe _prevEmergencySecurityCouncil, + uint256 _emergencySecurityCouncilThreshold, + IUpgradeExecutor _upgradeExecutor + ) { + newEmergencySecurityCouncil = _newEmergencySecurityCouncil; + prevEmergencySecurityCouncil = _prevEmergencySecurityCouncil; + emergencySecurityCouncilThreshold = _emergencySecurityCouncilThreshold; + upgradeExecutor = _upgradeExecutor; + } + + function perform() external { + // swap in new emergency security council + SecurityCouncilMgmtUpgradeLib.replaceEmergencySecurityCouncil({ + _prevSecurityCouncil: prevEmergencySecurityCouncil, + _newSecurityCouncil: newEmergencySecurityCouncil, + _threshold: emergencySecurityCouncilThreshold, + _upgradeExecutor: upgradeExecutor + }); + + // confirm updates + bytes32 EXECUTOR_ROLE = upgradeExecutor.EXECUTOR_ROLE(); + require( + upgradeExecutor.hasRole(EXECUTOR_ROLE, address(newEmergencySecurityCouncil)), + "NonGovernanceChainSCMgmtActivationAction: new emergency security council not set" + ); + require( + !upgradeExecutor.hasRole(EXECUTOR_ROLE, address(prevEmergencySecurityCouncil)), + "NonGovernanceChainSCMgmtActivationAction: prev emergency security council still set" + ); + } +} diff --git a/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/SecurityCouncilMgmtUpgradeLib.sol b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/SecurityCouncilMgmtUpgradeLib.sol new file mode 100644 index 00000000..d12c9212 --- /dev/null +++ b/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/SecurityCouncilMgmtUpgradeLib.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../../security-council-mgmt/interfaces/IGnosisSafe.sol"; +import "../../../interfaces/IUpgradeExecutor.sol"; + +library SecurityCouncilMgmtUpgradeLib { + function replaceEmergencySecurityCouncil( + IGnosisSafe _prevSecurityCouncil, + IGnosisSafe _newSecurityCouncil, + uint256 _threshold, + IUpgradeExecutor _upgradeExecutor + ) internal { + requireSafesEquivalent(_prevSecurityCouncil, _newSecurityCouncil, _threshold); + bytes32 EXECUTOR_ROLE = _upgradeExecutor.EXECUTOR_ROLE(); + require( + _upgradeExecutor.hasRole(EXECUTOR_ROLE, address(_prevSecurityCouncil)), + "SecurityCouncilMgmtUpgradeLib: prev council not executor" + ); + require( + !_upgradeExecutor.hasRole(EXECUTOR_ROLE, address(_newSecurityCouncil)), + "SecurityCouncilMgmtUpgradeLib: new council already executor" + ); + + _upgradeExecutor.revokeRole(EXECUTOR_ROLE, address(_prevSecurityCouncil)); + _upgradeExecutor.grantRole(EXECUTOR_ROLE, address(_newSecurityCouncil)); + } + + function requireSafesEquivalent( + IGnosisSafe _safe1, + IGnosisSafe safe2, + uint256 _expectedThreshold + ) internal view { + uint256 newSecurityCouncilThreshold = safe2.getThreshold(); + require( + _safe1.getThreshold() == newSecurityCouncilThreshold, + "SecurityCouncilMgmtUpgradeLib: threshold mismatch" + ); + require( + newSecurityCouncilThreshold == _expectedThreshold, + "SecurityCouncilMgmtUpgradeLib: unexpected threshold" + ); + + address[] memory prevOwners = _safe1.getOwners(); + address[] memory newOwners = safe2.getOwners(); + require( + areUniqueAddressArraysEqual(prevOwners, newOwners), + "SecurityCouncilMgmtUpgradeLib: owners mismatch" + ); + } + + /// @notice assumes each address array has no repeated elements (i.e., as is the enforced for gnosis safe owners) + function areUniqueAddressArraysEqual(address[] memory array1, address[] memory array2) + public + pure + returns (bool) + { + if (array1.length != array2.length) { + return false; + } + + for (uint256 i = 0; i < array1.length; i++) { + bool found = false; + for (uint256 j = 0; j < array2.length; j++) { + if (array1[i] == array2[j]) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + + return true; + } +} diff --git a/src/gov-action-contracts/execution-record/ActionExecutionRecord.sol b/src/gov-action-contracts/execution-record/ActionExecutionRecord.sol new file mode 100644 index 00000000..64269321 --- /dev/null +++ b/src/gov-action-contracts/execution-record/ActionExecutionRecord.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "./KeyValueStore.sol"; + +/// @notice Stores a record that the action executed. +/// Can be useful for enforcing dependency between actions +/// @dev This contract is designed to be inherited by action contracts, so it +/// it must not use any local storage +contract ActionExecutionRecord { + /// @notice The key value store used to record the execution + /// @dev Local storage cannot be used in action contracts as they're delegate called into + KeyValueStore public immutable store; + + /// @notice A unique id for this action contract + bytes32 public immutable actionContractId; + + constructor(KeyValueStore _store, string memory _uniqueActionName) { + store = _store; + actionContractId = keccak256(bytes(_uniqueActionName)); + } + + /// @notice Sets a value in the store + /// @dev Combines the provided key with the action contract id + function _set(uint256 key, uint256 value) internal { + store.set(computeKey(key), value); + } + + /// @notice Gets a value from the store + /// @dev Combines the provided key with the action contract id + function _get(uint256 key) internal view returns (uint256) { + return store.get(computeKey(key)); + } + + /// @notice This contract uses a composite key of the provided key and the action contract id. + /// This function can be used to calculate the composite key + function computeKey(uint256 key) public view returns (uint256) { + return uint256(keccak256(abi.encode(actionContractId, key))); + } +} diff --git a/src/gov-action-contracts/execution-record/KeyValueStore.sol b/src/gov-action-contracts/execution-record/KeyValueStore.sol new file mode 100644 index 00000000..8dbf7f77 --- /dev/null +++ b/src/gov-action-contracts/execution-record/KeyValueStore.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +/// @title A global key value store +/// @notice Stores values against a key, combines msg.sender with the provided key to ensure uniqueness +contract KeyValueStore { + event ValueSet(address indexed sender, uint256 indexed key, uint256 value); + + mapping(uint256 => uint256) public store; + + /// @notice Sets a value in the store + /// @dev Combines the provided key with the msg.sender to ensure uniqueness + function set(uint256 key, uint256 value) external { + store[computeKey(msg.sender, key)] = value; + emit ValueSet({sender: msg.sender, key: key, value: value}); + } + + /// @notice Get a value from the store for the current msg.sender + function get(uint256 key) external view returns (uint256) { + return _get(msg.sender, key); + } + + /// @notice Get a value from the store for any sender + function get(address owner, uint256 key) external view returns (uint256) { + return _get(owner, key); + } + + /// @notice Compute the composite key for a specific user + function computeKey(address owner, uint256 key) public pure returns (uint256) { + return uint256(keccak256(abi.encode(owner, key))); + } + + function _get(address owner, uint256 key) internal view returns (uint256) { + return store[computeKey(owner, key)]; + } +} diff --git a/src/interfaces/IArbitrumDAOConstitution.sol b/src/interfaces/IArbitrumDAOConstitution.sol index a5a3de56..21f03d49 100644 --- a/src/interfaces/IArbitrumDAOConstitution.sol +++ b/src/interfaces/IArbitrumDAOConstitution.sol @@ -5,4 +5,4 @@ interface IArbitrumDAOConstitution { function constitutionHash() external view returns (bytes32); function setConstitutionHash(bytes32 _constitutionHash) external; function owner() external view returns (address); -} \ No newline at end of file +} diff --git a/src/interfaces/ICoreTimelock.sol b/src/interfaces/ICoreTimelock.sol new file mode 100644 index 00000000..492c61ba --- /dev/null +++ b/src/interfaces/ICoreTimelock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "./IArbitrumTimelock.sol"; +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; + +interface ICoreTimelock is IArbitrumTimelock, IAccessControlUpgradeable { + function TIMELOCK_ADMIN_ROLE() external returns (bytes32); + function PROPOSER_ROLE() external returns (bytes32); + function EXECUTOR_ROLE() external returns (bytes32); + function CANCELLER_ROLE() external returns (bytes32); +} diff --git a/src/interfaces/IUpgradeExecutor.sol b/src/interfaces/IUpgradeExecutor.sol new file mode 100644 index 00000000..e9f02416 --- /dev/null +++ b/src/interfaces/IUpgradeExecutor.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; + +interface IUpgradeExecutor is IAccessControlUpgradeable { + function execute(address upgrade, bytes memory upgradeCallData) external; + function ADMIN_ROLE() external returns (bytes32); + function EXECUTOR_ROLE() external returns (bytes32); +} diff --git a/src/security-council-mgmt/Common.sol b/src/security-council-mgmt/Common.sol new file mode 100644 index 00000000..1433d754 --- /dev/null +++ b/src/security-council-mgmt/Common.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +/// @notice Security council members are members of one of two cohorts. +/// Periodically all the positions on a cohort are put up for election, +/// and the are members replaced with new ones. +enum Cohort { + FIRST, + SECOND +} + +/// @notice Date struct for convenience +struct Date { + uint256 year; + uint256 month; + uint256 day; + uint256 hour; +} + +error ZeroAddress(); +error NotAContract(address account); diff --git a/src/security-council-mgmt/SecurityCouncilManager.sol b/src/security-council-mgmt/SecurityCouncilManager.sol new file mode 100644 index 00000000..fb03bf74 --- /dev/null +++ b/src/security-council-mgmt/SecurityCouncilManager.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../ArbitrumTimelock.sol"; +import "../UpgradeExecutor.sol"; +import "../L1ArbitrumTimelock.sol"; +import "./SecurityCouncilMgmtUtils.sol"; +import "./interfaces/ISecurityCouncilManager.sol"; +import "./SecurityCouncilMemberSyncAction.sol"; +import "../UpgradeExecRouteBuilder.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "./Common.sol"; + +/// @title The Security Council Manager +/// @notice The source of truth for an array of Security Councils that are under management. +/// Can be used to change members, and replace whole cohorts, ensuring that all managed +/// Security Councils stay in sync. +/// @dev The cohorts in the Security Council Manager can be updated from a number of different sources. +/// Care must be taken in the timing of these updates to avoid race conditions, as well as to avoid +/// invalidating other operations. +/// An example of this could be replacing a member whilst there is an ongoing election. This contract +/// ensures that a member cannot be in both cohorts, so if a cohort is elected but just prior the security +/// council decides to replace a member in the previous cohort, then a member could end up in both cohorts. +/// Since the functions in this contract ensure that this cannot be case, one of the transactions will fail. +/// To avoid this care must be taken whilst elections are ongoing. +contract SecurityCouncilManager is + Initializable, + AccessControlUpgradeable, + ISecurityCouncilManager +{ + event CohortReplaced(address[] newCohort, Cohort indexed cohort); + event MemberAdded(address indexed newMember, Cohort indexed cohort); + event MemberRemoved(address indexed member, Cohort indexed cohort); + event MemberReplaced(address indexed replacedMember, address indexed newMember, Cohort cohort); + event MemberRotated(address indexed replacedAddress, address indexed newAddress, Cohort cohort); + event SecurityCouncilAdded( + address indexed securityCouncil, + address indexed updateAction, + uint256 securityCouncilsLength + ); + event SecurityCouncilRemoved( + address indexed securityCouncil, + address indexed updateAction, + uint256 securityCouncilsLength + ); + event UpgradeExecRouteBuilderSet(address indexed UpgradeExecRouteBuilder); + + // The Security Council members are separated into two cohorts, allowing a whole cohort to be replaced, as + // specified by the Arbitrum Constitution. + // These two cohort arrays contain the source of truth for the members of the Security Council. When a membership + // change needs to be made, a change to these arrays is first made here locally, then pushed to each of the Security Councils + // A member cannot be in both cohorts at the same time + address[] internal firstCohort; + address[] internal secondCohort; + + /// @notice Address of the l2 timelock used by core governance + address payable public l2CoreGovTimelock; + + /// @notice The list of Security Councils under management. Any changes to the cohorts in this manager + /// will be pushed to each of these security councils, ensuring that they all stay in sync + SecurityCouncilData[] public securityCouncils; + + /// @notice Address of UpgradeExecRouteBuilder. Used to help create security council updates + UpgradeExecRouteBuilder public router; + + /// @notice Maximum possible number of Security Councils to manage + /// @dev Since the councils array will be iterated this provides a safety check to make too many Sec Councils + /// aren't added to the array. + uint256 public immutable MAX_SECURITY_COUNCILS = 500; + + /// @notice Nonce to ensure that scheduled updates create unique entries in the timelocks + uint256 public updateNonce; + + /// @notice Size of cohort under ordinary circumstances + uint256 public cohortSize; + + /// @notice Magic value used by the L1 timelock to indicate that a retryable ticket should be created + /// Value is defined in L1ArbitrumTimelock contract https://etherscan.io/address/0xE6841D92B0C345144506576eC13ECf5103aC7f49#readProxyContract#F5 + address public constant RETRYABLE_TICKET_MAGIC = 0xa723C008e76E379c55599D2E4d93879BeaFDa79C; + + bytes32 public constant COHORT_REPLACER_ROLE = keccak256("COHORT_REPLACER"); + bytes32 public constant MEMBER_ADDER_ROLE = keccak256("MEMBER_ADDER"); + bytes32 public constant MEMBER_REPLACER_ROLE = keccak256("MEMBER_REPLACER"); + bytes32 public constant MEMBER_ROTATOR_ROLE = keccak256("MEMBER_ROTATOR"); + bytes32 public constant MEMBER_REMOVER_ROLE = keccak256("MEMBER_REMOVER"); + + constructor() { + _disableInitializers(); + } + + function initialize( + address[] memory _firstCohort, + address[] memory _secondCohort, + SecurityCouncilData[] memory _securityCouncils, + SecurityCouncilManagerRoles memory _roles, + address payable _l2CoreGovTimelock, + UpgradeExecRouteBuilder _router + ) external initializer { + if (_firstCohort.length != _secondCohort.length) { + revert CohortLengthMismatch(_firstCohort, _secondCohort); + } + firstCohort = _firstCohort; + secondCohort = _secondCohort; + cohortSize = _firstCohort.length; + _grantRole(DEFAULT_ADMIN_ROLE, _roles.admin); + _grantRole(COHORT_REPLACER_ROLE, _roles.cohortUpdator); + _grantRole(MEMBER_ADDER_ROLE, _roles.memberAdder); + for (uint256 i = 0; i < _roles.memberRemovers.length; i++) { + _grantRole(MEMBER_REMOVER_ROLE, _roles.memberRemovers[i]); + } + _grantRole(MEMBER_ROTATOR_ROLE, _roles.memberRotator); + _grantRole(MEMBER_REPLACER_ROLE, _roles.memberReplacer); + + if (!Address.isContract(_l2CoreGovTimelock)) { + revert NotAContract({account: _l2CoreGovTimelock}); + } + l2CoreGovTimelock = _l2CoreGovTimelock; + + _setUpgradeExecRouteBuilder(_router); + for (uint256 i = 0; i < _securityCouncils.length; i++) { + _addSecurityCouncil(_securityCouncils[i]); + } + } + + /// @inheritdoc ISecurityCouncilManager + function replaceCohort(address[] memory _newCohort, Cohort _cohort) + external + onlyRole(COHORT_REPLACER_ROLE) + { + if (_newCohort.length != cohortSize) { + revert InvalidNewCohortLength({cohort: _newCohort, cohortSize: cohortSize}); + } + + // delete the old cohort + _cohort == Cohort.FIRST ? delete firstCohort : delete secondCohort; + + for (uint256 i = 0; i < _newCohort.length; i++) { + _addMemberToCohortArray(_newCohort[i], _cohort); + } + + _scheduleUpdate(); + emit CohortReplaced(_newCohort, _cohort); + } + + function _addMemberToCohortArray(address _newMember, Cohort _cohort) internal { + if (_newMember == address(0)) { + revert ZeroAddress(); + } + address[] storage cohort = _cohort == Cohort.FIRST ? firstCohort : secondCohort; + if (cohort.length == cohortSize) { + revert CohortFull({cohort: _cohort}); + } + if (firstCohortIncludes(_newMember)) { + revert MemberInCohort({member: _newMember, cohort: Cohort.FIRST}); + } + if (secondCohortIncludes(_newMember)) { + revert MemberInCohort({member: _newMember, cohort: Cohort.SECOND}); + } + + cohort.push(_newMember); + } + + function _removeMemberFromCohortArray(address _member) internal returns (Cohort) { + for (uint256 i = 0; i < 2; i++) { + address[] storage cohort = i == 0 ? firstCohort : secondCohort; + for (uint256 j = 0; j < cohort.length; j++) { + if (_member == cohort[j]) { + cohort[j] = cohort[cohort.length - 1]; + cohort.pop(); + return i == 0 ? Cohort.FIRST : Cohort.SECOND; + } + } + } + revert NotAMember({member: _member}); + } + + /// @inheritdoc ISecurityCouncilManager + function addMember(address _newMember, Cohort _cohort) external onlyRole(MEMBER_ADDER_ROLE) { + _addMemberToCohortArray(_newMember, _cohort); + _scheduleUpdate(); + emit MemberAdded(_newMember, _cohort); + } + + /// @inheritdoc ISecurityCouncilManager + function removeMember(address _member) external onlyRole(MEMBER_REMOVER_ROLE) { + if (_member == address(0)) { + revert ZeroAddress(); + } + Cohort cohort = _removeMemberFromCohortArray(_member); + _scheduleUpdate(); + emit MemberRemoved({member: _member, cohort: cohort}); + } + + /// @inheritdoc ISecurityCouncilManager + function replaceMember(address _memberToReplace, address _newMember) + external + onlyRole(MEMBER_REPLACER_ROLE) + { + Cohort cohort = _swapMembers(_memberToReplace, _newMember); + emit MemberReplaced({ + replacedMember: _memberToReplace, + newMember: _newMember, + cohort: cohort + }); + } + + /// @inheritdoc ISecurityCouncilManager + function rotateMember(address _currentAddress, address _newAddress) + external + onlyRole(MEMBER_ROTATOR_ROLE) + { + Cohort cohort = _swapMembers(_currentAddress, _newAddress); + emit MemberRotated({ + replacedAddress: _currentAddress, + newAddress: _newAddress, + cohort: cohort + }); + } + + function _swapMembers(address _addressToRemove, address _addressToAdd) + internal + returns (Cohort) + { + if (_addressToRemove == address(0) || _addressToAdd == address(0)) { + revert ZeroAddress(); + } + Cohort cohort = _removeMemberFromCohortArray(_addressToRemove); + _addMemberToCohortArray(_addressToAdd, cohort); + _scheduleUpdate(); + return cohort; + } + + function _addSecurityCouncil(SecurityCouncilData memory _securityCouncilData) internal { + if (securityCouncils.length == MAX_SECURITY_COUNCILS) { + revert MaxSecurityCouncils(securityCouncils.length); + } + + if ( + _securityCouncilData.updateAction == address(0) + || _securityCouncilData.securityCouncil == address(0) + ) { + revert ZeroAddress(); + } + + if (_securityCouncilData.chainId == 0) { + revert SecurityCouncilZeroChainID(_securityCouncilData); + } + + if (!router.upExecLocationExists(_securityCouncilData.chainId)) { + revert SecurityCouncilNotInRouter(_securityCouncilData); + } + + for (uint256 i = 0; i < securityCouncils.length; i++) { + SecurityCouncilData storage existantSecurityCouncil = securityCouncils[i]; + + if ( + existantSecurityCouncil.chainId == _securityCouncilData.chainId + && existantSecurityCouncil.securityCouncil == _securityCouncilData.securityCouncil + ) { + revert SecurityCouncilAlreadyInRouter(_securityCouncilData); + } + } + + securityCouncils.push(_securityCouncilData); + emit SecurityCouncilAdded( + _securityCouncilData.securityCouncil, + _securityCouncilData.updateAction, + securityCouncils.length + ); + } + + /// @inheritdoc ISecurityCouncilManager + function addSecurityCouncil(SecurityCouncilData memory _securityCouncilData) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + _addSecurityCouncil(_securityCouncilData); + } + + /// @inheritdoc ISecurityCouncilManager + function removeSecurityCouncil(SecurityCouncilData memory _securityCouncilData) + external + onlyRole(DEFAULT_ADMIN_ROLE) + returns (bool) + { + for (uint256 i = 0; i < securityCouncils.length; i++) { + SecurityCouncilData storage securityCouncilData = securityCouncils[i]; + if ( + securityCouncilData.securityCouncil == _securityCouncilData.securityCouncil + && securityCouncilData.chainId == _securityCouncilData.chainId + && securityCouncilData.updateAction == _securityCouncilData.updateAction + ) { + SecurityCouncilData storage lastSecurityCouncil = + securityCouncils[securityCouncils.length - 1]; + + securityCouncils[i] = lastSecurityCouncil; + securityCouncils.pop(); + emit SecurityCouncilRemoved( + securityCouncilData.securityCouncil, + securityCouncilData.updateAction, + securityCouncils.length + ); + return true; + } + } + revert SecurityCouncilNotInManager(_securityCouncilData); + } + + /// @inheritdoc ISecurityCouncilManager + function setUpgradeExecRouteBuilder(UpgradeExecRouteBuilder _router) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + _setUpgradeExecRouteBuilder(_router); + } + + function _setUpgradeExecRouteBuilder(UpgradeExecRouteBuilder _router) internal { + address routerAddress = address(_router); + + if (!Address.isContract(routerAddress)) { + revert NotAContract({account: routerAddress}); + } + + router = _router; + emit UpgradeExecRouteBuilderSet(routerAddress); + } + + /// @inheritdoc ISecurityCouncilManager + function getFirstCohort() external view returns (address[] memory) { + return firstCohort; + } + + /// @inheritdoc ISecurityCouncilManager + function getSecondCohort() external view returns (address[] memory) { + return secondCohort; + } + + /// @inheritdoc ISecurityCouncilManager + function getBothCohorts() public view returns (address[] memory) { + address[] memory members = new address[](firstCohort.length + secondCohort.length); + for (uint256 i = 0; i < firstCohort.length; i++) { + members[i] = firstCohort[i]; + } + for (uint256 i = 0; i < secondCohort.length; i++) { + members[firstCohort.length + i] = secondCohort[i]; + } + return members; + } + + /// @inheritdoc ISecurityCouncilManager + function securityCouncilsLength() public view returns (uint256) { + return securityCouncils.length; + } + + /// @inheritdoc ISecurityCouncilManager + function firstCohortIncludes(address account) public view returns (bool) { + return cohortIncludes(Cohort.FIRST, account); + } + + /// @inheritdoc ISecurityCouncilManager + function secondCohortIncludes(address account) public view returns (bool) { + return cohortIncludes(Cohort.SECOND, account); + } + + /// @inheritdoc ISecurityCouncilManager + function cohortIncludes(Cohort cohort, address account) public view returns (bool) { + address[] memory cohortMembers = cohort == Cohort.FIRST ? firstCohort : secondCohort; + return SecurityCouncilMgmtUtils.isInArray(account, cohortMembers); + } + + /// @inheritdoc ISecurityCouncilManager + function generateSalt(address[] memory _members, uint256 nonce) + external + pure + returns (bytes32) + { + return keccak256(abi.encode(_members, nonce)); + } + + /// @inheritdoc ISecurityCouncilManager + function getScheduleUpdateInnerData(uint256 nonce) + public + view + returns (address[] memory, address, bytes memory) + { + // build a union array of security council members + address[] memory newMembers = getBothCohorts(); + + // build batch call to L1 timelock + address[] memory actionAddresses = new address[](securityCouncils.length); + bytes[] memory actionDatas = new bytes[](securityCouncils.length); + uint256[] memory chainIds = new uint256[](securityCouncils.length); + + for (uint256 i = 0; i < securityCouncils.length; i++) { + SecurityCouncilData memory securityCouncilData = securityCouncils[i]; + actionAddresses[i] = securityCouncilData.updateAction; + chainIds[i] = securityCouncilData.chainId; + actionDatas[i] = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, + securityCouncilData.securityCouncil, + newMembers, + nonce + ); + } + + // unique salt used for replay protection in the L1 timelock + bytes32 salt = this.generateSalt(newMembers, nonce); + (address to, bytes memory data) = router.createActionRouteData( + chainIds, + actionAddresses, + new uint256[](securityCouncils.length), // all values are always 0 + actionDatas, + 0, + salt + ); + + return (newMembers, to, data); + } + + /// @dev Create a union of the second and first cohort, then update all Security Councils under management with that unioned array. + /// Updates will need to be scheduled through timelocks and target upgrade executors + function _scheduleUpdate() internal { + // always update the nonce + // this is used to ensure that proposals in the timelocks are unique + // and calls to the upgradeExecutors are in the correct order + updateNonce++; + (address[] memory newMembers, address to, bytes memory data) = + getScheduleUpdateInnerData(updateNonce); + + ArbitrumTimelock(l2CoreGovTimelock).schedule({ + target: to, // ArbSys address - this will trigger a call from L2->L1 + value: 0, + // call to ArbSys.sendTxToL1; target the L1 timelock with the calldata previously constructed + data: data, + predecessor: bytes32(0), + // must be unique as the proposal hash is used for replay protection in the L2 timelock + // we cant be sure another proposal wont use this salt, and the same target + data + // but in that case the proposal will do what we want it to do anyway + // this can however block the execution of the election - so in this case the + // Security Council would need to unblock it by setting the election to executed state + // in the Member Election governor + salt: this.generateSalt(newMembers, updateNonce), + delay: ArbitrumTimelock(l2CoreGovTimelock).getMinDelay() + }); + } + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + + uint256[43] private __gap; +} diff --git a/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol b/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol new file mode 100644 index 00000000..f67be7f3 --- /dev/null +++ b/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "./interfaces/IGnosisSafe.sol"; +import "./SecurityCouncilMgmtUtils.sol"; +import "../gov-action-contracts/execution-record/ActionExecutionRecord.sol"; + +/// @notice Action contract for updating security council members. Used by the security council management system. +/// Expected to be delegate called into by an Upgrade Executor +contract SecurityCouncilMemberSyncAction is ActionExecutionRecord { + error PreviousOwnerNotFound(address targetOwner, address securityCouncil); + error ExecFromModuleError(bytes data, address securityCouncil); + + event UpdateNonceTooLow( + address indexed securityCouncil, uint256 currrentNonce, uint256 providedNonce + ); + + /// @dev Used in the gnosis safe as the first entry in their ownership linked list + address public constant SENTINEL_OWNERS = address(0x1); + + constructor(KeyValueStore _store) + ActionExecutionRecord(_store, "SecurityCouncilMemberSyncAction") + {} + + /// @notice Updates members of security council multisig to match provided array + /// @dev This function contains O(n^2) operations, so doesnt scale for large numbers of members. Expected count is 12, which is acceptable. + /// Gnosis OwnerManager handles reverting if address(0) is passed to remove/add owner + /// @param _securityCouncil The security council to update + /// @param _updatedMembers The new list of members. The Security Council will be updated to have this exact list of members + /// @return res indicates whether an update took place + function perform(address _securityCouncil, address[] memory _updatedMembers, uint256 _nonce) + external + returns (bool res) + { + // make sure that _nonce is greater than the last nonce + // we do this to ensure that a previous update does not occur after a later one + // the mechanism just checks greater, not n+1, because the Security Council Manager always + // sends the latest full list of members so it doesn't matter if some updates are missed + // Additionally a retryable ticket could be used to execute the update, and since tickets + // expire if not executed after some time, then allowing updates to be skipped means that the + // system will not be blocked if a retryable ticket is expires + uint256 updateNonce = getUpdateNonce(_securityCouncil); + if (_nonce <= updateNonce) { + // when nonce is too now, we simply return, we don't revert. + // this way an out of date update will actual execute, rather than remaining in an unexecuted state forever + emit UpdateNonceTooLow(_securityCouncil, updateNonce, _nonce); + return false; + } + + // store the nonce as a record of execution + // use security council as the key to ensure that updates to different security councils are kept separate + _setUpdateNonce(_securityCouncil, _nonce); + + IGnosisSafe securityCouncil = IGnosisSafe(_securityCouncil); + // preserve current threshold, the safe ensures that the threshold is never lower than the member count + uint256 threshold = securityCouncil.getThreshold(); + + address[] memory previousOwners = securityCouncil.getOwners(); + + for (uint256 i = 0; i < _updatedMembers.length; i++) { + address member = _updatedMembers[i]; + if (!securityCouncil.isOwner(member)) { + _addMember(securityCouncil, member, threshold); + } + } + + for (uint256 i = 0; i < previousOwners.length; i++) { + address owner = previousOwners[i]; + if (!SecurityCouncilMgmtUtils.isInArray(owner, _updatedMembers)) { + _removeMember(securityCouncil, owner, threshold); + } + } + return true; + } + + function _addMember(IGnosisSafe securityCouncil, address _member, uint256 _threshold) + internal + { + _execFromModule( + securityCouncil, + abi.encodeWithSelector(IGnosisSafe.addOwnerWithThreshold.selector, _member, _threshold) + ); + } + + function _removeMember(IGnosisSafe securityCouncil, address _member, uint256 _threshold) + internal + { + address previousOwner = getPrevOwner(securityCouncil, _member); + _execFromModule( + securityCouncil, + abi.encodeWithSelector( + IGnosisSafe.removeOwner.selector, previousOwner, _member, _threshold + ) + ); + } + + function getPrevOwner(IGnosisSafe securityCouncil, address _owner) + public + view + returns (address) + { + // owners are stored as a linked list and removal requires the previous owner + address[] memory owners = securityCouncil.getOwners(); + address previousOwner = SENTINEL_OWNERS; + for (uint256 i = 0; i < owners.length; i++) { + address currentOwner = owners[i]; + if (currentOwner == _owner) { + return previousOwner; + } + previousOwner = currentOwner; + } + revert PreviousOwnerNotFound({ + targetOwner: _owner, + securityCouncil: address(securityCouncil) + }); + } + + function getUpdateNonce(address securityCouncil) public view returns (uint256) { + return _get(uint160(securityCouncil)); + } + + function _setUpdateNonce(address securityCouncil, uint256 nonce) internal { + _set(uint160(securityCouncil), nonce); + } + + /// @notice Execute provided operation via gnosis safe's trusted execTransactionFromModule entry point + function _execFromModule(IGnosisSafe securityCouncil, bytes memory data) internal { + if ( + !securityCouncil.execTransactionFromModule( + address(securityCouncil), 0, data, OpEnum.Operation.Call + ) + ) { + revert ExecFromModuleError({data: data, securityCouncil: address(securityCouncil)}); + } + } +} diff --git a/src/security-council-mgmt/SecurityCouncilMgmtUtils.sol b/src/security-council-mgmt/SecurityCouncilMgmtUtils.sol new file mode 100644 index 00000000..aa1e6a6a --- /dev/null +++ b/src/security-council-mgmt/SecurityCouncilMgmtUtils.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +library SecurityCouncilMgmtUtils { + function isInArray(address addr, address[] memory arr) internal pure returns (bool) { + for (uint256 i = 0; i < arr.length; i++) { + if (arr[i] == addr) { + return true; + } + } + return false; + } + + // filters an array of addresses by removing any addresses that are in the excludeList + function filterAddressesWithExcludeList( + address[] memory input, + mapping(address => bool) storage excludeList + ) internal view returns (address[] memory) { + address[] memory intermediate = new address[](input.length); + uint256 intermediateLength = 0; + + for (uint256 i = 0; i < input.length; i++) { + address nominee = input[i]; + if (!excludeList[nominee]) { + intermediate[intermediateLength] = nominee; + intermediateLength++; + } + } + + address[] memory output = new address[](intermediateLength); + for (uint256 i = 0; i < intermediateLength; i++) { + output[i] = intermediate[i]; + } + + return output; + } +} diff --git a/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol b/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol new file mode 100644 index 00000000..026158f0 --- /dev/null +++ b/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "../governors/SecurityCouncilMemberElectionGovernor.sol"; +import "../governors/SecurityCouncilNomineeElectionGovernor.sol"; +import "../SecurityCouncilManager.sol"; +import "../governors/SecurityCouncilMemberRemovalGovernor.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "../interfaces/ISecurityCouncilManager.sol"; +import "../interfaces/IGnosisSafe.sol"; +import "../../ArbitrumTimelock.sol"; +import "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; +import "../../UpgradeExecRouteBuilder.sol"; +import "../Common.sol"; + +struct DeployParams { + ChainAndUpExecLocation[] upgradeExecutors; + address govChainEmergencySecurityCouncil; + address l1ArbitrumTimelock; + address l2CoreGovTimelock; + address govChainProxyAdmin; + address[] secondCohort; + address[] firstCohort; + address l2UpgradeExecutor; + address arbToken; + uint256 l1TimelockMinDelay; + uint256 removalGovVotingDelay; + uint256 removalGovVotingPeriod; + uint256 removalGovQuorumNumerator; + uint256 removalGovProposalThreshold; + uint256 removalGovVoteSuccessNumerator; + uint64 removalGovMinPeriodAfterQuorum; + uint256 removalProposalExpirationBlocks; + SecurityCouncilData[] securityCouncils; + Date firstNominationStartDate; + uint256 nomineeVettingDuration; + address nomineeVetter; + uint256 nomineeQuorumNumerator; + uint256 nomineeVotingPeriod; + uint256 memberVotingPeriod; + uint256 fullWeightDuration; +} + +struct ContractImplementations { + address nomineeElectionGovernor; + address memberElectionGovernor; + address securityCouncilManager; + address securityCouncilMemberRemoverGov; +} + +/// @notice Factory for deploying L2 Security Council management contracts. +/// Prerequisites: core Arb DAO governance contracts, and a SecurityCouncilMemberSyncAction deployed for each governed security council (on each corresponding chain) +contract L2SecurityCouncilMgmtFactory is Ownable { + event ContractsDeployed(DeployedContracts deployedContracts); + + // contracts deployed by factory + struct DeployedContracts { + SecurityCouncilNomineeElectionGovernor nomineeElectionGovernor; + SecurityCouncilMemberElectionGovernor memberElectionGovernor; + ISecurityCouncilManager securityCouncilManager; + SecurityCouncilMemberRemovalGovernor securityCouncilMemberRemoverGov; + UpgradeExecRouteBuilder upgradeExecRouteBuilder; + } + + error AddressNotInCouncil(address[] securityCouncil, address account); + error InvalidCohortsSize(uint256 councilSize, uint256 firstCohortSize, uint256 secondCohortSize); + + /// @dev deploys a transparent proxy for the given implementation contract + function _deployProxy(address proxyAdmin, address impl, bytes memory initData) + internal + returns (address) + { + return address(new TransparentUpgradeableProxy(impl, proxyAdmin, initData)); + } + + /// @notice deploys the L2 Security Council management contracts + /// @param dp the deployment parameters + /// @param impls contract implementations to deploy proxies for + function deploy(DeployParams memory dp, ContractImplementations memory impls) + external + onlyOwner + returns (DeployedContracts memory) + { + if (!Address.isContract(dp.govChainEmergencySecurityCouncil)) { + revert NotAContract(dp.govChainEmergencySecurityCouncil); + } + + if (!Address.isContract(dp.govChainProxyAdmin)) { + revert NotAContract(dp.govChainProxyAdmin); + } + + if (!Address.isContract(dp.l2UpgradeExecutor)) { + revert NotAContract(dp.l2UpgradeExecutor); + } + + if (!Address.isContract(dp.arbToken)) { + revert NotAContract(dp.arbToken); + } + + if (dp.nomineeVetter == address(0)) { + revert ZeroAddress(); + } + IGnosisSafe govChainEmergencySCSafe = IGnosisSafe(dp.govChainEmergencySecurityCouncil); + address[] memory owners = govChainEmergencySCSafe.getOwners(); + if (owners.length != (dp.firstCohort.length + dp.secondCohort.length)) { + revert InvalidCohortsSize(owners.length, dp.firstCohort.length, dp.secondCohort.length); + } + + for (uint256 i = 0; i < dp.firstCohort.length; i++) { + if (!govChainEmergencySCSafe.isOwner(dp.firstCohort[i])) { + revert AddressNotInCouncil(owners, dp.firstCohort[i]); + } + } + + for (uint256 i = 0; i < dp.secondCohort.length; i++) { + if (!govChainEmergencySCSafe.isOwner(dp.secondCohort[i])) { + revert AddressNotInCouncil(owners, dp.secondCohort[i]); + } + } + + DeployedContracts memory deployedContracts; + + // deploy nominee election governor + deployedContracts.nomineeElectionGovernor = SecurityCouncilNomineeElectionGovernor( + payable(_deployProxy(dp.govChainProxyAdmin, impls.nomineeElectionGovernor, "")) + ); + + // deploy member election governor + deployedContracts.memberElectionGovernor = SecurityCouncilMemberElectionGovernor( + payable(_deployProxy(dp.govChainProxyAdmin, impls.memberElectionGovernor, "")) + ); + + // deploy security council manager + deployedContracts.securityCouncilManager = SecurityCouncilManager( + _deployProxy(dp.govChainProxyAdmin, impls.securityCouncilManager, "") + ); + + // deploy security council member remover gov + deployedContracts.securityCouncilMemberRemoverGov = SecurityCouncilMemberRemovalGovernor( + payable(_deployProxy(dp.govChainProxyAdmin, impls.securityCouncilMemberRemoverGov, "")) + ); + + // create council manager roles + address[] memory memberRemovers = new address[](2); + memberRemovers[0] = address(deployedContracts.securityCouncilMemberRemoverGov); + memberRemovers[1] = dp.govChainEmergencySecurityCouncil; + SecurityCouncilManagerRoles memory roles = SecurityCouncilManagerRoles({ + admin: dp.l2UpgradeExecutor, + cohortUpdator: address(deployedContracts.memberElectionGovernor), + memberAdder: dp.govChainEmergencySecurityCouncil, + memberRemovers: memberRemovers, + memberRotator: dp.govChainEmergencySecurityCouncil, + memberReplacer: dp.govChainEmergencySecurityCouncil + }); + + deployedContracts.upgradeExecRouteBuilder = new UpgradeExecRouteBuilder({ + _upgradeExecutors: dp.upgradeExecutors, + _l1ArbitrumTimelock: dp.l1ArbitrumTimelock, + _l1TimelockMinDelay: dp.l1TimelockMinDelay + }); + + // init the deployed contracts + _initElectionGovernors( + dp, + deployedContracts.securityCouncilManager, + deployedContracts.nomineeElectionGovernor, + deployedContracts.memberElectionGovernor + ); + + deployedContracts.securityCouncilManager.initialize({ + _firstCohort: dp.firstCohort, + _secondCohort: dp.secondCohort, + _securityCouncils: dp.securityCouncils, + _roles: roles, + _l2CoreGovTimelock: payable(dp.l2CoreGovTimelock), + _router: deployedContracts.upgradeExecRouteBuilder + }); + + _initRemovalGov( + dp, + deployedContracts.securityCouncilManager, + deployedContracts.securityCouncilMemberRemoverGov + ); + + emit ContractsDeployed(deployedContracts); + return deployedContracts; + } + + /// @dev initializes the removal governor + function _initRemovalGov( + DeployParams memory dp, + ISecurityCouncilManager _securityCouncilManager, + SecurityCouncilMemberRemovalGovernor securityCouncilMemberRemoverGov + ) internal { + securityCouncilMemberRemoverGov.initialize({ + _securityCouncilManager: _securityCouncilManager, + _voteSuccessNumerator: dp.removalGovVoteSuccessNumerator, + _token: IVotesUpgradeable(dp.arbToken), + _owner: dp.l2UpgradeExecutor, + _votingDelay: dp.removalGovVotingDelay, + _votingPeriod: dp.removalGovVotingPeriod, + _quorumNumerator: dp.removalGovQuorumNumerator, + _proposalThreshold: dp.removalGovProposalThreshold, + _minPeriodAfterQuorum: dp.removalGovMinPeriodAfterQuorum, + _proposalExpirationBlocks: dp.removalProposalExpirationBlocks + }); + } + + /// @dev initializes the election governors + function _initElectionGovernors( + DeployParams memory dp, + ISecurityCouncilManager securityCouncilManager, + SecurityCouncilNomineeElectionGovernor nomineeElectionGovernor, + SecurityCouncilMemberElectionGovernor memberElectionGovernor + ) internal { + nomineeElectionGovernor.initialize( + SecurityCouncilNomineeElectionGovernor.InitParams({ + firstNominationStartDate: dp.firstNominationStartDate, + nomineeVettingDuration: dp.nomineeVettingDuration, + nomineeVetter: dp.nomineeVetter, + securityCouncilManager: securityCouncilManager, + securityCouncilMemberElectionGovernor: memberElectionGovernor, + token: IVotesUpgradeable(dp.arbToken), + owner: dp.l2UpgradeExecutor, + quorumNumeratorValue: dp.nomineeQuorumNumerator, + votingPeriod: dp.nomineeVotingPeriod + }) + ); + + memberElectionGovernor.initialize({ + _nomineeElectionGovernor: nomineeElectionGovernor, + _securityCouncilManager: securityCouncilManager, + _token: IVotesUpgradeable(dp.arbToken), + _owner: dp.l2UpgradeExecutor, + _votingPeriod: dp.memberVotingPeriod, + _fullWeightDuration: dp.fullWeightDuration + }); + } +} diff --git a/src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol b/src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol new file mode 100644 index 00000000..768792d3 --- /dev/null +++ b/src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol"; +import "../interfaces/ISecurityCouncilMemberElectionGovernor.sol"; +import "../interfaces/ISecurityCouncilNomineeElectionGovernor.sol"; +import "../interfaces/ISecurityCouncilManager.sol"; +import "./modules/ElectionGovernor.sol"; + +/// @title SecurityCouncilMemberElectionGovernor +/// @notice Narrows a set of nominees down to a set of members. +/// @dev Proposals are created by the SecurityCouncilNomineeElectionGovernor. +/// This governor is responsible for executing the final election result by calling the SecurityCouncilManager. +contract SecurityCouncilMemberElectionGovernor is + Initializable, + GovernorUpgradeable, + GovernorVotesUpgradeable, + SecurityCouncilMemberElectionGovernorCountingUpgradeable, + GovernorSettingsUpgradeable, + OwnableUpgradeable, + ElectionGovernor, + ISecurityCouncilMemberElectionGovernor +{ + /// @notice The SecurityCouncilNomineeElectionGovernor that creates proposals for this governor and contains the list of compliant nominees + ISecurityCouncilNomineeElectionGovernor public nomineeElectionGovernor; + + /// @notice The SecurityCouncilManager that will execute the election result + ISecurityCouncilManager public securityCouncilManager; + + error InvalidDurations(uint256 fullWeightDuration, uint256 votingPeriod); + error OnlyNomineeElectionGovernor(); + error ProposeDisabled(); + error CastVoteDisabled(); + + constructor() { + _disableInitializers(); + } + + /// @param _nomineeElectionGovernor The SecurityCouncilNomineeElectionGovernor + /// @param _securityCouncilManager The SecurityCouncilManager + /// @param _token The token used for voting + /// @param _owner The owner of the governor + /// @param _votingPeriod The duration of voting on a proposal + /// @param _fullWeightDuration Duration of full weight voting (blocks) + function initialize( + ISecurityCouncilNomineeElectionGovernor _nomineeElectionGovernor, + ISecurityCouncilManager _securityCouncilManager, + IVotesUpgradeable _token, + address _owner, + uint256 _votingPeriod, + uint256 _fullWeightDuration + ) public initializer { + if (_fullWeightDuration > _votingPeriod) { + revert InvalidDurations(_fullWeightDuration, _votingPeriod); + } + + __Governor_init("SecurityCouncilMemberElectionGovernor"); + __GovernorVotes_init(_token); + __SecurityCouncilMemberElectionGovernorCounting_init({ + initialFullWeightDuration: _fullWeightDuration + }); + __GovernorSettings_init(0, _votingPeriod, 0); + _transferOwnership(_owner); + + if (!Address.isContract(address(_nomineeElectionGovernor))) { + revert NotAContract(address(_nomineeElectionGovernor)); + } + nomineeElectionGovernor = _nomineeElectionGovernor; + if (!Address.isContract(address(_securityCouncilManager))) { + revert NotAContract(address(_securityCouncilManager)); + } + securityCouncilManager = _securityCouncilManager; + } + + modifier onlyNomineeElectionGovernor() { + if (msg.sender != address(nomineeElectionGovernor)) { + revert OnlyNomineeElectionGovernor(); + } + _; + } + + /// @inheritdoc ISecurityCouncilMemberElectionGovernor + function proposeFromNomineeElectionGovernor(uint256 electionIndex) + external + onlyNomineeElectionGovernor + returns (uint256) + { + // we use the same getProposeArgs to ensure the proposal id is consistent across governors + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory callDatas, + string memory description + ) = getProposeArgs(electionIndex); + return GovernorUpgradeable.propose(targets, values, callDatas, description); + } + + /// @notice Allows the owner to make calls from the governor + /// @dev See {L2ArbitrumGovernor-relay} + function relay(address target, uint256 value, bytes calldata data) + external + virtual + override + onlyOwner + { + AddressUpgradeable.functionCallWithValue(target, data, value); + } + + /// @dev `GovernorUpgradeable` function to execute a proposal overridden to handle member elections. + /// We know that topNominees() will return a full list. + /// Calls `SecurityCouncilManager.replaceCohort` with the list of nominees. + function _execute( + uint256 proposalId, + address[] memory, /* targets */ + uint256[] memory, /* values */ + bytes[] memory callDatas, + bytes32 /* descriptionHash */ + ) internal override { + // we know that the election index is part of the calldatas + uint256 electionIndex = extractElectionIndex(callDatas); + + // it's possible for this call to fail because of checks in the security council manager + // getting into a state inconsistent with the elections, if it does the Security Council + // will need to update the Manager so that this replaceCohort can go through + // Otherwise this and future elections will remain blocked. + securityCouncilManager.replaceCohort({ + _newCohort: topNominees(proposalId), + _cohort: electionIndexToCohort(electionIndex) + }); + } + + /// @notice Normally "the number of votes required in order for a voter to become a proposer." But in our case it is 0. + /// @dev Since we only want proposals to be created via `proposeFromNomineeElectionGovernor`, we set the proposal threshold to 0. + /// `proposeFromNomineeElectionGovernor` determines the rules for creating a proposal. + function proposalThreshold() + public + pure + override(GovernorSettingsUpgradeable, GovernorUpgradeable) + returns (uint256) + { + return 0; + } + + /// @notice Quorum is always 0. + function quorum(uint256) public pure override returns (uint256) { + return 0; + } + + /// @dev Whether the account is a compliant nominee. + /// checks the SecurityCouncilNomineeElectionGovernor to see if the account is a compliant nominee + function _isCompliantNominee(uint256 proposalId, address possibleNominee) + internal + view + override + returns (bool) + { + return nomineeElectionGovernor.isCompliantNominee(proposalId, possibleNominee); + } + + /// @dev Returns all the compliant (non excluded) nominees for the requested proposal + function _compliantNominees(uint256 proposalId) + internal + view + override + returns (address[] memory) + { + return nomineeElectionGovernor.compliantNominees(proposalId); + } + + /// @inheritdoc SecurityCouncilMemberElectionGovernorCountingUpgradeable + function _targetMemberCount() internal view override returns (uint256) { + return securityCouncilManager.cohortSize(); + } + + /// @notice Always reverts. + /// @dev `GovernorUpgradeable` function to create a proposal overridden to just revert. + /// We only want proposals to be created via `proposeFromNomineeElectionGovernor`. + function propose(address[] memory, uint256[] memory, bytes[] memory, string memory) + public + virtual + override + returns (uint256) + { + revert ProposeDisabled(); + } + + /// @notice Always reverts. Use castVoteWithReasonAndParams instead + function castVote(uint256, uint8) public virtual override returns (uint256) { + revert CastVoteDisabled(); + } + + /// @notice Always reverts. Use castVoteWithReasonAndParams instead + function castVoteWithReason(uint256, uint8, string calldata) + public + virtual + override + returns (uint256) + { + revert CastVoteDisabled(); + } + + /// @notice Always reverts. Use castVoteWithReasonAndParamsBySig instead + function castVoteBySig(uint256, uint8, uint8, bytes32, bytes32) + public + virtual + override + returns (uint256) + { + revert CastVoteDisabled(); + } + + /// @inheritdoc ElectionGovernor + function castVoteWithReasonAndParamsBySig( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override(GovernorUpgradeable, ElectionGovernor) returns (uint256) { + return ElectionGovernor.castVoteWithReasonAndParamsBySig( + proposalId, support, reason, params, v, r, s + ); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[48] private __gap; +} diff --git a/src/security-council-mgmt/governors/SecurityCouncilMemberRemovalGovernor.sol b/src/security-council-mgmt/governors/SecurityCouncilMemberRemovalGovernor.sol new file mode 100644 index 00000000..3c14d744 --- /dev/null +++ b/src/security-council-mgmt/governors/SecurityCouncilMemberRemovalGovernor.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import + "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorPreventLateQuorumUpgradeable.sol"; +import + "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorCountingSimpleUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./../interfaces/ISecurityCouncilManager.sol"; +import "../Common.sol"; +import "./modules/ArbitrumGovernorVotesQuorumFractionUpgradeable.sol"; +import "./modules/ArbitrumGovernorProposalExpirationUpgradeable.sol"; + +/// @title SecurityCouncilMemberRemovalGovernor +/// @notice Allows the DAO to remove a security council member +contract SecurityCouncilMemberRemovalGovernor is + Initializable, + GovernorUpgradeable, + GovernorVotesUpgradeable, + GovernorPreventLateQuorumUpgradeable, + GovernorCountingSimpleUpgradeable, + ArbitrumGovernorVotesQuorumFractionUpgradeable, + GovernorSettingsUpgradeable, + ArbitrumGovernorProposalExpirationUpgradeable, + OwnableUpgradeable +{ + uint256 public constant VOTE_SUCCESS_DENOMINATOR = 10_000; + uint256 public voteSuccessNumerator; + ISecurityCouncilManager public securityCouncilManager; + + event VoteSuccessNumeratorSet(uint256 indexed voteSuccessNumerator); + event MemberRemovalProposed(address memberToRemove, string description); + + error InvalidOperationsLength(uint256 len); + error TargetNotManager(address target); + error ValueNotZero(uint256 value); + error UnexpectedCalldataLength(uint256 len); + error CallNotRemoveMember(bytes4 selector, bytes4 expectedSelector); + error MemberNotFound(address memberToRemove); + error AbstainDisallowed(); + error InvalidVoteSuccessNumerator(uint256 voteSuccessNumerator); + + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the contract + /// @param _voteSuccessNumerator value that with denominator 10_000 determines the ration of for/against votes required for success + /// @param _securityCouncilManager security council manager contract + /// @param _token The address of the governance token + /// @param _owner The DAO (Upgrade Executor); admin over proposal role + /// @param _votingDelay The delay between a proposal submission and voting starts + /// @param _votingPeriod The period for which the vote lasts + /// @param _quorumNumerator The proportion of the circulating supply required to reach a quorum + /// @param _proposalThreshold The number of delegated votes required to create a proposal + /// @param _minPeriodAfterQuorum The minimum number of blocks available for voting after the quorum is reached + function initialize( + uint256 _voteSuccessNumerator, + ISecurityCouncilManager _securityCouncilManager, + IVotesUpgradeable _token, + address _owner, + uint256 _votingDelay, + uint256 _votingPeriod, + uint256 _quorumNumerator, + uint256 _proposalThreshold, + uint64 _minPeriodAfterQuorum, + uint256 _proposalExpirationBlocks + ) public initializer { + __Governor_init("SecurityCouncilMemberRemovalGovernor"); + __GovernorSettings_init(_votingDelay, _votingPeriod, _proposalThreshold); + __GovernorCountingSimple_init(); + __GovernorVotes_init(_token); + __ArbitrumGovernorVotesQuorumFraction_init(_quorumNumerator); + __GovernorPreventLateQuorum_init(_minPeriodAfterQuorum); + __ArbitrumGovernorProposalExpirationUpgradeable_init(_proposalExpirationBlocks); + _transferOwnership(_owner); + + if (!Address.isContract(address(_securityCouncilManager))) { + revert NotAContract(address(_securityCouncilManager)); + } + + securityCouncilManager = _securityCouncilManager; + _setVoteSuccessNumerator(_voteSuccessNumerator); + } + + /// @notice Assumes the passed in bytes is an abi encoded function call, and splits into the selector and the rest + /// @param calldataWithSelector The call data to split + /// @return The selector + /// @return The rest of the function call data + function separateSelector(bytes calldata calldataWithSelector) + external + pure + returns (bytes4, bytes memory) + { + bytes4 selector = bytes4(calldataWithSelector[:4]); + bytes memory rest = calldataWithSelector[4:]; + return (selector, rest); + } + + /// @notice Propose a security council member removal. Method conforms to the governor propose interface + /// but enforces that only calls to securityCouncilManager's removeMember can be proposed. + /// @param targets Target contract operation; must be [securityCouncilManager] + /// @param values Value for removeMember; must be [0] + /// @param calldatas Operation calldata; must be [removeMember with address argument] + /// @param description rationale for member removal + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) public override returns (uint256) { + if (targets.length != 1) { + revert InvalidOperationsLength(targets.length); + } + // length equality of targets, values, and calldatas is checked in GovernorUpgradeable.propose + + if (targets[0] != address(securityCouncilManager)) { + revert TargetNotManager(targets[0]); + } + if (values[0] != 0) { + revert ValueNotZero(values[0]); + } + // selector + 1 word to hold the address + if (calldatas[0].length != 36) { + revert UnexpectedCalldataLength(calldatas[0].length); + } + + (bytes4 selector, bytes memory rest) = this.separateSelector(calldatas[0]); + if (selector != ISecurityCouncilManager.removeMember.selector) { + revert CallNotRemoveMember(selector, ISecurityCouncilManager.removeMember.selector); + } + + address memberToRemove = abi.decode(rest, (address)); + if ( + !securityCouncilManager.firstCohortIncludes(memberToRemove) + && !securityCouncilManager.secondCohortIncludes(memberToRemove) + ) { + revert MemberNotFound(memberToRemove); + } + + emit MemberRemovalProposed(memberToRemove, description); + return GovernorUpgradeable.propose(targets, values, calldatas, description); + } + + /// @notice override to allow for required vote success ratio that isn't 0.5 + /// @param proposalId target proposal id + function _voteSucceeded(uint256 proposalId) + internal + view + virtual + override(GovernorCountingSimpleUpgradeable, GovernorUpgradeable) + returns (bool) + { + (uint256 againstVotes, uint256 forVotes,) = proposalVotes(proposalId); + + // for-votes / total-votes >= success-numerator/ success-denominator + return + VOTE_SUCCESS_DENOMINATOR * forVotes >= (forVotes + againstVotes) * voteSuccessNumerator; + } + + /// @notice A removal proposal if a threshold of all cast votes vote in favor of removal. + /// Thus, abstaining would be exactly equivalent to voting against. + /// To prevent any confusion, abstaining is disallowed. + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) internal virtual override(GovernorCountingSimpleUpgradeable, GovernorUpgradeable) { + if (VoteType(support) == VoteType.Abstain) { + revert AbstainDisallowed(); + } + GovernorCountingSimpleUpgradeable._countVote(proposalId, account, support, weight, params); + } + + /// @notice Set numerator for removal vote to succeed; only DAO can call + /// @param _voteSuccessNumerator new numerator value + function setVoteSuccessNumerator(uint256 _voteSuccessNumerator) public onlyOwner { + _setVoteSuccessNumerator(_voteSuccessNumerator); + } + + function _setVoteSuccessNumerator(uint256 _voteSuccessNumerator) internal { + if (!(0 < _voteSuccessNumerator && _voteSuccessNumerator <= VOTE_SUCCESS_DENOMINATOR)) { + revert InvalidVoteSuccessNumerator(_voteSuccessNumerator); + } + voteSuccessNumerator = _voteSuccessNumerator; + emit VoteSuccessNumeratorSet(_voteSuccessNumerator); + } + + /// @inheritdoc IGovernorUpgradeable + function COUNTING_MODE() + public + pure + virtual + override(GovernorCountingSimpleUpgradeable, IGovernorUpgradeable) + returns (string memory) + { + return "support=for,against&quorum=for"; + } + + /// @notice Allows the owner to make calls from the governor + /// @dev See {L2ArbitrumGovernor-relay} + function relay(address target, uint256 value, bytes calldata data) + external + virtual + override + onlyOwner + { + AddressUpgradeable.functionCallWithValue(target, data, value); + } + + /// @inheritdoc GovernorUpgradeable + function proposalThreshold() + public + view + override(GovernorUpgradeable, GovernorSettingsUpgradeable) + returns (uint256) + { + return GovernorSettingsUpgradeable.proposalThreshold(); + } + + /// @inheritdoc GovernorPreventLateQuorumUpgradeable + function _castVote( + uint256 proposalId, + address account, + uint8 support, + string memory reason, + bytes memory params + ) + internal + override(GovernorUpgradeable, GovernorPreventLateQuorumUpgradeable) + returns (uint256) + { + return GovernorPreventLateQuorumUpgradeable._castVote( + proposalId, account, support, reason, params + ); + } + + /// @inheritdoc GovernorPreventLateQuorumUpgradeable + function proposalDeadline(uint256 proposalId) + public + view + override(GovernorUpgradeable, GovernorPreventLateQuorumUpgradeable) + returns (uint256) + { + return GovernorPreventLateQuorumUpgradeable.proposalDeadline(proposalId); + } + + function state(uint256 proposalId) + public + view + override(ArbitrumGovernorProposalExpirationUpgradeable, GovernorUpgradeable) + returns (ProposalState) + { + // We override the state to transition to expire unexecuted proposals + // This is because of potential race conditions between the removal governor and other + // parties (normal elections + security council) calling the other functions on the security council manager + // The manager does some checks to ensure that the removal can occur (eg the account is a member) - if these checks fail + // then the proposal in here will fail. If this is the case we want the proposal to expire + // after some time so that it doesnt become executable (in an unexpected way) far in the future due + // to normal changes in the security council membership. + return ArbitrumGovernorProposalExpirationUpgradeable.state(proposalId); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[48] private __gap; +} diff --git a/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol b/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol new file mode 100644 index 00000000..d1e161ee --- /dev/null +++ b/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorSettingsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../interfaces/ISecurityCouncilMemberElectionGovernor.sol"; +import "../interfaces/ISecurityCouncilNomineeElectionGovernor.sol"; +import "./modules/SecurityCouncilNomineeElectionGovernorCountingUpgradeable.sol"; +import "./modules/ArbitrumGovernorVotesQuorumFractionUpgradeable.sol"; +import "./modules/SecurityCouncilNomineeElectionGovernorTiming.sol"; +import "./modules/ElectionGovernor.sol"; +import "../SecurityCouncilMgmtUtils.sol"; + +/// @title SecurityCouncilNomineeElectionGovernor +/// @notice Governor contract for selecting Security Council Nominees (phase 1 of the Security Council election process). +contract SecurityCouncilNomineeElectionGovernor is + Initializable, + GovernorUpgradeable, + GovernorVotesUpgradeable, + SecurityCouncilNomineeElectionGovernorCountingUpgradeable, + ArbitrumGovernorVotesQuorumFractionUpgradeable, + GovernorSettingsUpgradeable, + OwnableUpgradeable, + SecurityCouncilNomineeElectionGovernorTiming, + ElectionGovernor, + ISecurityCouncilNomineeElectionGovernor +{ + /// @notice parameters for `initialize` + /// @param firstNominationStartDate First election start date + /// @param nomineeVettingDuration Duration of the nominee vetting period (expressed in blocks) + /// @param nomineeVetter Address of the nominee vetter + /// @param securityCouncilManager Security council manager contract + /// @param token Token used for voting + /// @param owner Owner of the governor (the Arbitrum DAO) + /// @param quorumNumeratorValue Numerator of the quorum fraction (0.2% = 20) + /// @param votingPeriod Duration of the voting period (expressed in blocks) + /// Note that the voting period + nominee vetting duration must be << than 6 months to ensure elections dont overlap + struct InitParams { + Date firstNominationStartDate; + uint256 nomineeVettingDuration; + address nomineeVetter; + ISecurityCouncilManager securityCouncilManager; + ISecurityCouncilMemberElectionGovernor securityCouncilMemberElectionGovernor; + IVotesUpgradeable token; + address owner; + uint256 quorumNumeratorValue; + uint256 votingPeriod; + } + + /// @notice Information about a nominee election + /// @param isContender Whether the account is a contender + /// @param isExcluded Whether the account has been excluded by the nomineeVetter + /// @param excludedNomineeCount The number of nominees that have been excluded by the nomineeVetter + struct ElectionInfo { + mapping(address => bool) isContender; + mapping(address => bool) isExcluded; + uint256 excludedNomineeCount; + } + + /// @notice Address responsible for blocking non compliant nominees + address public nomineeVetter; + + /// @notice Security council manager contract + /// @dev Used to execute the election result immediately if <= 6 compliant nominees are chosen + ISecurityCouncilManager public securityCouncilManager; + + /// @notice Security council member election governor contract + ISecurityCouncilMemberElectionGovernor public securityCouncilMemberElectionGovernor; + + /// @notice Number of elections created + uint256 public electionCount; + + /// @notice Maps proposalId to ElectionInfo + mapping(uint256 => ElectionInfo) internal _elections; + + event NomineeVetterChanged(address indexed oldNomineeVetter, address indexed newNomineeVetter); + event ContenderAdded(uint256 indexed proposalId, address indexed contender); + event NomineeExcluded(uint256 indexed proposalId, address indexed nominee); + + error OnlyNomineeVetter(); + error CreateTooEarly(uint256 blockTimestamp, uint256 startTime); + error AlreadyContender(address contender); + error ProposalNotActive(ProposalState state); + error AccountInOtherCohort(Cohort cohort, address account); + error ProposalNotSucceededState(ProposalState state); + error ProposalNotInVettingPeriod(uint256 blockNumber, uint256 vettingDeadline); + error NomineeAlreadyExcluded(address nominee); + error CompliantNomineeTargetHit(uint256 nomineeCount, uint256 expectedCount); + error ProposalInVettingPeriod(uint256 blockNumber, uint256 vettingDeadline); + error InsufficientCompliantNomineeCount(uint256 compliantNomineeCount, uint256 expectedCount); + error ProposeDisabled(); + error NotNominee(address nominee); + error ProposalIdMismatch(uint256 nomineeProposalId, uint256 memberProposalId); + error QuorumNumeratorTooLow(uint256 quorumNumeratorValue); + error CastVoteDisabled(); + error LastMemberElectionNotExecuted(uint256 prevProposalId); + + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the governor + function initialize(InitParams memory params) public initializer { + __Governor_init("SecurityCouncilNomineeElectionGovernor"); + __GovernorVotes_init(params.token); + __SecurityCouncilNomineeElectionGovernorCounting_init(); + __ArbitrumGovernorVotesQuorumFraction_init(params.quorumNumeratorValue); + __GovernorSettings_init(0, params.votingPeriod, 0); // votingDelay and proposalThreshold are set to 0 + __SecurityCouncilNomineeElectionGovernorTiming_init( + params.firstNominationStartDate, params.nomineeVettingDuration + ); + _transferOwnership(params.owner); + + nomineeVetter = params.nomineeVetter; + if (!Address.isContract(address(params.securityCouncilManager))) { + revert NotAContract(address(params.securityCouncilManager)); + } + securityCouncilManager = params.securityCouncilManager; + if (!Address.isContract(address(params.securityCouncilMemberElectionGovernor))) { + revert NotAContract(address(params.securityCouncilMemberElectionGovernor)); + } + securityCouncilMemberElectionGovernor = params.securityCouncilMemberElectionGovernor; + + // elsewhere we make assumptions that the number of nominees + // is not greater than 500 + // This value can still be updated via updateQuorumNumerator to a lower value + // if it is deemed ok, however we put a quick check here as a reminder + if ((quorumDenominator() / params.quorumNumeratorValue) > 500) { + revert QuorumNumeratorTooLow(params.quorumNumeratorValue); + } + } + + /// @notice Allows the nominee vetter to call certain functions + modifier onlyNomineeVetter() { + if (msg.sender != nomineeVetter) { + revert OnlyNomineeVetter(); + } + _; + } + + /// @notice Some operations can only be performed during the vetting period. + modifier onlyVettingPeriod(uint256 proposalId) { + // voting is over and the proposal must have succeeded, not active or executed + ProposalState state_ = state(proposalId); + if (state_ != ProposalState.Succeeded) { + revert ProposalNotSucceededState(state_); + } + + // the proposal must not have passed the vetting deadline + uint256 vettingDeadline = proposalVettingDeadline(proposalId); + if (block.number > vettingDeadline) { + revert ProposalNotInVettingPeriod(block.number, vettingDeadline); + } + + _; + } + + /// @notice Creates a new nominee election proposal. + /// Can be called by anyone every 6 months. + /// @return proposalId The id of the proposal + function createElection() external returns (uint256 proposalId) { + // require that the last member election has executed + _requireLastMemberElectionHasExecuted(); + + // each election has a deterministic start time + uint256 thisElectionStartTs = electionToTimestamp(electionCount); + if (block.timestamp < thisElectionStartTs) { + revert CreateTooEarly(block.timestamp, thisElectionStartTs); + } + + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory callDatas, + string memory description + ) = getProposeArgs(electionCount); + + proposalId = GovernorUpgradeable.propose(targets, values, callDatas, description); + + electionCount++; + } + + /// @dev Revert if the previous member election has not executed. + /// Ensures that there are no unexpected behaviors from multiple elections running at the same time. + /// If, for some reason, the previous member election is blocked, + /// it is up to the security council or DAO to unblock the previous election before creating a new one. + function _requireLastMemberElectionHasExecuted() internal view { + if (electionCount == 0) { + return; + } + + ( + address[] memory prevTargets, + uint256[] memory prevValues, + bytes[] memory prevCallDatas, + string memory prevDescription + ) = getProposeArgs(electionCount - 1); + + uint256 prevProposalId = + hashProposal(prevTargets, prevValues, prevCallDatas, keccak256(bytes(prevDescription))); + + if ( + IGovernorUpgradeable(address(securityCouncilMemberElectionGovernor)).state( + prevProposalId + ) != ProposalState.Executed + ) { + revert LastMemberElectionNotExecuted(prevProposalId); + } + } + + /// @notice Put `msg.sender` up for nomination. Must be called before a contender can receive votes. + /// Contenders are expected to control an address than can create a signature that would be a + /// recognised by a Gnosis Safe. They need to be able to do this with this same address on each of the + /// chains where the Security Council is active. It is expected that the nominee vetter will check this + /// during the vetting phase and exclude any contenders which dont meet this criteria. + /// @dev Can be called only while a proposal is active (in voting phase) + /// A contender cannot be a member of the opposite cohort. + function addContender(uint256 proposalId) external { + ElectionInfo storage election = _elections[proposalId]; + + if (election.isContender[msg.sender]) { + revert AlreadyContender(msg.sender); + } + + ProposalState state_ = state(proposalId); + if (state_ != ProposalState.Active) { + revert ProposalNotActive(state_); + } + + // check to make sure the contender is not part of the other cohort (the cohort not currently up for election) + // this only checks against the current the current other cohort, and against the current cohort membership + // in the security council, so changes to those will mean this check will be inconsistent. + // this check then is only a relevant check when the elections are running as expected - one at a time, + // every 6 months. Updates to the sec council manager using methods other than replaceCohort can effect this check + // and it's expected that the entity making those updates understands this. + if (securityCouncilManager.cohortIncludes(otherCohort(), msg.sender)) { + revert AccountInOtherCohort(otherCohort(), msg.sender); + } + + election.isContender[msg.sender] = true; + + emit ContenderAdded(proposalId, msg.sender); + } + + /// @notice Allows the owner to change the nomineeVetter + function setNomineeVetter(address _nomineeVetter) external onlyGovernance { + address oldNomineeVetter = nomineeVetter; + nomineeVetter = _nomineeVetter; + emit NomineeVetterChanged(oldNomineeVetter, _nomineeVetter); + } + + /// @notice Allows the owner to make calls from the governor + /// @dev See {L2ArbitrumGovernor-relay} + function relay(address target, uint256 value, bytes calldata data) + external + virtual + override + onlyOwner + { + AddressUpgradeable.functionCallWithValue(target, data, value); + } + + /// @notice Allows the nomineeVetter to exclude a noncompliant nominee. + /// @dev Can be called only after a nominee election proposal has "succeeded" (voting has ended) and before the nominee vetting period has ended. + /// Will revert if the provided account is not a nominee (had less than the required votes). + function excludeNominee(uint256 proposalId, address nominee) + external + onlyNomineeVetter + onlyVettingPeriod(proposalId) + { + ElectionInfo storage election = _elections[proposalId]; + if (election.isExcluded[nominee]) { + revert NomineeAlreadyExcluded(nominee); + } + if (!isNominee(proposalId, nominee)) { + revert NotNominee(nominee); + } + + election.isExcluded[nominee] = true; + election.excludedNomineeCount++; + + emit NomineeExcluded(proposalId, nominee); + } + + /// @notice Allows the nomineeVetter to explicitly include a nominee if there are fewer nominees than the target. + /// @dev Can be called only when a proposal is succeeded (voting has ended) and there are fewer compliant nominees than the target. + /// Will revert if the provided account is already a nominee + /// The Constitution must be followed adding nominees. For example this method can be used by the Foundation to add a + /// random member of the outgoing security council, if less than 6 members meet the threshold to become a nominee + function includeNominee(uint256 proposalId, address account) external onlyNomineeVetter { + if (account == address(0)) { + revert ZeroAddress(); + } + + ProposalState state_ = state(proposalId); + if (state_ != ProposalState.Succeeded) { + revert ProposalNotSucceededState(state_); + } + + if (isNominee(proposalId, account)) { + revert NomineeAlreadyAdded(account); + } + + uint256 cnCount = compliantNomineeCount(proposalId); + uint256 cohortSize = securityCouncilManager.cohortSize(); + if (cnCount >= cohortSize) { + revert CompliantNomineeTargetHit(cnCount, cohortSize); + } + + // can't include nominees from the other cohort (the cohort not currently up for election) + // this only checks against the current the current other cohort, and against the current cohort membership + // in the security council, so changes to those will mean this check will be inconsistent. + // this check then is only a relevant check when the elections are running as expected - one at a time, + // every 6 months. Updates to the sec council manager using methods other than replaceCohort can effect this check + // and it's expected that the entity making those updates understands this. + if (securityCouncilManager.cohortIncludes(otherCohort(), account)) { + revert AccountInOtherCohort(otherCohort(), account); + } + + _addNominee(proposalId, account); + } + + /// @dev `GovernorUpgradeable` function to execute a proposal overridden to handle nominee elections. + /// Can be called by anyone via `execute` after voting and nominee vetting periods have ended. + /// If the number of compliant nominees is > the target number of nominees, + /// we move on to the next phase by calling the SecurityCouncilMemberElectionGovernor. + /// @param proposalId The id of the proposal + function _execute( + uint256 proposalId, + address[] memory, /* targets */ + uint256[] memory, /* values */ + bytes[] memory callDatas, + bytes32 /*descriptionHash*/ + ) internal virtual override { + // we can only execute when the vetting deadline has passed + uint256 vettingDeadline = proposalVettingDeadline(proposalId); + if (block.number <= vettingDeadline) { + revert ProposalInVettingPeriod(block.number, vettingDeadline); + } + + uint256 cnCount = compliantNomineeCount(proposalId); + uint256 cohortSize = securityCouncilManager.cohortSize(); + if (cnCount < cohortSize) { + revert InsufficientCompliantNomineeCount(cnCount, cohortSize); + } + + uint256 electionIndex = extractElectionIndex(callDatas); + uint256 memberElectionProposalId = + securityCouncilMemberElectionGovernor.proposeFromNomineeElectionGovernor(electionIndex); + + // proposals in the member and nominee governors should have the same ids + // so we do a safety check here to ensure this is the case + if (memberElectionProposalId != proposalId) { + revert ProposalIdMismatch(proposalId, memberElectionProposalId); + } + } + + /// @notice Normally "the number of votes required in order for a voter to become a proposer." But in our case it is 0. + /// @dev Since we only want proposals to be created via `createElection`, we set the proposal threshold to 0. + /// `createElection` determines the rules for creating a proposal. + function proposalThreshold() + public + view + virtual + override(GovernorSettingsUpgradeable, GovernorUpgradeable) + returns (uint256) + { + return 0; + } + + /// @inheritdoc ISecurityCouncilNomineeElectionGovernor + function isCompliantNominee(uint256 proposalId, address account) public view returns (bool) { + return isNominee(proposalId, account) && !_elections[proposalId].isExcluded[account]; + } + + /// @inheritdoc ISecurityCouncilNomineeElectionGovernor + function compliantNominees(uint256 proposalId) public view returns (address[] memory) { + ElectionInfo storage election = _elections[proposalId]; + address[] memory maybeCompliantNominees = + SecurityCouncilNomineeElectionGovernorCountingUpgradeable.nominees(proposalId); + return SecurityCouncilMgmtUtils.filterAddressesWithExcludeList( + maybeCompliantNominees, election.isExcluded + ); + } + + /// @notice Current number of compliant nominees for the proposal + function compliantNomineeCount(uint256 proposalId) public view returns (uint256) { + return nomineeCount(proposalId) - _elections[proposalId].excludedNomineeCount; + } + + /// @notice Returns cohort currently up for election + /// @dev Between elections returns the value from the previous election + function currentCohort() public view returns (Cohort) { + // current cohort is at electionCount - 1 + return electionCount == 0 ? Cohort.FIRST : electionIndexToCohort(electionCount - 1); + } + + /// @notice Returns cohort not currently up for election + /// @dev Between elections returns the value from the previous election + function otherCohort() public view returns (Cohort) { + // previous cohort is at electionCount - 2 + return (electionCount < 2) ? Cohort.SECOND : electionIndexToCohort(electionCount - 2); + } + + /// @notice returns true if the nominee has been excluded by the nomineeVetter for the given proposal + function isExcluded(uint256 proposalId, address possibleExcluded) public view returns (bool) { + return _elections[proposalId].isExcluded[possibleExcluded]; + } + + /// @notice returns the number of excluded nominees for the given proposal + function excludedNomineeCount(uint256 proposalId) public view returns (uint256) { + return _elections[proposalId].excludedNomineeCount; + } + + /// @inheritdoc SecurityCouncilNomineeElectionGovernorCountingUpgradeable + function isContender(uint256 proposalId, address possibleContender) + public + view + virtual + override + returns (bool) + { + return _elections[proposalId].isContender[possibleContender]; + } + + /// @notice Always reverts. + /// @dev `GovernorUpgradeable` function to create a proposal overridden to just revert. + /// We only want proposals to be created via `createElection`. + function propose(address[] memory, uint256[] memory, bytes[] memory, string memory) + public + virtual + override + returns (uint256) + { + revert ProposeDisabled(); + } + + /// @notice Always reverts. Use castVoteWithReasonAndParams instead + function castVote(uint256, uint8) public virtual override returns (uint256) { + revert CastVoteDisabled(); + } + + /// @notice Always reverts. Use castVoteWithReasonAndParams instead + function castVoteWithReason(uint256, uint8, string calldata) + public + virtual + override + returns (uint256) + { + revert CastVoteDisabled(); + } + + /// @notice Always reverts. Use castVoteWithReasonAndParamsBySig instead + function castVoteBySig(uint256, uint8, uint8, bytes32, bytes32) + public + virtual + override + returns (uint256) + { + revert CastVoteDisabled(); + } + + /// @inheritdoc ElectionGovernor + function castVoteWithReasonAndParamsBySig( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override(GovernorUpgradeable, ElectionGovernor) returns (uint256) { + return ElectionGovernor.castVoteWithReasonAndParamsBySig( + proposalId, support, reason, params, v, r, s + ); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[45] private __gap; +} diff --git a/src/security-council-mgmt/governors/modules/ArbitrumGovernorProposalExpirationUpgradeable.sol b/src/security-council-mgmt/governors/modules/ArbitrumGovernorProposalExpirationUpgradeable.sol new file mode 100644 index 00000000..095ee7b8 --- /dev/null +++ b/src/security-council-mgmt/governors/modules/ArbitrumGovernorProposalExpirationUpgradeable.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; + +/// @title ArbitrumGovernorProposalExpirationUpgradeable +/// @notice GovernorUpgradeable whose proposals expire after a certain amount of time +/// Proposals that have succeeded transition to the Expired state after the expiration time +abstract contract ArbitrumGovernorProposalExpirationUpgradeable is + Initializable, + GovernorUpgradeable +{ + /// @notice The number of blocks after which a Succeeeded proposal transitions to Expired + uint256 public proposalExpirationBlocks; + + function __ArbitrumGovernorProposalExpirationUpgradeable_init(uint256 _proposalExpirationBlocks) + internal + onlyInitializing + { + __ArbitrumGovernorProposalExpirationUpgradeable_init_unchained(_proposalExpirationBlocks); + } + + function __ArbitrumGovernorProposalExpirationUpgradeable_init_unchained( + uint256 _proposalExpirationBlocks + ) internal onlyInitializing { + proposalExpirationBlocks = _proposalExpirationBlocks; + } + + /// @notice Returns the state of a proposal, given its id + /// @dev Overridden to return Expired if the proposal has succeeded but has expired + function state(uint256 proposalId) public view virtual override returns (ProposalState) { + ProposalState currentState = super.state(proposalId); + + if ( + currentState == ProposalState.Succeeded + && block.number > proposalExpirationDeadline(proposalId) + ) { + return ProposalState.Expired; + } + + return currentState; + } + + /// @notice The block at which the proposal expires + function proposalExpirationDeadline(uint256 proposalId) public view returns (uint256) { + return _proposalExpirationCountdownStart(proposalId) + proposalExpirationBlocks; + } + + /// @notice Returns the block number at which the proposal expiration countdown starts + function _proposalExpirationCountdownStart(uint256 proposalId) + internal + view + virtual + returns (uint256) + { + return proposalDeadline(proposalId); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/src/security-council-mgmt/governors/modules/ArbitrumGovernorVotesQuorumFractionUpgradeable.sol b/src/security-council-mgmt/governors/modules/ArbitrumGovernorVotesQuorumFractionUpgradeable.sol new file mode 100644 index 00000000..70c09598 --- /dev/null +++ b/src/security-council-mgmt/governors/modules/ArbitrumGovernorVotesQuorumFractionUpgradeable.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import + "@openzeppelin/contracts-upgradeable/governance/extensions/GovernorVotesQuorumFractionUpgradeable.sol"; + +/// @title ArbitrumGovernorVotesQuorumFractionUpgradeable +/// @notice GovernorVotesQuorumFractionUpgradeable with a quorum that excludes a special address +abstract contract ArbitrumGovernorVotesQuorumFractionUpgradeable is + Initializable, + GovernorVotesQuorumFractionUpgradeable +{ + /// @notice address for which votes will not be counted toward quorum + /// @dev A portion of the Arbitrum tokens will be held by entities (eg the treasury) that + /// are not eligible to vote. However, even if their voting/delegation is restricted their + /// tokens will still count towards the total supply, and will therefore affect the quorum. + /// Restricted addresses should be forced to delegate their votes to this special exclude + /// addresses which is not counted when calculating quorum + /// Example address that should be excluded: DAO treasury, foundation, unclaimed tokens, + /// burned tokens and swept (see TokenDistributor) tokens. + /// Note that Excluded Address is a readable name with no code or PK associated with it, and thus can't vote. + address public constant EXCLUDE_ADDRESS = address(0xA4b86); + + function __ArbitrumGovernorVotesQuorumFraction_init(uint256 quorumNumeratorValue) + internal + onlyInitializing + { + __GovernorVotesQuorumFraction_init(quorumNumeratorValue); + } + + /// @notice Get "circulating" votes supply; i.e., total minus excluded vote exclude address. + function getPastCirculatingSupply(uint256 blockNumber) public view virtual returns (uint256) { + return + token.getPastTotalSupply(blockNumber) - token.getPastVotes(EXCLUDE_ADDRESS, blockNumber); + } + + /// @notice Calculates the quorum size, excludes token delegated to the exclude address + function quorum(uint256 blockNumber) public view virtual override returns (uint256) { + return (getPastCirculatingSupply(blockNumber) * quorumNumerator(blockNumber)) + / quorumDenominator(); + } + + /// @inheritdoc GovernorVotesQuorumFractionUpgradeable + function quorumDenominator() public pure virtual override returns (uint256) { + // update to 10k to allow for higher precision + return 10_000; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/src/security-council-mgmt/governors/modules/ElectionGovernor.sol b/src/security-council-mgmt/governors/modules/ElectionGovernor.sol new file mode 100644 index 00000000..291ee222 --- /dev/null +++ b/src/security-council-mgmt/governors/modules/ElectionGovernor.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; +import "../../Common.sol"; + +/// @notice Common functionality used by nominee and member election governors +abstract contract ElectionGovernor is GovernorUpgradeable { + /// @notice When a vote is cast using a signature we store a hash of the vote data + /// so that the signature cannot be replayed + mapping(bytes32 => bool) public usedNonces; + + /// @notice The vote was already cast by the signer + /// @param voter The address that signed the vote + /// @param proposalId The proposal id for which this vote applies + /// @param replayHash The hash of the data that was signed + error VoteAlreadyCast(address voter, uint256 proposalId, bytes32 replayHash); + + /// @inheritdoc GovernorUpgradeable + /// @param reason Reason can be used as a nonce to ensure unique hashes when the same + /// votes wishes to vote the same way twice + function castVoteWithReasonAndParamsBySig( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override returns (uint256) { + bytes32 dataHash = _hashTypedDataV4( + keccak256( + abi.encode( + EXTENDED_BALLOT_TYPEHASH, + proposalId, + support, + keccak256(bytes(reason)), + keccak256(params) + ) + ) + ); + + address voter = ECDSAUpgradeable.recover(dataHash, v, r, s); + bytes32 replayHash = keccak256(bytes.concat(dataHash, bytes20(voter))); + + // ensure that the signature cannot be replayed by storing a nonce of the data + if (usedNonces[replayHash]) { + revert VoteAlreadyCast(voter, proposalId, replayHash); + } + usedNonces[replayHash] = true; + + return _castVote(proposalId, voter, support, reason, params); + } + + /// @notice Generate arguments to be passed to the governor propose function + /// @param electionIndex The index of the election to create a proposal for + /// @return Targets + /// @return Values + /// @return Calldatas + /// @return Description + function getProposeArgs(uint256 electionIndex) + public + pure + returns (address[] memory, uint256[] memory, bytes[] memory, string memory) + { + // encode the election index for later use + bytes[] memory electionData = new bytes[](1); + electionData[0] = abi.encode(electionIndex); + return ( + new address[](1), + new uint256[](1), + electionData, + electionIndexToDescription(electionIndex) + ); + } + + /// @notice Extract the election index from the call data + /// @param callDatas The proposal call data + function extractElectionIndex(bytes[] memory callDatas) internal pure returns (uint256) { + return abi.decode(callDatas[0], (uint256)); + } + + /// @notice Proposal descriptions are created deterministically from the election index + /// @param electionIndex The index of the election to create a proposal for + function electionIndexToDescription(uint256 electionIndex) + public + pure + returns (string memory) + { + return + string.concat("Security Council Election #", StringsUpgradeable.toString(electionIndex)); + } + + /// @notice Returns the cohort for a given `electionIndex` + function electionIndexToCohort(uint256 electionIndex) public pure returns (Cohort) { + return Cohort(electionIndex % 2); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol b/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol new file mode 100644 index 00000000..219a9dcd --- /dev/null +++ b/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; + +import "solady/utils/LibSort.sol"; + +/// @title SecurityCouncilMemberElectionGovernorCountingUpgradeable +/// @notice Counting module for the SecurityCouncilMemberElectionGovernor. +/// Voters can spread their votes across multiple nominees. +/// Implements linearly decreasing voting weights over time. +/// The `_targetMemberCount()` nominees with the most votes are selected as the winners. +abstract contract SecurityCouncilMemberElectionGovernorCountingUpgradeable is + Initializable, + GovernorUpgradeable +{ + struct ElectionInfo { + /// @dev The total votes used by a delegate. + mapping(address => uint256) votesUsed; + /// @dev The weight of votes received by a nominee. At the start of the election + /// each vote has weight 1, however after a cutoff point the weight of each + /// vote decreases linearly until it is 0 by the end of the election. + /// Using uint240 because of the sorting implementation, see `selectTopNominees` + mapping(address => uint240) weightReceived; + } + + /// @notice Duration of full weight voting (expressed in blocks) + uint256 public fullWeightDuration; + + /// @dev proposalId => ElectionInfo + mapping(uint256 => ElectionInfo) private _elections; + + /// @notice Emitted when a vote is cast for a nominee + /// @param voter The account that is casting the vote + /// @param proposalId The id of the proposal + /// @param nominee The nominee that is receiving the vote + /// @param votes The amount of votes that were just cast for the nominee + /// @param weight The weight of the vote that was just cast for the nominee + /// @param totalUsedVotes The total amount of votes the voter has used for this proposal + /// @param usableVotes The total amount of votes the voter has available for this proposal + /// @param weightReceived The total amount of voting weight the nominee has received for this proposal + event VoteCastForNominee( + address indexed voter, + uint256 indexed proposalId, + address indexed nominee, + uint256 votes, + uint256 weight, + uint256 totalUsedVotes, + uint256 usableVotes, + uint256 weightReceived + ); + /// @notice Emitted when the a new full weight duration is set + event FullWeightDurationSet(uint256 newFullWeightDuration); + + error FullWeightDurationGreaterThanVotingPeriod( + uint256 fullWeightDuration, uint256 votingPeriod + ); + error UnexpectedParamsLength(uint256 paramLength); + error NotCompliantNominee(address nominee); + error ZeroWeightVote(uint256 blockNumber, uint256 votes); + error InsufficientVotes(uint256 prevVotesUsed, uint256 votes, uint256 availableVotes); + error LengthsDontMatch(uint256 nomineesLength, uint256 weightsLength); + error NotEnoughNominees(uint256 numNominees, uint256 k); + error UintTooLarge(uint256 x); + error InvalidSupport(uint8 support); + + /// @param initialFullWeightDuration Duration of full weight voting (expressed in blocks) + function __SecurityCouncilMemberElectionGovernorCounting_init(uint256 initialFullWeightDuration) + internal + onlyInitializing + { + fullWeightDuration = initialFullWeightDuration; + emit FullWeightDurationSet(initialFullWeightDuration); + } + + /// @notice Set the full weight duration + /// @dev Note that this should not be called during an ongoing election + /// as it may lead to inconsistent weights in the ongoing election + function setFullWeightDuration(uint256 newFullWeightDuration) public onlyGovernance { + if (newFullWeightDuration > votingPeriod()) { + revert FullWeightDurationGreaterThanVotingPeriod(newFullWeightDuration, votingPeriod()); + } + + fullWeightDuration = newFullWeightDuration; + emit FullWeightDurationSet(newFullWeightDuration); + } + + /// @notice Register a vote by some account for a nominee. + /// @dev Reverts if the account does not have enough votes. + /// Reverts if the provided nominee is not a compliant nominee of the election. + /// Weight of the vote is determined using the votesToWeight function. + /// @param proposalId The id of the proposal + /// @param account The account that is voting + /// @param support The support of the vote (forced to 1) + /// @param availableVotes The amount of votes that account had at the time of the proposal snapshot + /// @param params Abi encoded (address nominee, uint256 votes) + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 availableVotes, + bytes memory params + ) internal virtual override { + if (support != 1) { + revert InvalidSupport(support); + } + + if (params.length != 64) { + revert UnexpectedParamsLength(params.length); + } + + (address nominee, uint256 votes) = abi.decode(params, (address, uint256)); + if (!_isCompliantNominee(proposalId, nominee)) { + revert NotCompliantNominee(nominee); + } + + uint240 weight = votesToWeight(proposalId, block.number, votes); + if (weight == 0) { + revert ZeroWeightVote(block.number, votes); + } + + ElectionInfo storage election = _elections[proposalId]; + uint256 prevVotesUsed = election.votesUsed[account]; + if (prevVotesUsed + votes > availableVotes) { + revert InsufficientVotes(prevVotesUsed, votes, availableVotes); + } + + uint240 prevWeightReceived = election.weightReceived[nominee]; + election.votesUsed[account] = prevVotesUsed + votes; + election.weightReceived[nominee] = prevWeightReceived + weight; + + emit VoteCastForNominee({ + voter: account, + proposalId: proposalId, + nominee: nominee, + votes: votes, + weight: weight, + totalUsedVotes: prevVotesUsed + votes, + usableVotes: availableVotes, + weightReceived: election.weightReceived[nominee] + }); + } + + /// @inheritdoc IGovernorUpgradeable + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=for¶ms=account&counting=n-winners"; + } + + /// @notice Number of votes used by an account for a given proposal + function votesUsed(uint256 proposalId, address account) public view returns (uint256) { + return _elections[proposalId].votesUsed[account]; + } + + /// @notice Weight received by a nominee for a given proposal + function weightReceived(uint256 proposalId, address nominee) public view returns (uint256) { + return _elections[proposalId].weightReceived[nominee]; + } + + /// @notice Whether the account has voted any amount for any nominee in the proposal + function hasVoted(uint256 proposalId, address account) public view override returns (bool) { + return votesUsed(proposalId, account) > 0; + } + + /// @notice The deadline after which voting weight will linearly decrease + /// @param proposalId The proposal to check the deadline for + function fullWeightVotingDeadline(uint256 proposalId) public view returns (uint256) { + uint256 startBlock = proposalSnapshot(proposalId); + return startBlock + fullWeightDuration; + } + + /// @notice Gets the top K nominees with greatest weight for a given proposal, + /// where K is the manager.cohortSize() + /// @dev Care must be taken of gas usage in this function. + /// This is an O(n) operation on all compliant nominees in the nominees governor. + /// The maximum number of nominees is set by the threshold of votes required to become a nominee. + /// Currently this is 0.2% of votable tokens, which corresponds to 500 max nominees. + /// Absolute worst case, this function uses 4502345 with 500 nominees, or about 9k gas per nominee (when called externally). + /// @param proposalId The proposal to find the top nominees for + function topNominees(uint256 proposalId) public view returns (address[] memory) { + address[] memory nominees = _compliantNominees(proposalId); + uint240[] memory weights = new uint240[](nominees.length); + ElectionInfo storage election = _elections[proposalId]; + for (uint256 i = 0; i < nominees.length; i++) { + weights[i] = election.weightReceived[nominees[i]]; + } + return selectTopNominees(nominees, weights, _targetMemberCount()); + } + + /// @notice Gets the top K nominees from a list of nominees and weights. + /// @param nominees The nominees to select from + /// @param weights The weights of the nominees + /// @param k The number of nominees to select + function selectTopNominees(address[] memory nominees, uint240[] memory weights, uint256 k) + public + pure + returns (address[] memory) + { + if (nominees.length != weights.length) { + revert LengthsDontMatch(nominees.length, weights.length); + } + if (nominees.length < k) { + revert NotEnoughNominees(nominees.length, k); + } + + uint256[] memory topNomineesPacked = new uint256[](k); + + for (uint16 i = 0; i < nominees.length; i++) { + // The nominee's index in the address array is stored in the 16 rightmost bits; the remaining bits store the nominee's weight + uint256 packed = (uint256(weights[i]) << 16) | i; + // Packed weight/index values can be compared when comparing weights, since the values of the weights will outweigh any difference in index; + // the index value only takes effect here as tie-breaker if the weights are equal. + // If the current weight is greater than the smallest of the top-6 weights so far, replace the smallest element with it and re-sort. + if (topNomineesPacked[0] < packed) { + topNomineesPacked[0] = packed; + LibSort.insertionSort(topNomineesPacked); + } + } + + address[] memory topNomineesAddresses = new address[](k); + for (uint16 i = 0; i < k; i++) { + // retrieve the index from the packed value to look up the nominee's address. + topNomineesAddresses[i] = nominees[uint16(topNomineesPacked[i])]; + } + + return topNomineesAddresses; + } + + /// @notice Returns the weight of a vote for a given proposal, block number, and number of votes. + /// Each vote has weight 1 until the fullWeightVotingDeadline is reached, after which each vote has linearly + /// decreasing weight, reaching 0 at the proposalDeadline. + function votesToWeight(uint256 proposalId, uint256 blockNumber, uint256 votes) + public + view + returns (uint240) + { + // Votes have zero weight until after snapshot block + // Votes are not counted anyway on the actual snapshot block due to a check + // in token.getPastVotes() + uint256 startBlock = proposalSnapshot(proposalId); + if (blockNumber <= startBlock) { + return 0; + } + // After proposalDeadline all votes have zero weight + uint256 endBlock = proposalDeadline(proposalId); + if (blockNumber > endBlock) { + return 0; + } + + // Between proposalSnapshot and fullWeightVotingDeadline all votes will have 100% weight - each vote has weight 1 + uint256 fullWeightVotingDeadline_ = fullWeightVotingDeadline(proposalId); + if (blockNumber <= fullWeightVotingDeadline_) { + return _downCast(votes); + } + + // Between the fullWeightVotingDeadline and the proposalDeadline each vote will have weight linearly decreased by time since fullWeightVotingDeadline + // slope denominator + uint256 decreasingWeightDuration = endBlock - fullWeightVotingDeadline_; + // slope numerator is -votes, slope denominator is decreasingWeightDuration, delta x is blockNumber - fullWeightVotingDeadline_ + // y intercept is votes + uint256 decreaseAmount = + votes * (blockNumber - fullWeightVotingDeadline_) / decreasingWeightDuration; + // subtract the decreased amount to get the remaining weight + return _downCast(votes - decreaseAmount); + } + + /// @notice Downcasts a uint256 to a uint240, reverting if the input is too large + function _downCast(uint256 x) internal pure returns (uint240) { + if (x > type(uint240).max) { + revert UintTooLarge(x); + } + return uint240(x); + } + + /// @notice True, since there is no minimum quorum + function _quorumReached(uint256) internal pure override returns (bool) { + return true; + } + + /// @notice True, since an election can only be only started if there are enough nominees + /// and candidates cannot be excluded after the election has started + function _voteSucceeded(uint256) internal pure override returns (bool) { + return true; + } + + /// @dev Whether the possibleNominee is a compliant nominee for the given proposal + function _isCompliantNominee(uint256 proposalId, address possibleNominee) + internal + view + virtual + returns (bool); + + /// @dev The list of all compliant (non excluded) nominees for the requested proposal + function _compliantNominees(uint256 proposalId) + internal + view + virtual + returns (address[] memory); + + /// @dev The target number of members to elect + function _targetMemberCount() internal view virtual returns (uint256); + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[48] private __gap; +} diff --git a/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorCountingUpgradeable.sol b/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorCountingUpgradeable.sol new file mode 100644 index 00000000..25a568f7 --- /dev/null +++ b/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorCountingUpgradeable.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; + +/// @title SecurityCouncilNomineeElectionGovernorCountingUpgradeable +/// @notice Counting module for the SecurityCouncilNomineeElectionGovernor +/// Keeps track of all contenders that receive enough votes to be a nominee +/// Voters can spread votes across multiple contenders +abstract contract SecurityCouncilNomineeElectionGovernorCountingUpgradeable is + Initializable, + GovernorUpgradeable +{ + /// @param votesUsed The amount of votes a voter has used + /// @param votesReceived The amount of votes a contender has received + /// @param nominees The list of contenders who've received enough votes to become a nominee + /// @param isNominee A mapping of contenders to whether or not they are a nominee + struct NomineeElectionCountingInfo { + mapping(address => uint256) votesUsed; + mapping(address => uint256) votesReceived; + address[] nominees; + mapping(address => bool) isNominee; + } + + // proposalId => NomineeElectionCountingInfo + mapping(uint256 => NomineeElectionCountingInfo) private _elections; + + /// @notice Emitted when a vote is cast for a contender + /// @param proposalId The id of the proposal + /// @param voter The account that is casting the vote + /// @param contender The contender that is receiving the vote + /// @param votes The amount of votes that were just cast for the contender + /// @param totalUsedVotes The total amount of votes the voter has used for this proposal + /// @param usableVotes The total amount of votes the voter has available for this proposal + event VoteCastForContender( + uint256 indexed proposalId, + address indexed voter, + address indexed contender, + uint256 votes, + uint256 totalUsedVotes, + uint256 usableVotes + ); + + event NewNominee(uint256 indexed proposalId, address indexed nominee); + + error UnexpectedParamsLength(uint256 paramLength); + error NotEligibleContender(address contender); + error NomineeAlreadyAdded(address nominee); + error InsufficientTokens(uint256 votes, uint256 prevVotesUsed, uint256 weight); + error InvalidSupport(uint8 support); + + function __SecurityCouncilNomineeElectionGovernorCounting_init() internal onlyInitializing {} + + /// @dev This function is responsible for counting votes when they are cast. + /// If this vote pushes the contender over the line, then the contender is added to the nominees + /// and only the necessary amount of votes will be deducted from the voter. + /// @param proposalId the id of the proposal + /// @param account the account that is casting the vote + /// @param support the support of the vote (forced to 1) + /// @param weight the amount of vote that account held at time of snapshot + /// @param params abi encoded (contender, votes) where votes is the amount of votes the account is using for this contender + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) internal virtual override { + if (support != 1) { + revert InvalidSupport(support); + } + + if (params.length != 64) { + revert UnexpectedParamsLength(params.length); + } + + // params is encoded as (address contender, uint256 votes) + (address contender, uint256 votes) = abi.decode(params, (address, uint256)); + + if (!isContender(proposalId, contender)) { + revert NotEligibleContender(contender); + } + if (isNominee(proposalId, contender)) { + revert NomineeAlreadyAdded(contender); + } + + NomineeElectionCountingInfo storage election = _elections[proposalId]; + uint256 prevVotesUsed = election.votesUsed[account]; + + if (votes + prevVotesUsed > weight) { + revert InsufficientTokens(votes, prevVotesUsed, weight); + } + + uint256 prevVotesReceived = election.votesReceived[contender]; + uint256 votesThreshold = quorum(proposalSnapshot(proposalId)); + + uint256 actualVotes = votes; + if (prevVotesReceived + votes >= votesThreshold) { + // we pushed the contender over the line + // we should only give the contender enough votes to get to the line so that we don't waste votes + actualVotes = votesThreshold - prevVotesReceived; + + // push the contender to the nominees + _addNominee(proposalId, contender); + } + + election.votesUsed[account] = prevVotesUsed + actualVotes; + election.votesReceived[contender] = prevVotesReceived + actualVotes; + + emit VoteCastForContender({ + proposalId: proposalId, + voter: account, + contender: contender, + votes: actualVotes, + totalUsedVotes: prevVotesUsed + actualVotes, + usableVotes: weight + }); + } + + /// @dev Transitions an account to being a nominee + function _addNominee(uint256 proposalId, address account) internal { + _elections[proposalId].nominees.push(account); + _elections[proposalId].isNominee[account] = true; + emit NewNominee(proposalId, account); + } + + /// @inheritdoc IGovernorUpgradeable + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=for¶ms=account&counting=threshold"; + } + + /// @notice Whether the account has voted any amount for any contender in the proposal + function hasVoted(uint256 proposalId, address account) public view override returns (bool) { + return _elections[proposalId].votesUsed[account] > 0; + } + + /// @notice Whether the contender has enough votes to be a nominee + function isNominee(uint256 proposalId, address contender) public view returns (bool) { + return _elections[proposalId].isNominee[contender]; + } + + /// @notice The number of nominees for a given proposal + function nomineeCount(uint256 proposalId) public view returns (uint256) { + return _elections[proposalId].nominees.length; + } + + /// @notice The list of nominees for a given proposal + function nominees(uint256 proposalId) public view returns (address[] memory) { + return _elections[proposalId].nominees; + } + + /// @notice The amount of votes an account has used for a given proposal + function votesUsed(uint256 proposalId, address account) public view returns (uint256) { + return _elections[proposalId].votesUsed[account]; + } + + /// @notice The amount of votes a contender has received for a given proposal + function votesReceived(uint256 proposalId, address contender) public view returns (uint256) { + return _elections[proposalId].votesReceived[contender]; + } + + /// @dev Whether the account is a contender for the proposal + function isContender(uint256 proposalId, address possibleContender) + public + view + virtual + returns (bool); + + /// @dev there is no minimum quorum for nominations proposals to pass, so we just return true + function _quorumReached(uint256) internal pure override returns (bool) { + return true; + } + + /// @dev the vote always succeeds, so we just return true + function _voteSucceeded(uint256) internal pure override returns (bool) { + return true; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol b/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol new file mode 100644 index 00000000..c8fd0566 --- /dev/null +++ b/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../interfaces/ISecurityCouncilManager.sol"; + +import "@openzeppelin/contracts-upgradeable/governance/GovernorUpgradeable.sol"; +import "solady/utils/DateTimeLib.sol"; +import "../../Common.sol"; + +/// @title SecurityCouncilNomineeElectionGovernorTiming +/// @notice Timing module for the SecurityCouncilNomineeElectionGovernor +abstract contract SecurityCouncilNomineeElectionGovernorTiming is + Initializable, + GovernorUpgradeable +{ + /// @notice First election start date + Date public firstNominationStartDate; + + /// @notice Duration of the nominee vetting period (expressed in blocks) + /// @dev This is the amount of time after voting ends that the nomineeVetter can exclude noncompliant nominees + uint256 public nomineeVettingDuration; + + error InvalidStartDate(uint256 year, uint256 month, uint256 day, uint256 hour); + error StartDateTooEarly(uint256 startTime, uint256 currentTime); + + /// @notice Initialize the timing module + /// @dev Checks to make sure the start date is in the future and is valid + function __SecurityCouncilNomineeElectionGovernorTiming_init( + Date memory _firstNominationStartDate, + uint256 _nomineeVettingDuration + ) internal onlyInitializing { + bool isSupportedDateTime = DateTimeLib.isSupportedDateTime({ + year: _firstNominationStartDate.year, + month: _firstNominationStartDate.month, + day: _firstNominationStartDate.day, + hour: _firstNominationStartDate.hour, + minute: 0, + second: 0 + }); + + if (!isSupportedDateTime) { + revert InvalidStartDate( + _firstNominationStartDate.year, + _firstNominationStartDate.month, + _firstNominationStartDate.day, + _firstNominationStartDate.hour + ); + } + + // make sure the start date is in the future + uint256 startTimestamp = DateTimeLib.dateTimeToTimestamp({ + year: _firstNominationStartDate.year, + month: _firstNominationStartDate.month, + day: _firstNominationStartDate.day, + hour: _firstNominationStartDate.hour, + minute: 0, + second: 0 + }); + + if (startTimestamp <= block.timestamp) { + revert StartDateTooEarly(startTimestamp, block.timestamp); + } + + firstNominationStartDate = _firstNominationStartDate; + nomineeVettingDuration = _nomineeVettingDuration; + } + + /// @notice Deadline for the nominee vetting period for a given `proposalId` + function proposalVettingDeadline(uint256 proposalId) public view returns (uint256) { + return proposalDeadline(proposalId) + nomineeVettingDuration; + } + + /// @notice Start timestamp of an election + /// @param electionIndex The index of the election + function electionToTimestamp(uint256 electionIndex) public view returns (uint256) { + // subtract one to make month 0 indexed + uint256 month = firstNominationStartDate.month - 1; + + month += 6 * electionIndex; + uint256 year = firstNominationStartDate.year + month / 12; + month = month % 12; + + // add one to make month 1 indexed + month += 1; + + return DateTimeLib.dateTimeToTimestamp({ + year: year, + month: month, + day: firstNominationStartDate.day, + hour: firstNominationStartDate.hour, + minute: 0, + second: 0 + }); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[45] private __gap; +} diff --git a/src/security-council-mgmt/interfaces/IGnosisSafe.sol b/src/security-council-mgmt/interfaces/IGnosisSafe.sol new file mode 100644 index 00000000..b85cc5bb --- /dev/null +++ b/src/security-council-mgmt/interfaces/IGnosisSafe.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +abstract contract OpEnum { + enum Operation { + Call, + DelegateCall + } +} + +interface IGnosisSafe { + function getOwners() external view returns (address[] memory); + function getThreshold() external view returns (uint256); + function isOwner(address owner) external view returns (bool); + function isModuleEnabled(address module) external view returns (bool); + function addOwnerWithThreshold(address owner, uint256 threshold) external; + function removeOwner(address prevOwner, address owner, uint256 threshold) external; + function execTransactionFromModule( + address to, + uint256 value, + bytes memory data, + OpEnum.Operation operation + ) external returns (bool success); +} diff --git a/src/security-council-mgmt/interfaces/ISecurityCouncilManager.sol b/src/security-council-mgmt/interfaces/ISecurityCouncilManager.sol new file mode 100644 index 00000000..076eadfa --- /dev/null +++ b/src/security-council-mgmt/interfaces/ISecurityCouncilManager.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../UpgradeExecRouteBuilder.sol"; +import "../Common.sol"; + +/// @notice Addresses to be given specific roles on the Security Council Manager +struct SecurityCouncilManagerRoles { + address admin; + address cohortUpdator; + address memberAdder; + address[] memberRemovers; + address memberRotator; + address memberReplacer; +} + +/// @notice Data for a Security Council to be managed +struct SecurityCouncilData { + /// @notice Address of the Security Council + address securityCouncil; + /// @notice Address of the update action contract that contains the logic for + /// updating council membership. Will be delegate called by the upgrade executor + address updateAction; + uint256 chainId; +} + +interface ISecurityCouncilManager { + // security council cohort errors + error NotAMember(address member); + error MemberInCohort(address member, Cohort cohort); + error CohortFull(Cohort cohort); + error InvalidNewCohortLength(address[] cohort, uint256 cohortSize); + error CohortLengthMismatch(address[] cohort1, address[] cohort2); + error InvalidCohort(Cohort cohort); + + // security council data errors + error MaxSecurityCouncils(uint256 securityCouncilCount); + error SecurityCouncilZeroChainID(SecurityCouncilData securiyCouncilData); + error SecurityCouncilNotInRouter(SecurityCouncilData securiyCouncilData); + error SecurityCouncilNotInManager(SecurityCouncilData securiyCouncilData); + error SecurityCouncilAlreadyInRouter(SecurityCouncilData securiyCouncilData); + + /// @notice initialize SecurityCouncilManager. + /// @param _firstCohort addresses of first cohort + /// @param _secondCohort addresses of second cohort + /// @param _securityCouncils data of all security councils to manage + /// @param _roles permissions for triggering modifications to security councils + /// @param _l2CoreGovTimelock timelock for core governance / constitutional proposal + /// @param _router UpgradeExecRouteBuilder address + function initialize( + address[] memory _firstCohort, + address[] memory _secondCohort, + SecurityCouncilData[] memory _securityCouncils, + SecurityCouncilManagerRoles memory _roles, + address payable _l2CoreGovTimelock, + UpgradeExecRouteBuilder _router + ) external; + /// @notice Replaces a whole cohort. + /// @dev Initiaties cross chain messages to update the individual Security Councils. + /// @param _newCohort New cohort members to replace existing cohort. Must have 6 members. + /// @param _cohort Cohort to replace. + function replaceCohort(address[] memory _newCohort, Cohort _cohort) external; + /// @notice Add a member to the specified cohort. + /// Cohorts cannot have more than 6 members, so the cohort must have less than 6 in order to call this. + /// New member cannot already be a member of either cohort. + /// @dev Initiaties cross chain messages to update the individual Security Councils. + /// When adding a member, make sure that the key does not conflict with any contenders/nominees of ongoing elections. + /// @param _newMember New member to add + /// @param _cohort Cohort to add member to + function addMember(address _newMember, Cohort _cohort) external; + /// @notice Remove a member. + /// @dev Searches both cohorts for the member. + /// Initiaties cross chain messages to update the individual Security Councils + /// @param _member Member to remove + function removeMember(address _member) external; + /// @notice Replace a member in a council - equivalent to removing a member, then adding another in its place. + /// Idendities of members should be different. + /// Functionality is equivalent to replaceMember, + /// though emits a different event to distinguish the security council's intent (different identities). + /// @dev Initiaties cross chain messages to update the individual Security Councils. + /// When replacing a member, make sure that the key does not conflict with any contenders/nominees of ongoing electoins. + /// @param _memberToReplace Security Council member to remove + /// @param _newMember Security Council member to add in their place + function replaceMember(address _memberToReplace, address _newMember) external; + /// @notice Security council member can rotate out their address for a new one; _currentAddress and _newAddress should be of the same identity. Functionality is equivalent to replaceMember, tho emits a different event to distinguish the security council's intent (same identity). + /// Rotation must be initiated by the security council. + /// @dev Initiaties cross chain messages to update the individual Security Councils. + /// When rotating a member, make sure that the key does not conflict with any contenders/nominees of ongoing elections. + /// @param _currentAddress Address to rotate out + /// @param _newAddress Address to rotate in + function rotateMember(address _currentAddress, address _newAddress) external; + /// @notice Is the account a member of the first cohort + function firstCohortIncludes(address account) external view returns (bool); + /// @notice Is the account a member of the second cohort + function secondCohortIncludes(address account) external view returns (bool); + /// @notice Is the account a member of the specified cohort + function cohortIncludes(Cohort cohort, address account) external view returns (bool); + /// @notice All members of the first cohort + function getFirstCohort() external view returns (address[] memory); + /// @notice All members of the second cohort + function getSecondCohort() external view returns (address[] memory); + /// @notice All members of both cohorts + function getBothCohorts() external view returns (address[] memory); + /// @notice Length of security councils array + function securityCouncilsLength() external view returns (uint256); + /// @notice Size of cohort under ordinary circumstances + function cohortSize() external view returns (uint256); + /// @notice Add new security council to be included in security council management system. + /// @param _securityCouncilData Security council info + function addSecurityCouncil(SecurityCouncilData memory _securityCouncilData) external; + /// @notice Remove security council from management system. + /// @param _securityCouncilData security council to be removed + function removeSecurityCouncil(SecurityCouncilData memory _securityCouncilData) + external + returns (bool); + /// @notice UpgradeExecRouteBuilder is immutable, so in lieu of upgrading it, it can be redeployed and reset here + /// @param _router new router address + function setUpgradeExecRouteBuilder(UpgradeExecRouteBuilder _router) external; + /// @notice Gets the data that will be used to update each of the security councils + /// @param nonce The nonce used to generate the timelock salts + /// @return The new members to be added to the councils + /// @return The address of the contract that will be called by the l2 timelock + /// @return The data that will be called from the l2 timelock + function getScheduleUpdateInnerData(uint256 nonce) + external + view + returns (address[] memory, address, bytes memory); + /// @notice Generate the salt used in the timelocks when scheduling an update + /// @param _members The new members to be added + /// @param nonce The manager nonce to make the salt unique - current nonce can be found by calling updateNonce + function generateSalt(address[] memory _members, uint256 nonce) + external + pure + returns (bytes32); + /// @notice Each update increments an internal nonce that keeps updates unique, current value stored here + function updateNonce() external returns (uint256); +} diff --git a/src/security-council-mgmt/interfaces/ISecurityCouncilMemberElectionGovernor.sol b/src/security-council-mgmt/interfaces/ISecurityCouncilMemberElectionGovernor.sol new file mode 100644 index 00000000..c5295950 --- /dev/null +++ b/src/security-council-mgmt/interfaces/ISecurityCouncilMemberElectionGovernor.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +interface ISecurityCouncilMemberElectionGovernor { + /// @notice Creates a new member election proposal from the most recent nominee election. + function proposeFromNomineeElectionGovernor(uint256 electionIndex) external returns (uint256); +} diff --git a/src/security-council-mgmt/interfaces/ISecurityCouncilNomineeElectionGovernor.sol b/src/security-council-mgmt/interfaces/ISecurityCouncilNomineeElectionGovernor.sol new file mode 100644 index 00000000..aadaf74a --- /dev/null +++ b/src/security-council-mgmt/interfaces/ISecurityCouncilNomineeElectionGovernor.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +/// @notice Minimal interface of nominee election governor required by other contracts +interface ISecurityCouncilNomineeElectionGovernor { + /// @notice Whether the account a compliant nominee for a given proposal + /// A compliant nominee is one who is a nominee, and has not been excluded + /// @param proposalId The id of the proposal + /// @param account The account to check + function isCompliantNominee(uint256 proposalId, address account) external view returns (bool); + /// @notice All compliant nominees of a given proposal + /// A compliant nominee is one who is a nominee, and has not been excluded + function compliantNominees(uint256 proposalId) external view returns (address[] memory); +} diff --git a/test/L2GovernanceFactory.t.sol b/test/L2GovernanceFactory.t.sol index ab926cb9..13ad9405 100644 --- a/test/L2GovernanceFactory.t.sol +++ b/test/L2GovernanceFactory.t.sol @@ -73,7 +73,6 @@ contract L2GovernanceFactoryTest is Test { ArbitrumDAOConstitution arbitrumDAOConstitution ) { - L2GovernanceFactory l2GovernanceFactory; { address _coreTimelockLogic = address(new ArbitrumTimelock()); address _coreGovernorLogic = address(new L2ArbitrumGovernor()); diff --git a/test/UpgradeExecRouteBuilder.t.sol b/test/UpgradeExecRouteBuilder.t.sol new file mode 100644 index 00000000..fb13667f --- /dev/null +++ b/test/UpgradeExecRouteBuilder.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../src/UpgradeExecRouteBuilder.sol"; +import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; + +import "forge-std/Test.sol"; + +contract UpgradeExecRouteBuilderTest is Test { + address l1TimelockAddress = 0xE6841D92B0C345144506576eC13ECf5103aC7f49; + address arbSys = 0x0000000000000000000000000000000000000064; + // generated using the an altered proposal creator that calls scheduleBatch instead of schedule + bytes aipData = + hex"8f2a0bb000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000067565fcc91c79be6e957056bdf0ed93287216afcc5ea02fec16f1900a177a3c5000000000000000000000000000000000000000000000000000000000003f4800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a723c008e76e379c55599d2e4d93879beafda79c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001800000000000000000000000004dbd4fc535ac27206064b68ffcf827b0a60bab3f000000000000000000000000cf57572261c7c2bcf21ffd220ea7d1a27d40a82700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000841cff79cd0000000000000000000000006274106eedd4848371d2c09e0352d67b795ed51600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004b147f40c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + address aip1Point2ActionAddress = 0x6274106eedD4848371D2C09e0352d67B795ED516; + bytes32 aip1Point2TimelockSalt = + 0x67565fcc91c79be6e957056bdf0ed93287216afcc5ea02fec16f1900a177a3c5; + + function deployRouteBuilder() internal returns (UpgradeExecRouteBuilder) { + ChainAndUpExecLocation[] memory chainLocations = new ChainAndUpExecLocation[](1); + chainLocations[0] = ChainAndUpExecLocation({ + chainId: 42_161, + location: UpExecLocation({ + upgradeExecutor: 0xCF57572261c7c2BCF21ffD220ea7d1a27D40A827, + inbox: 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f + }) + }); + + return new UpgradeExecRouteBuilder( + chainLocations, + l1TimelockAddress, + 259200 + ); + } + + // test that aip1.2 would have been created with the same call data if it had used + // the route builder + function testAIP1Point2() public { + UpgradeExecRouteBuilder routeBuilder = deployRouteBuilder(); + + uint256[] memory chainIds = new uint256[](1); + chainIds[0] = 42_161; + address[] memory actionAddresses = new address[](1); + actionAddresses[0] = aip1Point2ActionAddress; + (address to, bytes memory data) = routeBuilder.createActionRouteDataWithDefaults( + chainIds, actionAddresses, aip1Point2TimelockSalt + ); + + assertEq(to, arbSys); + assertEq( + data, abi.encodeWithSelector(ArbSys.sendTxToL1.selector, l1TimelockAddress, aipData) + ); + } + + // test all the error conditions for the createActionRouteData funtion on the route builder + function testRouteBuilderErrors() public { + UpgradeExecRouteBuilder routeBuilder = deployRouteBuilder(); + uint256[] memory chainIds = new uint256[](1); + chainIds[0] = 42_161; + address[] memory actionAddresses = new address[](1); + actionAddresses[0] = aip1Point2ActionAddress; + uint256[] memory actionValues = new uint256[](1); + actionValues[0] = 2; + bytes[] memory actionDatas = new bytes[](1); + actionDatas[0] = aipData; + bytes32 predecessor = bytes32(uint256(1)); + bytes32 salt = bytes32(uint256(0x20)); + vm.expectRevert( + abi.encodeWithSelector(UpgradeExecRouteBuilder.ParamLengthMismatch.selector, 1, 0) + ); + routeBuilder.createActionRouteData( + chainIds, new address[](0), actionValues, actionDatas, predecessor, salt + ); + + vm.expectRevert( + abi.encodeWithSelector(UpgradeExecRouteBuilder.ParamLengthMismatch.selector, 1, 0) + ); + routeBuilder.createActionRouteData( + chainIds, actionAddresses, new uint256[](0), actionDatas, predecessor, salt + ); + + vm.expectRevert( + abi.encodeWithSelector(UpgradeExecRouteBuilder.ParamLengthMismatch.selector, 1, 0) + ); + routeBuilder.createActionRouteData( + chainIds, actionAddresses, actionValues, new bytes[](0), predecessor, salt + ); + + uint256[] memory badChainIds = new uint256[](1); + badChainIds[0] = 42_162; + vm.expectRevert( + abi.encodeWithSelector(UpgradeExecRouteBuilder.UpgadeExecDoesntExist.selector, 42_162) + ); + routeBuilder.createActionRouteData( + badChainIds, actionAddresses, actionValues, actionDatas, predecessor, salt + ); + + bytes[] memory badActionDatas = new bytes[](1); + badActionDatas[0] = new bytes(0); + vm.expectRevert( + abi.encodeWithSelector( + UpgradeExecRouteBuilder.EmptyActionBytesData.selector, badActionDatas + ) + ); + routeBuilder.createActionRouteData( + chainIds, actionAddresses, actionValues, badActionDatas, predecessor, salt + ); + } +} diff --git a/test/security-council-mgmt/E2E.t.sol b/test/security-council-mgmt/E2E.t.sol new file mode 100644 index 00000000..e69cc1a8 --- /dev/null +++ b/test/security-council-mgmt/E2E.t.sol @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; + +import "../../src/UpgradeExecutor.sol"; +import "../../src/ArbitrumTimelock.sol"; +import "../../src/L2ArbitrumGovernor.sol"; +import "../../src/FixedDelegateErc20Wallet.sol"; +import "../../src/L2GovernanceFactory.sol"; +import "../../src/L1GovernanceFactory.sol"; +import "../../src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol"; +import "../util/InboxMock.sol"; +import "../../src/gov-action-contracts/AIPs/SecurityCouncilMgmt/L1SCMgmtActivationAction.sol"; +import + "../../src/gov-action-contracts/AIPs/SecurityCouncilMgmt/GovernanceChainSCMgmtActivationAction.sol"; +import "@gnosis.pm/safe-contracts/contracts/GnosisSafeL2.sol"; +import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol"; +import "../../src/gov-action-contracts/address-registries/L2AddressRegistry.sol"; +import "../util/DeployGnosisWithModule.sol"; +import "../../src/security-council-mgmt/Common.sol"; + +contract ArbSysMock { + event ArbSysL2ToL1Tx(address from, address to, uint256 value, bytes data); + + uint256 counter; + + struct L2ToL1Tx { + address from; + address to; + uint256 value; + bytes data; + } + + L2ToL1Tx[] public txs; + + function sendTxToL1(address destination, bytes calldata calldataForL1) + external + payable + returns (uint256 exitNum) + { + exitNum = counter; + counter = exitNum + 1; + txs.push(L2ToL1Tx(msg.sender, destination, msg.value, calldataForL1)); + emit ArbSysL2ToL1Tx(msg.sender, destination, msg.value, calldataForL1); + return exitNum; + } + + function getTx(uint256 index) public view returns (L2ToL1Tx memory) { + return txs[index]; + } +} + +library Parser { + // create a struct for all the arguments to the scheduleBatch function + struct ScheduleBatchArgs { + address[] targets; + uint256[] values; + bytes[] payloads; + bytes32 predecessor; + bytes32 salt; + uint256 delay; + } + + function scheduleBatchArgs(bytes calldata data) + public + pure + returns (ScheduleBatchArgs memory) + { + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory payloads, + bytes32 predecessor, + bytes32 salt, + uint256 delay + ) = abi.decode(data[4:], (address[], uint256[], bytes[], bytes32, bytes32, uint256)); + + return ScheduleBatchArgs(targets, values, payloads, predecessor, salt, delay); + } +} + +contract E2E is Test, DeployGnosisWithModule { + uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + + function applyL1ToL2Alias(address l1Address) internal pure returns (address) { + return address(uint160(l1Address) + offset); + } + + UpgradeExecutor upExecLogic = new UpgradeExecutor(); + L2ArbitrumGovernor arbGovLogic = new L2ArbitrumGovernor(); + ArbitrumTimelock arbTimelockLogic = new ArbitrumTimelock(); + FixedDelegateErc20Wallet fWalletLogic = new FixedDelegateErc20Wallet(); + L2ArbitrumToken l2TokenLogic = new L2ArbitrumToken(); + + // owners + address member1 = address(637); + address member2 = address(638); + address member3 = address(639); + address member4 = address(640); + address member5 = address(641); + address member6 = address(642); + address member7 = address(643); + address member8 = address(644); + address member9 = address(645); + address member10 = address(646); + address member11 = address(647); + address member12 = address(648); + address member13 = address(649); + address member14 = address(650); + address member15 = address(651); + address member16 = address(652); + address member17 = address(653); + address member18 = address(654); + + address[] members = [ + member1, + member2, + member3, + member4, + member5, + member6, + member7, + member8, + member9, + member10, + member11, + member12 + ]; + address[] cohort1 = [member1, member2, member3, member4, member5, member6]; + address[] cohort2 = [member7, member8, member9, member10, member11, member12]; + address[] newCohort1 = [member13, member14, member15, member16, member17, member18]; + + uint256 secCouncilThreshold = 9; + + // token + address l1Token = address(139); + uint256 l2TokenInitialSupply = 30 ether; + + // timelock + uint256 l2MinTimelockDelay = 42; + uint256 l1MinTimelockDelay = 43; + + // govs + uint256 votingPeriod = 44; + uint256 votingDelay = 45; + uint256 coreQuorumThreshold = 4; + uint256 treasuryQuorumThreshold = 3; + uint256 proposalThreshold = 5e6; + uint64 minPeriodAfterQuorum = 41; + + // councils + address novaEmergencyCouncil = address(deploySafe(members, secCouncilThreshold, address(0))); + address l2EmergencyCouncil = address(deploySafe(members, secCouncilThreshold, address(0))); + address l1EmergencyCouncil = address(deploySafe(members, secCouncilThreshold, address(0))); + address someRando = address(390); + address l2NonEmergencySecurityCouncil = + address(deploySafe(members, secCouncilThreshold, address(0))); + address l2InitialSupplyRecipient = address(456); + + bytes32 constitutionHash = bytes32("0x0123"); + uint256 l2TreasuryMinTimelockDelay = 87; + + DeployCoreParams l2DeployParams = DeployCoreParams({ + _l2MinTimelockDelay: l2MinTimelockDelay, + _l1Token: l1Token, + _l2TokenInitialSupply: l2TokenInitialSupply, + _votingPeriod: votingPeriod, + _votingDelay: votingDelay, + _coreQuorumThreshold: coreQuorumThreshold, + _treasuryQuorumThreshold: treasuryQuorumThreshold, + _proposalThreshold: proposalThreshold, + _minPeriodAfterQuorum: minPeriodAfterQuorum, + _l2NonEmergencySecurityCouncil: l2NonEmergencySecurityCouncil, + _l2InitialSupplyRecipient: l2InitialSupplyRecipient, + _l2EmergencySecurityCouncil: l2EmergencyCouncil, + _constitutionHash: constitutionHash, + _l2TreasuryMinTimelockDelay: l2TreasuryMinTimelockDelay + }); + + uint256 removalGovVotingDelay = 47; + uint256 removalGovVotingPeriod = 48; + uint256 removalGovQuorumNumerator = 200; + uint256 removalGovProposalThreshold = 10e5; + uint256 removalGovVoteSuccessNumerator = 201; + uint64 removalGovMinPeriodAfterQuorum = 49; + uint256 removalProposalExpirationBlocks = 139; + + uint256 nomineeVettingDuration = 100; + address nomineeVetter = address(437); + uint256 nomineeQuorumNumerator = 20; + uint256 nomineeVotingPeriod = 51; + uint256 memberVotingPeriod = 53; + uint256 fullWeightDuration = 39; + Date nominationStart = Date(1988, 1, 1, 1); + + uint256 chain1Id = 937; + uint256 chain2Id = 837; + uint256 chainNovaId = 737; + + function checkSafeUpdated( + GnosisSafeL2 safe, + address[] memory oldCohort1, + address[] memory oldCohort2, + address[] memory newCohort, + uint256 threshold + ) internal view { + address[] memory currentOwners = safe.getOwners(); + require(currentOwners.length == 12, "not 12 owners"); + require(safe.getThreshold() == threshold, "threshold changed"); + // check that each cohort1 is a not an owner of moduleL1Safe + for (uint256 i = 0; i < oldCohort1.length; i++) { + require(!safe.isOwner(oldCohort1[i]), "old cohort 1 member not removed"); + } + for (uint256 i = 0; i < oldCohort2.length; i++) { + require(safe.isOwner(oldCohort2[i]), "old cohort 2 member missing"); + } + for (uint256 i = 0; i < newCohort.length; i++) { + require(safe.isOwner(newCohort[i]), "new cohort 1 member not added"); + } + } + + struct DeployData { + L2GovernanceFactory l2GovFac; + L1GovernanceFactory l1GovFac; + L2AddressRegistry l2AddressRegistry; + L2SecurityCouncilMgmtFactory secFac; + GnosisSafeL2 moduleL2Safe; + GnosisSafeL2 moduleNovaSafe; + GnosisSafeL2 moduleL1Safe; + GnosisSafeL2 moduleL2SafeNonEmergency; + SecurityCouncilMemberSyncAction l2UpdateAction; + SecurityCouncilMemberSyncAction novaUpdateAction; + SecurityCouncilMemberSyncAction l1UpdateAction; + SecurityCouncilData[] councilData; + ChainAndUpExecLocation[] cExecLocs; + L2SecurityCouncilMgmtFactory.DeployedContracts secDeployedContracts; + InboxMock inbox; + InboxMock novaInbox; + L1ArbitrumTimelock l1Timelock; + UpgradeExecutor l1Executor; + address[] newMembers; + address to; + bytes data; + address novaExecutor; + } + + function deployNova(address l1Timelock) internal returns (address) { + ProxyAdmin novaAdmin = new ProxyAdmin(); + UpgradeExecutor novaExecutorLogic = new UpgradeExecutor(); + UpgradeExecutor novaExecutor = UpgradeExecutor( + address( + new TransparentUpgradeableProxy( + address(novaExecutorLogic), + address(novaAdmin), + "" + ) + ) + ); + address[] memory executors = new address[](2); + executors[0] = applyL1ToL2Alias(l1Timelock); + executors[1] = novaEmergencyCouncil; + novaExecutor.initialize(address(novaExecutor), executors); + + novaAdmin.transferOwnership(address(novaExecutor)); + + return address(novaExecutor); + } + + function deploy() internal { + // set the current block + vm.roll(1000); + DeployData memory vars; + + vars.l2GovFac = new L2GovernanceFactory( + address(arbTimelockLogic), + address(arbGovLogic), + address(arbTimelockLogic), + address(fWalletLogic), + address(arbGovLogic), + address(l2TokenLogic), + address(upExecLogic) + ); + + vars.l1GovFac = new L1GovernanceFactory(); + ( + DeployedContracts memory l2DeployedCoreContracts, + DeployedTreasuryContracts memory l2DeployedTreasuryContracts + ) = vars.l2GovFac.deployStep1(l2DeployParams); + + vars.inbox = new InboxMock(address(0)); + vars.novaInbox = new InboxMock(address(0)); + { + (L1ArbitrumTimelock l1Timelock,, UpgradeExecutor l1Executor) = vars.l1GovFac.deployStep2( + address(upExecLogic), + l1MinTimelockDelay, + address(vars.inbox), + address(l2DeployedCoreContracts.coreTimelock), + l1EmergencyCouncil + ); + vars.l1Timelock = l1Timelock; + vars.l1Executor = l1Executor; + } + + vars.l2GovFac.deployStep3(applyL1ToL2Alias(address(vars.l1Timelock))); + + vars.novaExecutor = deployNova(address(vars.l1Timelock)); + + // deploy sec council + vars.l2AddressRegistry = new L2AddressRegistry( + IL2ArbitrumGoverner(address(l2DeployedCoreContracts.coreGov)), + IL2ArbitrumGoverner(address(l2DeployedTreasuryContracts.treasuryGov)), + IFixedDelegateErc20Wallet(address(l2DeployedTreasuryContracts.arbTreasury)), + IArbitrumDAOConstitution(address(l2DeployedCoreContracts.arbitrumDAOConstitution)) + ); + + vars.secFac = new L2SecurityCouncilMgmtFactory(); + + vars.moduleL2Safe = GnosisSafeL2( + payable( + deploySafe(members, secCouncilThreshold, address(l2DeployedCoreContracts.executor)) + ) + ); + vars.moduleL2SafeNonEmergency = GnosisSafeL2( + payable( + deploySafe(members, secCouncilThreshold, address(l2DeployedCoreContracts.executor)) + ) + ); + vars.moduleL1Safe = GnosisSafeL2( + payable(deploySafe(members, secCouncilThreshold, address(vars.l1Executor))) + ); + vars.moduleNovaSafe = GnosisSafeL2( + payable(deploySafe(members, secCouncilThreshold, address(vars.novaExecutor))) + ); + + vars.l2UpdateAction = new SecurityCouncilMemberSyncAction(new KeyValueStore()); + vars.novaUpdateAction = new SecurityCouncilMemberSyncAction(new KeyValueStore()); + vars.l1UpdateAction = new SecurityCouncilMemberSyncAction(new KeyValueStore()); + + vars.councilData = new SecurityCouncilData[](4); + vars.councilData[0] = + SecurityCouncilData(address(vars.moduleL2Safe), address(vars.l2UpdateAction), chain2Id); + vars.councilData[1] = SecurityCouncilData( + address(vars.moduleL2SafeNonEmergency), address(vars.l2UpdateAction), chain2Id + ); + vars.councilData[2] = + SecurityCouncilData(address(vars.moduleL1Safe), address(vars.l1UpdateAction), chain1Id); + vars.councilData[3] = SecurityCouncilData( + address(vars.moduleNovaSafe), address(vars.novaUpdateAction), chainNovaId + ); + + vars.cExecLocs = new ChainAndUpExecLocation[](3); + vars.cExecLocs[0] = + ChainAndUpExecLocation(chain1Id, UpExecLocation(address(0), address(vars.l1Executor))); + vars.cExecLocs[1] = ChainAndUpExecLocation( + chain2Id, UpExecLocation(address(vars.inbox), address(l2DeployedCoreContracts.executor)) + ); + vars.cExecLocs[2] = ChainAndUpExecLocation( + chainNovaId, UpExecLocation(address(vars.novaInbox), address(vars.novaExecutor)) + ); + + { + DeployParams memory secDeployParams = DeployParams({ + upgradeExecutors: vars.cExecLocs, + govChainEmergencySecurityCouncil: address(vars.moduleL2Safe), + l1ArbitrumTimelock: address(vars.l1Timelock), + l2CoreGovTimelock: address(l2DeployedCoreContracts.coreTimelock), + govChainProxyAdmin: address(l2DeployedCoreContracts.proxyAdmin), + secondCohort: cohort2, + firstCohort: cohort1, + l2UpgradeExecutor: address(l2DeployedCoreContracts.executor), + arbToken: address(l2DeployedCoreContracts.token), + l1TimelockMinDelay: l1MinTimelockDelay, + removalGovVotingDelay: removalGovVotingDelay, + removalGovVotingPeriod: removalGovVotingPeriod, + removalGovQuorumNumerator: removalGovQuorumNumerator, + removalGovProposalThreshold: removalGovProposalThreshold, + removalGovVoteSuccessNumerator: removalGovVoteSuccessNumerator, + removalGovMinPeriodAfterQuorum: removalGovMinPeriodAfterQuorum, + removalProposalExpirationBlocks: removalProposalExpirationBlocks, + securityCouncils: vars.councilData, + firstNominationStartDate: nominationStart, + nomineeVettingDuration: nomineeVettingDuration, + nomineeVetter: nomineeVetter, + nomineeQuorumNumerator: nomineeQuorumNumerator, + nomineeVotingPeriod: nomineeVotingPeriod, + memberVotingPeriod: memberVotingPeriod, + fullWeightDuration: fullWeightDuration + }); + + ContractImplementations memory contractImpls = ContractImplementations({ + securityCouncilManager: address(new SecurityCouncilManager()), + securityCouncilMemberRemoverGov: address(new SecurityCouncilMemberRemovalGovernor()), + nomineeElectionGovernor: address(new SecurityCouncilNomineeElectionGovernor()), + memberElectionGovernor: address(new SecurityCouncilMemberElectionGovernor()) + }); + + vars.secDeployedContracts = vars.secFac.deploy(secDeployParams, contractImpls); + } + + L1SCMgmtActivationAction installL1 = new L1SCMgmtActivationAction( + IGnosisSafe(address(vars.moduleL1Safe)), + IGnosisSafe(l1EmergencyCouncil), + secCouncilThreshold, + IUpgradeExecutor(address(vars.l1Executor)), + ICoreTimelock(address(vars.l1Timelock)) + ); + + // l1 sec council conducts the install + vm.prank(l1EmergencyCouncil); + vars.l1Executor.execute( + address(installL1), abi.encodeWithSelector(L1SCMgmtActivationAction.perform.selector) + ); + + GovernanceChainSCMgmtActivationAction installL2 = new GovernanceChainSCMgmtActivationAction( + IGnosisSafe(address(vars.moduleL2Safe)), + IGnosisSafe(address(vars.moduleL2SafeNonEmergency)), + IGnosisSafe(l2EmergencyCouncil), + IGnosisSafe(l2NonEmergencySecurityCouncil), + secCouncilThreshold, + secCouncilThreshold, + address(vars.secDeployedContracts.securityCouncilManager), + vars.l2AddressRegistry + ); + + vm.prank(l2EmergencyCouncil); + l2DeployedCoreContracts.executor.execute( + address(installL2), + abi.encodeWithSelector(GovernanceChainSCMgmtActivationAction.perform.selector) + ); + + // setup complete, try an election - warp to the next election + vm.warp( + DateTimeLib.dateTimeToTimestamp({ + year: nominationStart.year, + month: nominationStart.month, + day: nominationStart.day, + hour: nominationStart.hour, + minute: 0, + second: 1 + }) + ); + + // initial supply recipient delegates to itself + vm.prank(l2InitialSupplyRecipient); + l2DeployedCoreContracts.token.delegate(l2InitialSupplyRecipient); + + // start the election + uint256 propId = vars.secDeployedContracts.nomineeElectionGovernor.createElection(); + vm.roll(block.number + 1); + + // contenders up for election - and vote for them + for (uint256 i = 0; i < newCohort1.length; i++) { + address newMember = newCohort1[i]; + vm.prank(newMember); + vars.secDeployedContracts.nomineeElectionGovernor.addContender(propId); + vm.prank(l2InitialSupplyRecipient); + vars.secDeployedContracts.nomineeElectionGovernor.castVoteWithReasonAndParams( + propId, 1, "vote for a nominee", abi.encode(newMember, 1 ether) + ); + } + + { + // nomination complete - transition to member election + vm.roll(block.number + nomineeVotingPeriod + nomineeVettingDuration); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory callDatas, + string memory description + ) = vars.secDeployedContracts.nomineeElectionGovernor.getProposeArgs( + vars.secDeployedContracts.nomineeElectionGovernor.electionCount() - 1 + ); + vars.secDeployedContracts.nomineeElectionGovernor.execute( + targets, values, callDatas, keccak256(bytes(description)) + ); + } + + // vote for the new members + vm.roll(block.number + 1); + for (uint256 i = 0; i < newCohort1.length; i++) { + address newMember = newCohort1[i]; + vm.prank(l2InitialSupplyRecipient); + vars.secDeployedContracts.memberElectionGovernor.castVoteWithReasonAndParams( + propId, 1, "vote for a member", abi.encode(newMember, 1 ether) + ); + } + + { + // member election complete - transition to timelock + vm.roll(block.number + memberVotingPeriod); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory callDatas, + string memory description + ) = vars.secDeployedContracts.nomineeElectionGovernor.getProposeArgs( + vars.secDeployedContracts.nomineeElectionGovernor.electionCount() - 1 + ); + vars.secDeployedContracts.memberElectionGovernor.execute( + targets, values, callDatas, keccak256(bytes(description)) + ); + } + + // exec in the l2 timelock + { + (address[] memory newMembers, address to, bytes memory data) = vars + .secDeployedContracts + .securityCouncilManager + .getScheduleUpdateInnerData( + vars.secDeployedContracts.securityCouncilManager.updateNonce() + ); + vars.newMembers = newMembers; + vars.to = to; + vars.data = data; + } + vm.warp(block.timestamp + l2MinTimelockDelay); + vm.etch(address(100), address(new ArbSysMock()).code); + l2DeployedCoreContracts.coreTimelock.execute( + vars.to, + 0, + vars.data, + 0, + vars.secDeployedContracts.securityCouncilManager.generateSalt( + vars.newMembers, vars.secDeployedContracts.securityCouncilManager.updateNonce() + ) + ); + + // execute in the outbox mock + ArbSysMock.L2ToL1Tx memory l2ToL1Tx = ArbSysMock(address(100)).getTx(0); + vars.inbox.setL2ToL1Sender(l2ToL1Tx.from); + vm.prank(address(vars.inbox.bridge())); + address(l2ToL1Tx.to).call{value: l2ToL1Tx.value}(l2ToL1Tx.data); + + // parse the schedule batch args + Parser.ScheduleBatchArgs memory args = Parser.scheduleBatchArgs(l2ToL1Tx.data); + + // execute in the l1 timelock + vm.warp(block.timestamp + l1MinTimelockDelay); + vars.l1Timelock.executeBatch( + args.targets, args.values, args.payloads, args.predecessor, args.salt + ); + + // check the l1 safe updated + checkSafeUpdated(vars.moduleL1Safe, cohort1, cohort2, newCohort1, secCouncilThreshold); + + // execute the retryables and check the safes + InboxMock.RetryableTicket memory ticket1 = vars.inbox.getRetryableTicket(0); + vm.prank(applyL1ToL2Alias(ticket1.from)); + address(ticket1.to).call{value: ticket1.value}(ticket1.data); + checkSafeUpdated(vars.moduleL2Safe, cohort1, cohort2, newCohort1, secCouncilThreshold); + + InboxMock.RetryableTicket memory ticket2 = vars.inbox.getRetryableTicket(1); + vm.prank(applyL1ToL2Alias(ticket2.from)); + address(ticket2.to).call{value: ticket2.value}(ticket2.data); + checkSafeUpdated( + vars.moduleL2SafeNonEmergency, cohort1, cohort2, newCohort1, secCouncilThreshold + ); + + InboxMock.RetryableTicket memory ticket3 = vars.novaInbox.getRetryableTicket(0); + vm.prank(applyL1ToL2Alias(ticket3.from)); + address(ticket3.to).call{value: ticket3.value}(ticket3.data); + checkSafeUpdated(vars.moduleNovaSafe, cohort1, cohort2, newCohort1, secCouncilThreshold); + } + + function testE2E() public { + deploy(); + } +} diff --git a/test/security-council-mgmt/L2SecurityCouncilMgmtFactory.t.sol b/test/security-council-mgmt/L2SecurityCouncilMgmtFactory.t.sol new file mode 100644 index 00000000..9aa26e80 --- /dev/null +++ b/test/security-council-mgmt/L2SecurityCouncilMgmtFactory.t.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "../util/TestUtil.sol"; +import "../util/DeployGnosisWithModule.sol"; + +import "../../src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol"; +import "../../src/security-council-mgmt/governors/SecurityCouncilMemberRemovalGovernor.sol"; +import "../../src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol"; + +import "../../src/security-council-mgmt/SecurityCouncilManager.sol"; + +import "../../src/security-council-mgmt/interfaces/IGnosisSafe.sol"; + +contract L2SecurityCouncilMgmtFactoryTest is Test, DeployGnosisWithModule { + ChainAndUpExecLocation[] upgradeExecutors; + address govChainEmergencySecurityCouncil; + address l1ArbitrumTimelock = address(333); + address l2CoreGovTimelock; + address govChainProxyAdmin; + address[] secondCohort = new address[](1); + address[] firstCohort = new address[](1); + address l2UpgradeExecutor; + address arbToken; + uint256 l1TimelockMinDelay; + + uint256 removalGovVotingDelay = uint256(2); + uint256 removalGovVotingPeriod = uint256(3); + uint256 removalGovQuorumNumerator = uint256(4); + uint256 removalGovProposalThreshold = uint256(5); + uint64 removalGovMinPeriodAfterQuorum = uint64(6); + uint256 removalGovVoteSuccessNumerator = uint256(7); + uint256 removalProposalExpirationBlocks = uint256(137); + SecurityCouncilData[] securityCouncils; + Date firstNominationStartDate = + Date({year: uint256(2000), month: uint256(1), day: uint256(1), hour: uint256(1)}); + + uint256 nomineeVettingDuration = uint256(7); + address nomineeVetter = address(111_456); + uint256 nomineeQuorumNumerator = uint256(200); + uint256 nomineeVotingPeriod = uint256(9); + uint256 memberVotingPeriod = uint256(112); + uint256 fullWeightDuration = uint256(111); + + L2SecurityCouncilMgmtFactory fac; + address owner = address(222); + + address rando = address(11_114); + + address firstCohortMember = address(3456); + address secondCohortMember = address(7654); + + function getDeployParams() public returns (DeployParams memory deployParams) { + ChainAndUpExecLocation[] memory upgradeExecutors; + + address[] memory scOwners = new address[](2); + scOwners[0] = firstCohortMember; + scOwners[1] = secondCohortMember; + + govChainEmergencySecurityCouncil = deploySafe(scOwners, 1, address(123)); + govChainProxyAdmin = TestUtil.deployStubContract(); + l2UpgradeExecutor = TestUtil.deployStubContract(); + arbToken = TestUtil.deployStubContract(); + l2CoreGovTimelock = TestUtil.deployStubContract(); + + firstCohort[0] = firstCohortMember; + secondCohort[0] = secondCohortMember; + + vm.prank(owner); + fac = new L2SecurityCouncilMgmtFactory(); + return DeployParams({ + upgradeExecutors: upgradeExecutors, + govChainEmergencySecurityCouncil: govChainEmergencySecurityCouncil, + l1ArbitrumTimelock: l1ArbitrumTimelock, + l2CoreGovTimelock: l2CoreGovTimelock, + govChainProxyAdmin: govChainProxyAdmin, + firstCohort: firstCohort, + secondCohort: secondCohort, + l2UpgradeExecutor: l2UpgradeExecutor, + arbToken: arbToken, + l1TimelockMinDelay: l1TimelockMinDelay, + removalGovVotingDelay: removalGovVotingDelay, + removalGovVotingPeriod: removalGovVotingPeriod, + removalGovProposalThreshold: removalGovProposalThreshold, + removalGovVoteSuccessNumerator: removalGovVoteSuccessNumerator, + removalGovQuorumNumerator: removalGovQuorumNumerator, + removalGovMinPeriodAfterQuorum: removalGovMinPeriodAfterQuorum, + removalProposalExpirationBlocks: removalProposalExpirationBlocks, + securityCouncils: securityCouncils, + firstNominationStartDate: firstNominationStartDate, + nomineeVettingDuration: nomineeVettingDuration, + nomineeVetter: nomineeVetter, + nomineeQuorumNumerator: nomineeQuorumNumerator, + nomineeVotingPeriod: nomineeVotingPeriod, + memberVotingPeriod: memberVotingPeriod, + fullWeightDuration: fullWeightDuration + }); + } + + function getContractImplementations() public returns (ContractImplementations memory) { + return ContractImplementations({ + securityCouncilManager: address(new SecurityCouncilManager()), + securityCouncilMemberRemoverGov: address(new SecurityCouncilMemberRemovalGovernor()), + nomineeElectionGovernor: address(new SecurityCouncilNomineeElectionGovernor()), + memberElectionGovernor: address(new SecurityCouncilMemberElectionGovernor()) + }); + } + + function testOnlyOwnerCanDeploy() public { + DeployParams memory dp = getDeployParams(); + ContractImplementations memory ci = getContractImplementations(); + + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + fac.deploy(dp, ci); + } + + function testSecurityCouncilManagerDeployment() public { + DeployParams memory dp = getDeployParams(); + ContractImplementations memory ci = getContractImplementations(); + + vm.prank(owner); + L2SecurityCouncilMgmtFactory.DeployedContracts memory deployed = fac.deploy(dp, ci); + + SecurityCouncilManager securityCouncilManager = + SecurityCouncilManager(address(deployed.securityCouncilManager)); + + assertTrue( + securityCouncilManager.hasRole( + securityCouncilManager.DEFAULT_ADMIN_ROLE(), l2UpgradeExecutor + ), + "DAO has admin role" + ); + assertTrue( + securityCouncilManager.hasRole( + securityCouncilManager.MEMBER_ADDER_ROLE(), govChainEmergencySecurityCouncil + ), + "emergency security council has adder role" + ); + assertTrue( + securityCouncilManager.hasRole( + securityCouncilManager.MEMBER_ROTATOR_ROLE(), govChainEmergencySecurityCouncil + ), + "emergency security council has rotator role" + ); + assertTrue( + securityCouncilManager.hasRole( + securityCouncilManager.MEMBER_REPLACER_ROLE(), govChainEmergencySecurityCouncil + ), + "emergency security council has replacer role" + ); + assertTrue( + securityCouncilManager.hasRole( + securityCouncilManager.COHORT_REPLACER_ROLE(), + address(deployed.memberElectionGovernor) + ), + "memberElectionGovernor has replacer role" + ); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual( + securityCouncilManager.getFirstCohort(), firstCohort + ), + "first cohort set" + ); + assertTrue( + TestUtil.areUniqueAddressArraysEqual( + securityCouncilManager.getSecondCohort(), secondCohort + ), + "second cohort set" + ); + + assertEq( + l2CoreGovTimelock, + securityCouncilManager.l2CoreGovTimelock(), + "l2 core gov timelock set" + ); + assertEq( + address(deployed.upgradeExecRouteBuilder), + address(securityCouncilManager.router()), + "l2 core gov timelock set" + ); + } + + function testRemovalGovDeployment() public { + DeployParams memory dp = getDeployParams(); + ContractImplementations memory ci = getContractImplementations(); + + vm.prank(owner); + L2SecurityCouncilMgmtFactory.DeployedContracts memory deployed = fac.deploy(dp, ci); + + SecurityCouncilMemberRemovalGovernor rg = SecurityCouncilMemberRemovalGovernor( + payable(address(deployed.securityCouncilMemberRemoverGov)) + ); + + assertEq(rg.owner(), l2UpgradeExecutor, "upgrade exec is owner"); + assertEq( + rg.voteSuccessNumerator(), removalGovVoteSuccessNumerator, "voteSuccessNumerator set" + ); + + assertEq(rg.votingDelay(), removalGovVotingDelay, "removalGovVotingDelay set"); + assertEq(rg.votingPeriod(), removalGovVotingPeriod, "removalGovVotingPeriod set"); + assertEq(rg.quorumNumerator(), removalGovQuorumNumerator, "removalGovQuorumNumerator set"); + assertEq( + rg.proposalThreshold(), removalGovProposalThreshold, "removalGovProposalThreshold set" + ); + assertEq( + rg.lateQuorumVoteExtension(), + removalGovMinPeriodAfterQuorum, + "removalGovMinPeriodAfterQuorum set" + ); + } + + function testNomineeElectionGovDeployment() public { + DeployParams memory dp = getDeployParams(); + ContractImplementations memory ci = getContractImplementations(); + + vm.prank(owner); + L2SecurityCouncilMgmtFactory.DeployedContracts memory deployed = fac.deploy(dp, ci); + + SecurityCouncilNomineeElectionGovernor nomineeElectionGovernor = + deployed.nomineeElectionGovernor; + + assertEq( + nomineeElectionGovernor.nomineeVettingDuration(), + nomineeVettingDuration, + "nomineeVettingDuration set" + ); + assertEq(nomineeElectionGovernor.nomineeVetter(), nomineeVetter, "nomineeVetter set"); + assertEq( + address(nomineeElectionGovernor.securityCouncilManager()), + address(deployed.securityCouncilManager), + "securityCouncilManager set" + ); + assertEq(address(nomineeElectionGovernor.token()), arbToken, "token set"); + assertEq(nomineeElectionGovernor.owner(), l2UpgradeExecutor, "owner set"); + assertEq(nomineeElectionGovernor.votingPeriod(), nomineeVotingPeriod, "votingPeriod set"); + assertEq( + address(nomineeElectionGovernor.securityCouncilMemberElectionGovernor()), + address(deployed.memberElectionGovernor), + "securityCouncilMemberElectionGovernor set" + ); + assertEq( + nomineeElectionGovernor.quorumNumerator(), + nomineeQuorumNumerator, + "quorumNumeratorValue set" + ); + } + + function testMemberElectionGovDeployment() public { + DeployParams memory dp = getDeployParams(); + ContractImplementations memory ci = getContractImplementations(); + + vm.prank(owner); + L2SecurityCouncilMgmtFactory.DeployedContracts memory deployed = fac.deploy(dp, ci); + + SecurityCouncilMemberElectionGovernor memberElectionGovernor = + deployed.memberElectionGovernor; + + // assertEq(memberElectionGovernor.maxNominees(), firstCohort.length, "maxNominees set"); + assertEq( + address(memberElectionGovernor.nomineeElectionGovernor()), + address(deployed.nomineeElectionGovernor), + "nomineeElectionGovernor set" + ); + assertEq( + address(memberElectionGovernor.securityCouncilManager()), + address(deployed.securityCouncilManager), + "securityCouncilManager set" + ); + assertEq(address(memberElectionGovernor.token()), arbToken, "token set"); + assertEq(memberElectionGovernor.owner(), l2UpgradeExecutor, "owner set"); + assertEq(memberElectionGovernor.votingPeriod(), memberVotingPeriod, "votingPeriod set"); + assertEq( + memberElectionGovernor.fullWeightDuration(), + fullWeightDuration, + "fullWeightDuration set" + ); + } +} diff --git a/test/security-council-mgmt/SecurityCouncilManager.t.sol b/test/security-council-mgmt/SecurityCouncilManager.t.sol new file mode 100644 index 00000000..c68be6ce --- /dev/null +++ b/test/security-council-mgmt/SecurityCouncilManager.t.sol @@ -0,0 +1,530 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "../../src/security-council-mgmt/SecurityCouncilManager.sol"; +import "../../src/UpgradeExecRouteBuilder.sol"; + +import "../util/TestUtil.sol"; +import "../util/MockArbSys.sol"; +import "../../src/security-council-mgmt/Common.sol"; + +contract MockArbitrumTimelock { + event CallScheduled( + bytes32 indexed id, + uint256 indexed index, + address target, + uint256 value, + bytes data, + bytes32 predecessor, + uint256 delay + ); + + function getMinDelay() external view returns (uint256) { + return uint256(123); + } + + function schedule( + address target, + uint256 value, + bytes calldata data, + bytes32 predecessor, + bytes32 salt, + uint256 delay + ) public virtual { + emit CallScheduled(salt, 0, target, value, data, predecessor, delay); + } +} + +contract SecurityCouncilManagerTest is Test { + address[] firstCohort = new address[](6); + address[6] _firstCohort = + [address(1111), address(1112), address(1113), address(1114), address(1115), address(1116)]; + + address[] secondCohort = new address[](6); + address[6] _secondCohort = + [address(2221), address(2222), address(2223), address(2224), address(2225), address(2226)]; + + address[] newCohort = new address[](6); + address[6] _newCohort = + [address(3331), address(3332), address(3333), address(3334), address(3335), address(3336)]; + + address[] newCohortWithADup = new address[](6); + address dup = address(3335); + address[6] _newCohortWithADup = + [address(3331), address(3332), address(3333), address(3334), dup, dup]; + + SecurityCouncilManager scm; + UpgradeExecRouteBuilder uerb; + address[] memberRemovers = new address[](2); + address memberRemover1 = address(4444); + address memberRemover2 = address(4445); + + SecurityCouncilManagerRoles roles = SecurityCouncilManagerRoles({ + admin: address(4441), + cohortUpdator: address(4442), + memberAdder: address(4443), + memberRemovers: memberRemovers, + memberRotator: address(4446), + memberReplacer: address(4447) + }); + + address rando = address(6661); + + address memberToAdd = address(7771); + + address l1ArbitrumTimelock = address(8881); + + address payable l2CoreGovTimelock; + + uint256 l1TimelockMinDelay = uint256(1); + ChainAndUpExecLocation[] chainAndUpExecLocation; + SecurityCouncilData[] securityCouncils; + + SecurityCouncilData firstSC = SecurityCouncilData({ + securityCouncil: address(9991), + updateAction: address(9992), + chainId: 2 + }); + + SecurityCouncilData scToAdd = SecurityCouncilData({ + securityCouncil: address(9993), + updateAction: address(9994), + chainId: 3 + }); + + ChainAndUpExecLocation firstChainAndUpExecLocation = ChainAndUpExecLocation({ + chainId: 2, + location: UpExecLocation({inbox: address(9993), upgradeExecutor: address(9994)}) + }); + + ChainAndUpExecLocation secondChainAndUpExecLocation = ChainAndUpExecLocation({ + chainId: 3, + location: UpExecLocation({inbox: address(9995), upgradeExecutor: address(9996)}) + }); + + address[] bothCohorts; + + function setUp() public { + chainAndUpExecLocation.push(firstChainAndUpExecLocation); + chainAndUpExecLocation.push(secondChainAndUpExecLocation); + uerb = new UpgradeExecRouteBuilder({ + _upgradeExecutors:chainAndUpExecLocation, + _l1ArbitrumTimelock: l1ArbitrumTimelock, + _l1TimelockMinDelay: l1TimelockMinDelay + }); + for (uint256 i = 0; i < 6; i++) { + secondCohort[i] = _secondCohort[i]; + firstCohort[i] = _firstCohort[i]; + bothCohorts.push(_firstCohort[i]); + bothCohorts.push(_secondCohort[i]); + newCohort[i] = _newCohort[i]; + newCohortWithADup[i] = _newCohortWithADup[i]; + } + address prox = TestUtil.deployProxy(address(new SecurityCouncilManager())); + scm = SecurityCouncilManager(payable(prox)); + l2CoreGovTimelock = payable(address(new MockArbitrumTimelock())); + + securityCouncils.push(firstSC); + scm.initialize(firstCohort, secondCohort, securityCouncils, roles, l2CoreGovTimelock, uerb); + } + + function testInitialization() public { + vm.expectRevert("Initializable: contract is already initialized"); + scm.initialize(firstCohort, secondCohort, securityCouncils, roles, l2CoreGovTimelock, uerb); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(firstCohort, scm.getFirstCohort()), + "first cohort set" + ); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(secondCohort, scm.getSecondCohort()), + "second cohort set" + ); + + assertTrue(scm.hasRole(scm.DEFAULT_ADMIN_ROLE(), roles.admin), "admin role set"); + assertTrue( + scm.hasRole(scm.COHORT_REPLACER_ROLE(), roles.cohortUpdator), + "election executor role set" + ); + assertTrue(scm.hasRole(scm.MEMBER_ADDER_ROLE(), roles.memberAdder), "member adder role set"); + assertTrue( + scm.hasRole(scm.MEMBER_REMOVER_ROLE(), roles.memberRemovers[0]), + "member remover role set" + ); + assertTrue( + scm.hasRole(scm.MEMBER_REMOVER_ROLE(), roles.memberRemovers[1]), + "member remover role set" + ); + assertTrue( + scm.hasRole(scm.MEMBER_ROTATOR_ROLE(), roles.memberRotator), + "member memberRotator role set" + ); + assertEq(l2CoreGovTimelock, scm.l2CoreGovTimelock(), "l2CoreGovTimelock set"); + + assertEq(address(uerb), address(scm.router()), "exec router set"); + } + + function testRemoveMemberAffordances() public { + vm.prank(rando); + vm.expectRevert(); + scm.removeMember(rando); + + vm.prank(roles.memberRemovers[0]); + vm.expectRevert(abi.encodeWithSelector(ISecurityCouncilManager.NotAMember.selector, rando)); + scm.removeMember(rando); + } + + function testRemoveMember() public { + vm.recordLogs(); + removeFirstMember(); + checkScheduleWasCalled(); + + address[] memory remainingMembers = new address[](5); + for (uint256 i = 1; i < firstCohort.length; i++) { + remainingMembers[i - 1] = firstCohort[i]; + } + assertTrue( + TestUtil.areUniqueAddressArraysEqual(remainingMembers, scm.getFirstCohort()), + "member removed from first chohort" + ); + } + + function testAddMemberSpecialAddresses() public { + vm.prank(roles.memberAdder); + vm.expectRevert(ZeroAddress.selector); + scm.addMember(address(0), Cohort.FIRST); + } + + function testAddMemberAffordances() public { + vm.prank(rando); + vm.expectRevert(); + scm.addMember(memberToAdd, Cohort.FIRST); + + vm.prank(roles.memberAdder); + vm.expectRevert( + abi.encodeWithSelector(ISecurityCouncilManager.CohortFull.selector, Cohort.FIRST) + ); + scm.addMember(memberToAdd, Cohort.FIRST); + + removeFirstMember(); + + vm.prank(roles.memberAdder); + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.MemberInCohort.selector, firstCohort[1], Cohort.FIRST + ) + ); + scm.addMember(firstCohort[1], Cohort.FIRST); + } + + function testAddMemberToFirstCohort() public { + removeFirstMember(); + vm.startPrank(roles.memberAdder); + vm.recordLogs(); + scm.addMember(memberToAdd, Cohort.FIRST); + checkScheduleWasCalled(); + vm.stopPrank(); + address[] memory newFirstCohort = new address[](6); + for (uint256 i = 1; i < firstCohort.length; i++) { + newFirstCohort[i - 1] = firstCohort[i]; + } + newFirstCohort[5] = memberToAdd; + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newFirstCohort, scm.getFirstCohort()), + "member added to first chohort" + ); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(secondCohort, scm.getSecondCohort()), + "second cohort untouched" + ); + } + + function testAddMemberToSecondCohort() public { + vm.prank(roles.memberRemovers[0]); + scm.removeMember(secondCohort[0]); + + vm.startPrank(roles.memberAdder); + vm.recordLogs(); + scm.addMember(memberToAdd, Cohort.SECOND); + checkScheduleWasCalled(); + vm.stopPrank(); + address[] memory newSecondCohort = new address[](6); + for (uint256 i = 1; i < secondCohort.length; i++) { + newSecondCohort[i - 1] = secondCohort[i]; + } + newSecondCohort[5] = memberToAdd; + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newSecondCohort, scm.getSecondCohort()), + "member added to second chohort" + ); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(firstCohort, scm.getFirstCohort()), + "first cohort untouched" + ); + } + + function testReplaceMemberAffordances() public { + vm.prank(rando); + vm.expectRevert(); + scm.replaceMember(rando, rando); + + vm.startPrank(roles.memberReplacer); + vm.expectRevert( + abi.encodeWithSelector(ISecurityCouncilManager.NotAMember.selector, memberToAdd) + ); + scm.replaceMember(memberToAdd, rando); + + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.MemberInCohort.selector, firstCohort[1], Cohort.FIRST + ) + ); + scm.replaceMember(firstCohort[0], firstCohort[1]); + + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.MemberInCohort.selector, secondCohort[0], Cohort.SECOND + ) + ); + scm.replaceMember(firstCohort[0], secondCohort[0]); + vm.stopPrank(); + } + + function testReplaceMemberInFirstCohort() public { + vm.startPrank(roles.memberReplacer); + vm.recordLogs(); + scm.replaceMember(firstCohort[0], memberToAdd); + checkScheduleWasCalled(); + + address[] memory newFirstCohortArray = new address[](6); + newFirstCohortArray[0] = memberToAdd; + for (uint256 i = 1; i < firstCohort.length; i++) { + newFirstCohortArray[i] = firstCohort[i]; + } + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newFirstCohortArray, scm.getFirstCohort()), + "first cohort updated" + ); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(secondCohort, scm.getSecondCohort()), + "second cohort untouched" + ); + vm.stopPrank(); + } + + function testReplaceMemberInSecondCohort() public { + vm.startPrank(roles.memberReplacer); + vm.recordLogs(); + scm.replaceMember(secondCohort[0], memberToAdd); + checkScheduleWasCalled(); + address[] memory newSecondCohortArray = new address[](6); + newSecondCohortArray[0] = memberToAdd; + for (uint256 i = 1; i < secondCohort.length; i++) { + newSecondCohortArray[i] = secondCohort[i]; + } + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newSecondCohortArray, scm.getSecondCohort()), + "second cohort updated" + ); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(firstCohort, scm.getFirstCohort()), + "first cohort untouched" + ); + vm.stopPrank(); + } + + function testRotateMember() public { + vm.startPrank(roles.memberRotator); + vm.recordLogs(); + scm.rotateMember(firstCohort[0], memberToAdd); + checkScheduleWasCalled(); + + address[] memory newFirstCohortArray = new address[](6); + newFirstCohortArray[0] = memberToAdd; + for (uint256 i = 1; i < firstCohort.length; i++) { + newFirstCohortArray[i] = firstCohort[i]; + } + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newFirstCohortArray, scm.getFirstCohort()), + "first cohort rotated" + ); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(secondCohort, scm.getSecondCohort()), + "second cohort untouched" + ); + vm.stopPrank(); + } + + function testAddSCAffordances() public { + vm.prank(rando); + vm.expectRevert(); + scm.addSecurityCouncil(scToAdd); + + vm.startPrank(roles.admin); + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.SecurityCouncilAlreadyInRouter.selector, firstSC + ) + ); + scm.addSecurityCouncil(firstSC); + + SecurityCouncilData memory scWithChainNotInRouter = SecurityCouncilData({ + securityCouncil: address(9991), + updateAction: address(9992), + chainId: 4 + }); + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.SecurityCouncilNotInRouter.selector, scWithChainNotInRouter + ) + ); + scm.addSecurityCouncil(scWithChainNotInRouter); + vm.stopPrank(); + } + + function testAddSC() public { + uint256 len = scm.securityCouncilsLength(); + vm.prank(roles.admin); + scm.addSecurityCouncil(scToAdd); + assertEq(len + 1, scm.securityCouncilsLength(), "confimred new SC added"); + + (address scAddress, address action, uint256 chainid) = + scm.securityCouncils(scm.securityCouncilsLength() - 1); + + assertEq(scAddress, scToAdd.securityCouncil, "confimred new SC added"); + assertEq(chainid, scToAdd.chainId, "confimred new SC added"); + } + + function testRemoveSCAffordances() public { + vm.prank(rando); + vm.expectRevert(); + scm.removeSecurityCouncil(firstSC); + + vm.prank(roles.admin); + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.SecurityCouncilNotInManager.selector, scToAdd + ) + ); + scm.removeSecurityCouncil(scToAdd); + } + + function testRemoveSeC() public { + vm.prank(roles.admin); + scm.removeSecurityCouncil(firstSC); + assertEq(scm.securityCouncilsLength(), 0, "SC removed"); + } + + function testUpdateCohortAffordances() public { + vm.prank(rando); + vm.expectRevert(); + scm.replaceCohort(newCohort, Cohort.FIRST); + + vm.startPrank(roles.cohortUpdator); + address[] memory newSmallCohort = new address[](1); + newSmallCohort[0] = rando; + vm.expectRevert( + abi.encodeWithSelector( + ISecurityCouncilManager.InvalidNewCohortLength.selector, + newSmallCohort, + newCohort.length + ) + ); + scm.replaceCohort(newSmallCohort, Cohort.FIRST); + vm.stopPrank(); + } + + function testUpdateFirstCohort() public { + vm.startPrank(roles.cohortUpdator); + + vm.recordLogs(); + scm.replaceCohort(newCohort, Cohort.FIRST); + checkScheduleWasCalled(); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newCohort, scm.getFirstCohort()), + "first cohort updated" + ); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(secondCohort, scm.getSecondCohort()), + "second cohort untouched" + ); + } + + function testUpdateSecondCohort() public { + vm.startPrank(roles.cohortUpdator); + + vm.recordLogs(); + scm.replaceCohort(newCohort, Cohort.SECOND); + checkScheduleWasCalled(); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newCohort, scm.getSecondCohort()), + "second cohort updated" + ); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(firstCohort, scm.getFirstCohort()), + "first cohort untouched" + ); + } + + function testCantUpdateCohortWithADup() public { + vm.startPrank(roles.cohortUpdator); + vm.expectRevert( + abi.encodeWithSelector(ISecurityCouncilManager.MemberInCohort.selector, dup, 1) + ); + scm.replaceCohort(newCohortWithADup, Cohort.SECOND); + } + + function testUpdateRouterAffordacnes() public { + UpgradeExecRouteBuilder newRouter = UpgradeExecRouteBuilder(TestUtil.deployStubContract()); + vm.prank(rando); + vm.expectRevert(); + scm.setUpgradeExecRouteBuilder(newRouter); + + vm.prank(roles.admin); + vm.expectRevert(abi.encodeWithSelector(NotAContract.selector, rando)); + scm.setUpgradeExecRouteBuilder(UpgradeExecRouteBuilder(rando)); + } + + function testUpdateRouter() public { + UpgradeExecRouteBuilder newRouter = UpgradeExecRouteBuilder(TestUtil.deployStubContract()); + vm.prank(roles.admin); + scm.setUpgradeExecRouteBuilder(UpgradeExecRouteBuilder(newRouter)); + assertEq(address(newRouter), address(scm.router()), "router set"); + } + + function testCohortMethods() public { + assertTrue(scm.firstCohortIncludes(firstCohort[0]), "firstCohortIncludes works"); + assertFalse(scm.firstCohortIncludes(secondCohort[0]), "firstCohortIncludes works"); + assertTrue(scm.secondCohortIncludes(secondCohort[0]), "secondCohortIncludes works"); + assertFalse(scm.secondCohortIncludes(firstCohort[0]), "secondCohortIncludes works"); + + assertTrue( + TestUtil.areUniqueAddressArraysEqual(scm.getBothCohorts(), bothCohorts), + "getBothCohorts works" + ); + } + + // // helpers + function checkScheduleWasCalled() internal { + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq( + entries[0].topics[0], + keccak256("CallScheduled(bytes32,uint256,address,uint256,bytes,bytes32,uint256)"), + "ArbSysL2ToL1Tx emitted" + ); + } + + function removeFirstMember() internal { + address memberToRemove = firstCohort[0]; + vm.prank(roles.memberRemovers[0]); + scm.removeMember(memberToRemove); + } +} diff --git a/test/security-council-mgmt/SecurityCouncilMemberRemovalGovernor.t.sol b/test/security-council-mgmt/SecurityCouncilMemberRemovalGovernor.t.sol new file mode 100644 index 00000000..54e234c6 --- /dev/null +++ b/test/security-council-mgmt/SecurityCouncilMemberRemovalGovernor.t.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "../../src/security-council-mgmt/governors/SecurityCouncilMemberRemovalGovernor.sol"; +import "../../src/security-council-mgmt/interfaces/ISecurityCouncilManager.sol"; +import "../../src/L2ArbitrumToken.sol"; +import "./../util/TestUtil.sol"; + +import "@openzeppelin/contracts-upgradeable/governance/IGovernorUpgradeable.sol"; + +address constant memberToRemove = address(999_999); + +contract MockSecurityCouncilManager { + function firstCohortIncludes(address x) external returns (bool) { + return x == memberToRemove; + } + + function secondCohortIncludes(address x) external returns (bool) { + return false; + } +} + +contract SecurityCouncilMemberRemovalGovernorTest is Test { + address l1TokenAddress = address(137); + uint256 initialTokenSupply = 50_000; + address tokenOwner = address(238); + uint256 votingPeriod = 180_000; + uint256 votingDelay = 10; + address excludeListMember = address(339); + uint256 quorumNumerator = 500; + uint256 proposalThreshold = 0; + uint64 initialVoteExtension = 5; + uint256 proposalExpirationBlocks = 15; + + address[] stubAddressArray = [address(640)]; + address someRando = address(741); + address owner = address(842); + uint256 voteSuccessNumerator = 6000; + ISecurityCouncilManager securityCouncilManager; + SecurityCouncilMemberRemovalGovernor scRemovalGov; + + address[] validTargets = new address[](1); + uint256[] validValues = new uint256[](1); + + bytes[] validCallDatas = new bytes[](1); + + string description = "xyz"; + + address rando = address(98_765); + + uint256 newVoteSuccessNumerator = 3; + + address secondTokenHolder = address(23_456_789); + address thirdTokenHolder = address(3_389_234); + + function setUp() public returns (SecurityCouncilMemberRemovalGovernor) { + L2ArbitrumToken token = + L2ArbitrumToken(TestUtil.deployProxy(address(new L2ArbitrumToken()))); + token.initialize(l1TokenAddress, initialTokenSupply, tokenOwner); + + vm.startPrank(tokenOwner); + token.delegate(tokenOwner); + token.transfer(secondTokenHolder, 19_999); + token.transfer(thirdTokenHolder, 5001); + vm.stopPrank(); + + vm.prank(secondTokenHolder); + token.delegate(secondTokenHolder); + + vm.prank(thirdTokenHolder); + token.delegate(thirdTokenHolder); + + scRemovalGov = SecurityCouncilMemberRemovalGovernor( + payable(TestUtil.deployProxy(address(new SecurityCouncilMemberRemovalGovernor()))) + ); + + securityCouncilManager = ISecurityCouncilManager(address(new MockSecurityCouncilManager())); + scRemovalGov.initialize( + voteSuccessNumerator, + securityCouncilManager, + token, + owner, + votingDelay, + votingPeriod, + quorumNumerator, + proposalThreshold, + initialVoteExtension, + proposalExpirationBlocks + ); + validTargets[0] = address(securityCouncilManager); + validCallDatas[0] = + abi.encodeWithSelector(ISecurityCouncilManager.removeMember.selector, memberToRemove); + + return scRemovalGov; + } + + function testInitFails() public { + L2ArbitrumToken token = + L2ArbitrumToken(TestUtil.deployProxy(address(new L2ArbitrumToken()))); + token.initialize(l1TokenAddress, initialTokenSupply, tokenOwner); + SecurityCouncilMemberRemovalGovernor scRemovalGov2 = SecurityCouncilMemberRemovalGovernor( + payable(TestUtil.deployProxy(address(new SecurityCouncilMemberRemovalGovernor()))) + ); + vm.expectRevert(abi.encodeWithSelector(NotAContract.selector, address(123_256_123))); + scRemovalGov2.initialize( + voteSuccessNumerator, + ISecurityCouncilManager(address(123_256_123)), + token, + owner, + votingDelay, + votingPeriod, + quorumNumerator, + proposalThreshold, + initialVoteExtension, + proposalExpirationBlocks + ); + } + + function testSuccessfulProposalAndCantAbstain() public { + uint256 proposalId = + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Pending, + "proposal created" + ); + vm.roll(block.number + votingDelay + 1); + vm.expectRevert( + abi.encodeWithSelector(SecurityCouncilMemberRemovalGovernor.AbstainDisallowed.selector) + ); + scRemovalGov.castVote(proposalId, 2); + } + + function testRelay() public { + // make sure relay can only be called by owner + vm.expectRevert("Ownable: caller is not the owner"); + scRemovalGov.relay(address(0), 0, new bytes(0)); + + // make sure relay can be called by owner, and that we can call an onlyGovernance function + vm.prank(owner); + scRemovalGov.relay( + address(scRemovalGov), + 0, + abi.encodeWithSelector(scRemovalGov.setVotingPeriod.selector, 121_212) + ); + assertEq(scRemovalGov.votingPeriod(), 121_212); + } + + function testProposalCreationTargetRestriction() public { + validTargets[0] = rando; + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.TargetNotManager.selector, rando + ) + ); + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + } + + function testProposalCreationValuesRestriction() public { + validValues[0] = 1; + + vm.expectRevert( + abi.encodeWithSelector(SecurityCouncilMemberRemovalGovernor.ValueNotZero.selector, 1) + ); + + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + } + + function testProposalCreationCallRestriction() public { + validCallDatas[0] = + abi.encodeWithSelector(ISecurityCouncilManager.addMember.selector, memberToRemove); + + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.CallNotRemoveMember.selector, + ISecurityCouncilManager.addMember.selector, + ISecurityCouncilManager.removeMember.selector + ) + ); + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + } + + function testProposalCreationCallParamRestriction() public { + validCallDatas[0] = + abi.encodeWithSelector(ISecurityCouncilManager.removeMember.selector, rando); + + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.MemberNotFound.selector, rando + ) + ); + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + } + + function testProposalCreationTargetLen() public { + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.InvalidOperationsLength.selector, 2 + ) + ); + scRemovalGov.propose(new address[](2), validValues, validCallDatas, description); + } + + function testProposalCreationUnexpectedCallDataLen() public { + validCallDatas[0] = abi.encodeWithSelector(ISecurityCouncilManager.removeMember.selector); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.UnexpectedCalldataLength.selector, 4 + ) + ); + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + } + + function testSetVoteSuccessNumeratorAffordance() public { + vm.prank(rando); + vm.expectRevert("Ownable: caller is not the owner"); + scRemovalGov.setVoteSuccessNumerator(newVoteSuccessNumerator); + + vm.prank(owner); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.InvalidVoteSuccessNumerator.selector, 0 + ) + ); + scRemovalGov.setVoteSuccessNumerator(0); + + vm.prank(owner); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberRemovalGovernor.InvalidVoteSuccessNumerator.selector, 10_001 + ) + ); + scRemovalGov.setVoteSuccessNumerator(10_001); + } + + function testSetVoteSuccessNumerator() public { + vm.prank(owner); + scRemovalGov.setVoteSuccessNumerator(newVoteSuccessNumerator); + assertEq( + scRemovalGov.voteSuccessNumerator(), + newVoteSuccessNumerator, + "newVoteSuccessNumerator set" + ); + } + + function testSuccessNumeratorInsufficientVotes() public { + uint256 proposalId = + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Pending, + "proposal created" + ); + vm.roll(block.number + votingDelay + 1); + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Active, + "proposal active" + ); + + vm.prank(tokenOwner); + scRemovalGov.castVote(proposalId, 1); + + vm.prank(secondTokenHolder); + scRemovalGov.castVote(proposalId, 0); + + vm.prank(thirdTokenHolder); + scRemovalGov.castVote(proposalId, 0); + + vm.roll(block.number + votingPeriod + 1); + + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Defeated, + "prposal fails if 50% for in favor" + ); + } + + function testSuccessNumeratorSufficientVotes() public { + uint256 proposalId = + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Pending, + "proposal created" + ); + vm.roll(block.number + votingDelay + 1); + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Active, + "proposal active" + ); + + vm.prank(tokenOwner); + scRemovalGov.castVote(proposalId, 1); + + vm.prank(secondTokenHolder); + scRemovalGov.castVote(proposalId, 0); + + vm.prank(thirdTokenHolder); + scRemovalGov.castVote(proposalId, 1); + + vm.roll(block.number + votingPeriod + 1); + + assertTrue( + scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Succeeded, + "proposal succeeds when over 60% vote in favor" + ); + } + + function testSeparateSelector() public { + bytes memory calldataWithSelector = abi.encodeWithSelector( + MockSecurityCouncilManager.firstCohortIncludes.selector, memberToRemove + ); + (bytes4 selector, bytes memory data) = scRemovalGov.separateSelector(calldataWithSelector); + assertEq( + selector, + MockSecurityCouncilManager.firstCohortIncludes.selector, + "separateSelector returns correct selector" + ); + assertEq(data, abi.encode(memberToRemove), "separateSelector returns correct data"); + assertEq( + abi.decode(data, (address)), memberToRemove, "separateSelector decodes to correct data" + ); + } + + function testProposalExpirationDeadline() public { + uint256 proposalId = + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + + assertEq( + scRemovalGov.proposalExpirationDeadline(proposalId), + scRemovalGov.proposalDeadline(proposalId) + scRemovalGov.proposalExpirationBlocks() + ); + } + + function testProposalDoesExpire() public { + uint256 proposalId = + scRemovalGov.propose(validTargets, validValues, validCallDatas, description); + + // make the proposal succeed + vm.roll(scRemovalGov.proposalDeadline(proposalId)); + vm.prank(tokenOwner); + scRemovalGov.castVote(proposalId, 1); + + // roll to right before expiration + vm.roll(scRemovalGov.proposalExpirationDeadline(proposalId)); + + assertTrue(scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Succeeded); + + // roll to the end of the expiration period + vm.roll(scRemovalGov.proposalExpirationDeadline(proposalId) + 1); + + assertTrue(scRemovalGov.state(proposalId) == IGovernorUpgradeable.ProposalState.Expired); + } +} diff --git a/test/security-council-mgmt/SecurityCouncilMemberSyncAction.t.sol b/test/security-council-mgmt/SecurityCouncilMemberSyncAction.t.sol new file mode 100644 index 00000000..1f941474 --- /dev/null +++ b/test/security-council-mgmt/SecurityCouncilMemberSyncAction.t.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "@gnosis.pm/safe-contracts/contracts/GnosisSafeL2.sol"; +import "../util/TestUtil.sol"; +import "../util/DeployGnosisWithModule.sol"; +import "../../src/UpgradeExecutor.sol"; + +import "../../src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol"; +import "../../src/security-council-mgmt/interfaces/IGnosisSafe.sol"; + +contract SecurityCouncilMemberSyncActionTest is Test, DeployGnosisWithModule { + address admin = address(1111); + address executor = address(2222); + + address[] owners = [ + address(3333), + address(4444), + address(5555), + address(6666), + address(7777), + address(8888), + address(9999), + address(10_111), + address(110_111), + address(121_111), + address(131_111), + address(141_111) + ]; + + struct Contracts { + UpgradeExecutor upgradeExecutor; + SecurityCouncilMemberSyncAction action; + KeyValueStore kvStore; + address safe; + uint256 threshold; + } + + function _deploy(address[] memory initialMembers, uint256 threshold) + internal + returns (Contracts memory) + { + UpgradeExecutor upgradeExecutor = + UpgradeExecutor(TestUtil.deployProxy(address(new UpgradeExecutor()))); + address[] memory executors = new address[](1); + executors[0] = executor; + upgradeExecutor.initialize(admin, executors); + + address safe = deploySafe(initialMembers, threshold, address(upgradeExecutor)); + KeyValueStore kvStore = new KeyValueStore(); + SecurityCouncilMemberSyncAction action = new SecurityCouncilMemberSyncAction(kvStore); + + return Contracts(upgradeExecutor, action, kvStore, safe, threshold); + } + + function updateMembersTest( + Contracts memory contracts, + address[] memory newMembers, + uint256 nonce + ) internal { + bytes memory upgradeCallData = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, contracts.safe, newMembers, nonce + ); + vm.prank(executor); + contracts.upgradeExecutor.execute(address(contracts.action), upgradeCallData); + assertTrue( + TestUtil.areUniqueAddressArraysEqual( + newMembers, IGnosisSafe(contracts.safe).getOwners() + ), + "updated sucessfully" + ); + assertEq( + IGnosisSafe(contracts.safe).getThreshold(), contracts.threshold, "threshold preserved" + ); + } + + function testNoopUpdate() public { + address[] memory newMembers = owners; + address[] memory prevMembers = owners; + + updateMembersTest(_deploy(prevMembers, 9), newMembers, 1); + } + + function testRemoveOne() public { + address[] memory newMembers = new address[](11); + + for (uint256 i = 0; i < 11; i++) { + newMembers[i] = owners[i]; + } + address[] memory prevMembers = owners; + updateMembersTest(_deploy(prevMembers, 9), newMembers, 1); + } + + function testAddOne() public { + address[] memory prevMembers = new address[](11); + + for (uint256 i = 0; i < 11; i++) { + prevMembers[i] = owners[i]; + } + address[] memory newMembers = owners; + updateMembersTest(_deploy(prevMembers, 9), newMembers, 1); + } + + function testUpdateCohort() public { + address[] memory prevMembers = owners; + address[] memory newMembers = new address[](12); + + address[6] memory newCohort = [ + address(16_111), + address(18_111), + address(19_111), + address(200_111), + address(210_111), + address(220_111) + ]; + + for (uint256 i = 0; i < 6; i++) { + newMembers[i] = newCohort[i]; + } + for (uint256 i = 6; i < owners.length; i++) { + newMembers[i] = owners[i]; + } + updateMembersTest(_deploy(prevMembers, 9), newMembers, 1); + } + + function testCantDropBelowThreshhold() public { + address[] memory prevMembers = owners; + address[] memory newMembers = new address[](8); + for (uint256 i = 0; i < 8; i++) { + newMembers[i] = owners[i]; + } + UpgradeExecutor upgradeExecutor = + UpgradeExecutor(TestUtil.deployProxy(address(new UpgradeExecutor()))); + address[] memory executors = new address[](1); + executors[0] = executor; + upgradeExecutor.initialize(admin, executors); + + address safe = deploySafe(prevMembers, 9, address(upgradeExecutor)); + + address action = address(new SecurityCouncilMemberSyncAction(new KeyValueStore())); + + bytes memory upgradeCallData = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, safe, newMembers + ); + + // [8], [9], [10] sucessfully get removed, leaving [11]'s previous owner to be [7]; removing [11] reverts as it's below the threshold, + bytes memory revertingRemoveMemberCall = abi.encodeWithSelector( + IGnosisSafe.removeOwner.selector, prevMembers[7], prevMembers[11], 9 + ); + vm.prank(executor); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.ExecFromModuleError.selector, + revertingRemoveMemberCall, + safe + ) + ); + upgradeExecutor.execute(action, upgradeCallData); + } + + function testGetPrevOwner() public { + UpgradeExecutor upgradeExecutor = + UpgradeExecutor(TestUtil.deployProxy(address(new UpgradeExecutor()))); + address[] memory executors = new address[](1); + executors[0] = executor; + upgradeExecutor.initialize(admin, executors); + + IGnosisSafe safe = IGnosisSafe(deploySafe(owners, 9, address(upgradeExecutor))); + + SecurityCouncilMemberSyncAction action = + new SecurityCouncilMemberSyncAction(new KeyValueStore()); + + assertEq( + action.SENTINEL_OWNERS(), + action.getPrevOwner(safe, owners[0]), + "sentinel owners is first owner's prev owner" + ); + + for (uint256 i = 1; i < owners.length; i++) { + assertEq(owners[i - 1], action.getPrevOwner(safe, owners[i]), "prev owner as exected"); + } + } + + function testNonces() public { + Contracts memory contracts = _deploy(owners, 9); + + uint256 nonceKey = contracts.action.computeKey(uint160(contracts.safe)); + + assertEq( + contracts.kvStore.get(address(contracts.upgradeExecutor), nonceKey), + 0, + "initial nonce is 0" + ); + + // push through an update + updateMembersTest(contracts, owners, 2); + + // make sure nonce is updated + assertEq( + contracts.kvStore.get(address(contracts.upgradeExecutor), nonceKey), 2, "nonce is 2" + ); + + address[] memory newMembers = new address[](11); + for (uint256 i = 0; i < 11; i++) { + newMembers[i] = owners[i]; + } + + // updates with nonce == to prev nonce should fail + bytes memory upgradeCallData = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, contracts.safe, newMembers, 2 + ); + vm.prank(executor); + contracts.upgradeExecutor.execute(address(contracts.action), upgradeCallData); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(owners, IGnosisSafe(contracts.safe).getOwners()), + "old members preserved" + ); + + assertEq( + contracts.kvStore.get(address(contracts.upgradeExecutor), nonceKey), + 2, + "nonce is still 2" + ); + + // updates with nonce < prev nonce should fail + upgradeCallData = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, contracts.safe, newMembers, 1 + ); + vm.prank(executor); + contracts.upgradeExecutor.execute(address(contracts.action), upgradeCallData); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(owners, IGnosisSafe(contracts.safe).getOwners()), + "old members preserved" + ); + assertEq( + contracts.kvStore.get(address(contracts.upgradeExecutor), nonceKey), + 2, + "nonce is still 2" + ); + + updateMembersTest(contracts, owners, 1); + + // updates with nonce > prev nonce should succeed + updateMembersTest(contracts, owners, 3); + + assertEq( + contracts.kvStore.get(address(contracts.upgradeExecutor), nonceKey), 3, "nonce is now 3" + ); + } +} diff --git a/test/security-council-mgmt/SecurityCouncilMgmtUtils.t.sol b/test/security-council-mgmt/SecurityCouncilMgmtUtils.t.sol new file mode 100644 index 00000000..fbfbd24d --- /dev/null +++ b/test/security-council-mgmt/SecurityCouncilMgmtUtils.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "../../src/security-council-mgmt/SecurityCouncilMgmtUtils.sol"; +import "../util/TestUtil.sol"; + +contract SecurityCouncilMgmtUtilsTests is Test { + function testIsInArray() public { + address[] memory arr1 = new address[](0); + assertFalse( + SecurityCouncilMgmtUtils.isInArray(address(1), arr1), "isInArray empty array false" + ); + address[] memory arr2 = new address[](2); + arr2[0] = address(1); + arr2[1] = address(2); + assertTrue( + SecurityCouncilMgmtUtils.isInArray(address(1), arr2), "isInArray first element true" + ); + assertTrue( + SecurityCouncilMgmtUtils.isInArray(address(2), arr2), "isInArray second element true" + ); + assertFalse( + SecurityCouncilMgmtUtils.isInArray(address(3), arr2), "isInArray other element false" + ); + } +} diff --git a/test/security-council-mgmt/SecurityCouncilUpgradeAction.t.sol b/test/security-council-mgmt/SecurityCouncilUpgradeAction.t.sol new file mode 100644 index 00000000..aa67cd96 --- /dev/null +++ b/test/security-council-mgmt/SecurityCouncilUpgradeAction.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "@gnosis.pm/safe-contracts/contracts/GnosisSafeL2.sol"; +import "../util/TestUtil.sol"; +import "../util/DeployGnosisWithModule.sol"; +import "../../src/UpgradeExecutor.sol"; + +import "../../src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol"; +import "../../src/security-council-mgmt/interfaces/IGnosisSafe.sol"; + +contract SecurityCouncilMemberSyncActionTest is Test, DeployGnosisWithModule { + address admin = address(1111); + address executor = address(2222); + + address[] owners = [ + address(3333), + address(4444), + address(5555), + address(6666), + address(7777), + address(8888), + address(9999), + address(10_111), + address(110_111), + address(121_111), + address(131_111), + address(141_111) + ]; + + function updateMembersTest( + address[] memory initialMembers, + address[] memory newMembers, + uint256 threshold + ) internal { + UpgradeExecutor upgradeExecutor = + UpgradeExecutor(TestUtil.deployProxy(address(new UpgradeExecutor()))); + address[] memory executors = new address[](1); + executors[0] = executor; + upgradeExecutor.initialize(admin, executors); + + address safe = deploySafe(initialMembers, threshold, address(upgradeExecutor)); + + address action = address(new SecurityCouncilMemberSyncAction(new KeyValueStore())); + + bytes memory upgradeCallData = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, safe, newMembers + ); + vm.prank(executor); + upgradeExecutor.execute(action, upgradeCallData); + assertTrue( + TestUtil.areUniqueAddressArraysEqual(newMembers, IGnosisSafe(safe).getOwners()), + "updated sucessfully" + ); + assertEq(IGnosisSafe(safe).getThreshold(), threshold, "threshold preserved"); + } + + function testNoopUpdate() public { + address[] memory newMembers = owners; + address[] memory prevMembers = owners; + + updateMembersTest(prevMembers, newMembers, 9); + } + + function testRemoveOne() public { + address[] memory newMembers = new address[](11); + + for (uint256 i = 0; i < 11; i++) { + newMembers[i] = owners[i]; + } + address[] memory prevMembers = owners; + updateMembersTest(prevMembers, newMembers, 9); + } + + function testAddOne() public { + address[] memory prevMembers = new address[](11); + + for (uint256 i = 0; i < 11; i++) { + prevMembers[i] = owners[i]; + } + address[] memory newMembers = owners; + updateMembersTest(prevMembers, newMembers, 9); + } + + function testUpdateCohort() public { + address[] memory prevMembers = owners; + address[] memory newMembers = new address[](12); + + address[6] memory newCohort = [ + address(16_111), + address(18_111), + address(19_111), + address(200_111), + address(210_111), + address(220_111) + ]; + + for (uint256 i = 0; i < 6; i++) { + newMembers[i] = newCohort[i]; + } + for (uint256 i = 6; i < owners.length; i++) { + newMembers[i] = owners[i]; + } + updateMembersTest(prevMembers, newMembers, 9); + } + + function testCantDropBelowThreshhold() public { + address[] memory prevMembers = owners; + address[] memory newMembers = new address[](8); + for (uint256 i = 0; i < 8; i++) { + newMembers[i] = owners[i]; + } + UpgradeExecutor upgradeExecutor = + UpgradeExecutor(TestUtil.deployProxy(address(new UpgradeExecutor()))); + address[] memory executors = new address[](1); + executors[0] = executor; + upgradeExecutor.initialize(admin, executors); + + address safe = deploySafe(prevMembers, 9, address(upgradeExecutor)); + + address action = address(new SecurityCouncilMemberSyncAction(new KeyValueStore())); + + bytes memory upgradeCallData = abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.perform.selector, safe, newMembers + ); + + // [8], [9], [10] sucessfully get removed, leaving [11]'s previous owner to be [7]; removing [11] reverts as it's below the threshold, + bytes memory revertingRemoveMemberCall = abi.encodeWithSelector( + IGnosisSafe.removeOwner.selector, prevMembers[7], prevMembers[11], 9 + ); + vm.prank(executor); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberSyncAction.ExecFromModuleError.selector, + revertingRemoveMemberCall, + safe + ) + ); + upgradeExecutor.execute(action, upgradeCallData); + } + + function testGetPrevOwner() public { + UpgradeExecutor upgradeExecutor = + UpgradeExecutor(TestUtil.deployProxy(address(new UpgradeExecutor()))); + address[] memory executors = new address[](1); + executors[0] = executor; + upgradeExecutor.initialize(admin, executors); + + IGnosisSafe safe = IGnosisSafe(deploySafe(owners, 9, address(upgradeExecutor))); + + SecurityCouncilMemberSyncAction action = + new SecurityCouncilMemberSyncAction(new KeyValueStore()); + + assertEq( + action.SENTINEL_OWNERS(), + action.getPrevOwner(safe, owners[0]), + "sentinel owners is first owner's prev owner" + ); + + for (uint256 i = 1; i < owners.length; i++) { + assertEq(owners[i - 1], action.getPrevOwner(safe, owners[i]), "prev owner as exected"); + } + } +} diff --git a/test/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.t.sol b/test/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.t.sol new file mode 100644 index 00000000..eadef41e --- /dev/null +++ b/test/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.t.sol @@ -0,0 +1,885 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import "../../util/TestUtil.sol"; +import "../../../src/security-council-mgmt/governors/modules/ElectionGovernor.sol"; + +import "../../../src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol"; +import "../../../src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol"; + +contract SecurityCouncilMemberElectionGovernorTest is Test { + struct InitParams { + ISecurityCouncilNomineeElectionGovernor nomineeElectionGovernor; + ISecurityCouncilManager securityCouncilManager; + IVotesUpgradeable token; + address owner; + uint256 votingPeriod; + uint256 maxNominees; + uint256 fullWeightDuration; + } + + SecurityCouncilMemberElectionGovernor governor; + address proxyAdmin = address(0x11); + + InitParams initParams = InitParams({ + nomineeElectionGovernor: ISecurityCouncilNomineeElectionGovernor(payable(address(0x22))), + securityCouncilManager: ISecurityCouncilManager(address(0x33)), + token: IVotesUpgradeable(address(0x44)), + owner: address(0x55), + votingPeriod: 2 ** 8, + maxNominees: 6, + fullWeightDuration: 2 ** 7 + }); + + address[] compliantNominees; + + function setUp() public { + governor = _deployGovernor(); + + vm.etch(address(initParams.nomineeElectionGovernor), "0x23"); + vm.etch(address(initParams.securityCouncilManager), "0x34"); + + governor.initialize({ + _nomineeElectionGovernor: initParams.nomineeElectionGovernor, + _securityCouncilManager: initParams.securityCouncilManager, + _token: initParams.token, + _owner: initParams.owner, + _votingPeriod: initParams.votingPeriod, + _fullWeightDuration: initParams.fullWeightDuration + }); + + _mockCohortSize(initParams.maxNominees); + + compliantNominees = new address[](0); + + vm.roll(10); + } + + function testInitReverts() public { + SecurityCouncilMemberElectionGovernor governor2 = _deployGovernor(); + + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernor.InvalidDurations.selector, + initParams.votingPeriod + 1, + initParams.votingPeriod + ) + ); + governor2.initialize({ + _nomineeElectionGovernor: initParams.nomineeElectionGovernor, + _securityCouncilManager: initParams.securityCouncilManager, + _token: initParams.token, + _owner: initParams.owner, + _votingPeriod: initParams.votingPeriod, + _fullWeightDuration: initParams.votingPeriod + 1 + }); + } + + function testProperInitialization() public { + assertEq( + address(governor.nomineeElectionGovernor()), address(initParams.nomineeElectionGovernor) + ); + assertEq( + address(governor.securityCouncilManager()), address(initParams.securityCouncilManager) + ); + assertEq(address(governor.token()), address(initParams.token)); + assertEq(governor.owner(), initParams.owner); + assertEq(governor.votingPeriod(), initParams.votingPeriod); + assertEq(governor.fullWeightDuration(), initParams.fullWeightDuration); + assertEq(governor.proposalThreshold(), 0); + assertEq(governor.quorum(100), 0); + } + + // test functions defined in SecurityCouncilMemberElectionGovernor + + function testCastVoteReverts() public { + vm.expectRevert(SecurityCouncilMemberElectionGovernor.CastVoteDisabled.selector); + governor.castVote(10, 0); + + vm.expectRevert(SecurityCouncilMemberElectionGovernor.CastVoteDisabled.selector); + governor.castVoteWithReason(10, 0, ""); + + vm.expectRevert(SecurityCouncilMemberElectionGovernor.CastVoteDisabled.selector); + governor.castVoteBySig(10, 0, 1, bytes32(uint256(0x20)), bytes32(uint256(0x21))); + } + + function testRelay() public { + // make sure relay can only be called by owner + vm.expectRevert("Ownable: caller is not the owner"); + governor.relay(address(0), 0, new bytes(0)); + + // make sure relay can be called by owner, and that we can call an onlyGovernance function + vm.prank(initParams.owner); + governor.relay( + address(governor), 0, abi.encodeWithSelector(governor.setVotingPeriod.selector, 121_212) + ); + assertEq(governor.votingPeriod(), 121_212); + } + + function testProposeReverts() public { + vm.expectRevert( + abi.encodeWithSelector(SecurityCouncilMemberElectionGovernor.ProposeDisabled.selector) + ); + governor.propose(new address[](0), new uint256[](0), new bytes[](0), ""); + + // should also fail if called by the nominee election governor + vm.prank(address(initParams.nomineeElectionGovernor)); + vm.expectRevert( + abi.encodeWithSelector(SecurityCouncilMemberElectionGovernor.ProposeDisabled.selector) + ); + governor.propose(new address[](0), new uint256[](0), new bytes[](0), ""); + } + + function testOnlyNomineeElectionGovernorCanPropose() public { + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernor.OnlyNomineeElectionGovernor.selector + ) + ); + governor.proposeFromNomineeElectionGovernor(0); + + _propose(0); + } + + function testExecute() public { + // we need to create a proposal, and vote for 6 nominees + uint256 proposalId = _createProposalAndVoteForSomeNominees(0, initParams.maxNominees); + + // roll to the end of voting + vm.roll(governor.proposalDeadline(proposalId) + 1); + + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) = governor.getProposeArgs(0); + + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector( + ElectionGovernor(payable(address(initParams.nomineeElectionGovernor))) + .electionIndexToCohort + .selector, + 0 + ), + abi.encode(0) + ); + vm.mockCall(address(initParams.securityCouncilManager), "", ""); + vm.expectCall( + address(initParams.securityCouncilManager), + 0, + abi.encodeWithSelector( + initParams.securityCouncilManager.replaceCohort.selector, + governor.topNominees(proposalId), + Cohort.FIRST + ) + ); + governor.execute({ + targets: targets, + values: values, + calldatas: calldatas, + descriptionHash: keccak256(bytes(description)) + }); + } + + function testSetFullWeightDuration() public { + // ensure onlyGovernance + vm.expectRevert("Governor: onlyGovernance"); + governor.setFullWeightDuration(0); + + // make sure governor can call + vm.prank(address(governor)); + governor.setFullWeightDuration(0); + assertEq(governor.fullWeightDuration(), 0); + + // make sure the duration cannot be longer than votingPeriod + vm.prank(address(governor)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable + .FullWeightDurationGreaterThanVotingPeriod + .selector, + initParams.votingPeriod + 1, + initParams.votingPeriod + ) + ); + governor.setFullWeightDuration(initParams.votingPeriod + 1); + } + + function testNoVoteForNonCompliantNominee() public { + uint256 proposalId = _propose(0); + + // make sure the nomineeElectionGovernor says the nominee is not compliant + _setCompliantNominee(proposalId, _nominee(0), false); + + // make sure the voter has enough votes + address voter = _voter(0); + _mockGetPastVotes({ + account: voter, + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + address nominee = _nominee(0); + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(voter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable + .NotCompliantNominee + .selector, + nominee + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(nominee, 100) + }); + } + + function testInvalidParams() public { + uint256 proposalId = _propose(0); + + // make sure the nomineeElectionGovernor says the nominee is not compliant + _setCompliantNominee(proposalId, _nominee(0), true); + + // make sure the voter has enough votes + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(_voter(0)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable + .UnexpectedParamsLength + .selector, + 32 + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(_nominee(0)) + }); + } + + function testNoZeroWeightVotes() public { + uint256 proposalId = _propose(0); + + // make sure the nomineeElectionGovernor says the nominee is not compliant + _setCompliantNominee(proposalId, _nominee(0), true); + + // make sure the voter has enough votes + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(_voter(0)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable.ZeroWeightVote.selector, + block.number, + 0 + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(_nominee(0), 0) + }); + } + + bytes32 private constant _TYPE_HASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 private constant _NAME_HASH = keccak256(bytes("SecurityCouncilMemberElectionGovernor")); + bytes32 private constant _VERSION_HASH = keccak256(bytes("1")); + bytes32 public constant EXTENDED_BALLOT_TYPEHASH = + keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); + + function _hashTypedDataV4(bytes32 structHash, address targetAddress) + internal + view + virtual + returns (bytes32) + { + bytes32 domainHash = keccak256( + abi.encode(_TYPE_HASH, _NAME_HASH, _VERSION_HASH, block.chainid, targetAddress) + ); + return ECDSAUpgradeable.toTypedDataHash(domainHash, structHash); + } + + function create712Hash( + uint256 proposalId, + uint8 support, + string memory reason, + bytes memory params, + address targetAddress + ) public view returns (bytes32) { + return _hashTypedDataV4( + keccak256( + abi.encode( + EXTENDED_BALLOT_TYPEHASH, + proposalId, + support, + keccak256(bytes(reason)), + keccak256(params) + ) + ), + targetAddress + ); + } + + function testCastBySig() public { + uint256 proposalId = _propose(0); + uint256 voterPrivKey = 0x4173fa62f15e8a9363d4dc11b951722b264fa38fbec64c0f6f14fc1e63f7edd4; + address voterAddress = vm.addr(voterPrivKey); + + // make sure the nomineeElectionGovernor says the nominee is not compliant + _setCompliantNominee(proposalId, _nominee(0), true); + + // make sure the voter has enough votes + _mockGetPastVotes({ + account: voterAddress, + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + bytes32 dataHash = + create712Hash(proposalId, 1, "a", abi.encode(_nominee(0), 10), address(governor)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(voterPrivKey, dataHash); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(voterAddress); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "a", + params: abi.encode(_nominee(0), 10), + v: v, + r: r, + s: s + }); + + bytes32 dataHash2 = + create712Hash(proposalId, 1, "b", abi.encode(_nominee(0), 10), address(governor)); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(voterPrivKey, dataHash2); + + vm.prank(voterAddress); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "b", + params: abi.encode(_nominee(0), 10), + v: v2, + r: r2, + s: s2 + }); + } + + function testCastBySigTwice() public { + uint256 proposalId = _propose(0); + uint256 voterPrivKey = 0x4173fa62f15e8a9363d4dc11b951722b264fa38fbec64c0f6f14fc1e63f7edd3; + address voterAddress = vm.addr(voterPrivKey); + + // make sure the nomineeElectionGovernor says the nominee is not compliant + _setCompliantNominee(proposalId, _nominee(0), true); + + // make sure the voter has enough votes + _mockGetPastVotes({ + account: voterAddress, + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + bytes32 dataHash = + create712Hash(proposalId, 1, "a", abi.encode(_nominee(0), 10), address(governor)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(voterPrivKey, dataHash); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(voterAddress); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "a", + params: abi.encode(_nominee(0), 10), + v: v, + r: r, + s: s + }); + + vm.prank(voterAddress); + vm.expectRevert( + abi.encodeWithSelector( + ElectionGovernor.VoteAlreadyCast.selector, + voterAddress, + proposalId, + keccak256(abi.encodePacked(dataHash, voterAddress)) + ) + ); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "a", + params: abi.encode(_nominee(0), 10), + v: v, + r: r, + s: s + }); + } + + function testForceSupport() public { + uint256 proposalId = _propose(0); + + _setCompliantNominee(proposalId, _nominee(0), true); + + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(_voter(0)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable.InvalidSupport.selector, 2 + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 2, + reason: "", + params: abi.encode(_nominee(0), 100) + }); + } + + function testCannotUseMoreVotesThanAvailable() public { + uint256 proposalId = _propose(0); + + // make sure the nomineeElectionGovernor says the nominee is compliant + _setCompliantNominee(proposalId, _nominee(0), true); + + // make sure the voter has some votes + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + // roll to the start of voting + vm.roll(governor.proposalSnapshot(proposalId) + 1); + + // try to use more votes than available + vm.prank(_voter(0)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable.InsufficientVotes.selector, + 0, + 101, + 100 + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(_nominee(0), 101) + }); + + // use some amount of votes that is less than available + vm.prank(_voter(0)); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(_nominee(0), 50) + }); + + // now try to use more votes than available + vm.prank(_voter(0)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable.InsufficientVotes.selector, + 50, + 51, + 100 + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(_nominee(0), 51) + }); + } + + function testSelectTopNomineesFails() public { + uint16 n = 100; + uint16 k = 6; + + uint240[] memory weights = TestUtil.randomUint240s(n, 6); + address[] memory addresses = TestUtil.randomAddresses(n - 1, 7); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable.LengthsDontMatch.selector, + n - 1, + n + ) + ); + governor.selectTopNominees(addresses, weights, k); + + weights = TestUtil.randomUint240s(k - 1, 6); + addresses = TestUtil.randomAddresses(k - 1, 7); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilMemberElectionGovernorCountingUpgradeable.NotEnoughNominees.selector, + k - 1, + k + ) + ); + governor.selectTopNominees(addresses, weights, k); + + // also test the boundary + weights = TestUtil.randomUint240s(k, 6); + addresses = TestUtil.randomAddresses(k, 7); + governor.selectTopNominees(addresses, weights, k); + } + + function testSelectTopNominees(uint256 seed) public { + vm.assume(seed > 0); + + uint16 n = 100; + uint16 k = 6; + + // make a random list of addresses and weights + uint240[] memory weights = TestUtil.randomUint240s(n, seed); + address[] memory addresses = TestUtil.randomAddresses(n, seed - 1); + + // call selectTopNominees + address[] memory topNominees = governor.selectTopNominees(addresses, weights, k); + assertEq(topNominees.length, k); + + // pack and sort original, compare to selectTopNominees + uint256[] memory packed = new uint256[](n); + for (uint16 i = 0; i < n; i++) { + packed[i] = (uint256(weights[i]) << 16) | i; + } + + LibSort.sort(packed); + + for (uint256 i = 0; i < k; i++) { + assertEq(topNominees[k - i - 1], addresses[packed[n - i - 1] & 0xffff]); + } + } + + function testVotesToWeight() public { + uint256 proposalId = _propose(0); + + uint256 startBlock = governor.proposalSnapshot(proposalId); + + // test weight before voting starts (block <= startBlock) + assertEq( + governor.votesToWeight(proposalId, startBlock, 100), 0, "right before voting starts" + ); + + // test weight right after voting starts (block == startBlock + 1) + assertEq( + governor.votesToWeight(proposalId, startBlock + 1, 100), + 100, + "right after voting starts" + ); + + // test weight right before full weight voting ends + // (block == startBlock + votingPeriod * fullWeightDurationNumerator / durationDenominator) + assertEq( + governor.votesToWeight(proposalId, governor.fullWeightVotingDeadline(proposalId), 100), + 100, + "right before full weight voting ends" + ); + + // test weight right after full weight voting ends + assertLe( + governor.votesToWeight( + proposalId, governor.fullWeightVotingDeadline(proposalId) + 1, 100 + ), + 100, + "right after full weight voting ends" + ); + + // test weight halfway through decreasing weight voting + uint256 halfwayPoint = ( + governor.fullWeightVotingDeadline(proposalId) + governor.proposalDeadline(proposalId) + ) / 2; + assertEq( + governor.votesToWeight(proposalId, halfwayPoint, 100), + 50, + "halfway through decreasing weight voting" + ); + + // test weight at proposal deadline + assertEq( + governor.votesToWeight(proposalId, governor.proposalDeadline(proposalId), 100), + 0, + "at proposal deadline" + ); + + // test governor with no full weight voting + vm.prank(address(governor)); + governor.setFullWeightDuration(0); + assertEq( + governor.votesToWeight(proposalId, governor.proposalDeadline(proposalId), 100), + 0, + "at proposal deadline, no full weight voting" + ); + + // test governor with no decreasing weight voting + vm.prank(address(governor)); + governor.setFullWeightDuration(initParams.votingPeriod); + assertEq( + governor.votesToWeight(proposalId, governor.proposalDeadline(proposalId), 100), + 100, + "at proposal deadline, no decreasing weight voting" + ); + } + + function testMiscVotesViews() public { + uint256 proposalId = _propose(0); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + _castVoteForCompliantNominee({ + proposalId: proposalId, + voter: _voter(0), + nominee: _nominee(0), + votes: 100 + }); + + assertEq(governor.hasVoted(proposalId, _voter(0)), true); + assertEq(governor.votesUsed(proposalId, _voter(0)), 100); + assertEq(governor.weightReceived(proposalId, _nominee(0)), 100); + } + + // helpers + + function _voter(uint8 i) internal pure returns (address) { + return address(uint160(0x1100 + i)); + } + + function _nominee(uint8 i) internal pure returns (address) { + return address(uint160(0x2200 + i)); + } + + function _createProposalAndVoteForSomeNominees( + uint256 nomineeElectionIndex, + uint256 numNominees + ) internal returns (uint256) { + uint256 proposalId = _propose(nomineeElectionIndex); + + // roll to the start of voting + vm.roll(governor.proposalSnapshot(proposalId) + 1); + + // vote for 6 compliant nominees + for (uint8 i = 0; i < numNominees; i++) { + // mock the number of votes they have + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: numNominees + }); + + _castVoteForCompliantNominee({ + proposalId: proposalId, + voter: _voter(0), + nominee: _nominee(i), + votes: 1 + }); + } + + return proposalId; + } + + function _mockCohortSize(uint256 count) internal { + vm.mockCall( + address(initParams.securityCouncilManager), + abi.encodeWithSelector(initParams.securityCouncilManager.cohortSize.selector), + abi.encode(count) + ); + + assertEq(initParams.securityCouncilManager.cohortSize(), count); + } + + function _mockGetPastVotes(address account, uint256 votes, uint256 blockNumber) internal { + vm.mockCall( + address(initParams.token), + abi.encodeWithSelector(initParams.token.getPastVotes.selector, account, blockNumber), + abi.encode(votes) + ); + } + + function _mockGetPastVotes(address account, uint256 votes) internal { + vm.mockCall( + address(initParams.token), + abi.encodeWithSelector(initParams.token.getPastVotes.selector, account), + abi.encode(votes) + ); + } + + function _setCompliantNominee(uint256 proposalId, address account, bool ans) internal { + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector( + initParams.nomineeElectionGovernor.isCompliantNominee.selector, proposalId, account + ), + abi.encode(ans) + ); + if (ans) { + // add the nominee to the list in this contract's storage if it isn't already there + // then mock the call to nomineeElectionGovernor.compliantNominees(uint) + uint256 index = TestUtil.indexOf(compliantNominees, account); + if (index == type(uint256).max) { + compliantNominees.push(account); + } + + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector( + initParams.nomineeElectionGovernor.compliantNominees.selector, proposalId + ), + abi.encode(compliantNominees) + ); + } else { + // we should remove the nominee from the list in this contract's storage if it is there + // then mock the call to nomineeElectionGovernor.compliantNominees(uint) + uint256 index = TestUtil.indexOf(compliantNominees, account); + if (index != type(uint256).max) { + compliantNominees[index] = compliantNominees[compliantNominees.length - 1]; + compliantNominees.pop(); + } + + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector( + initParams.nomineeElectionGovernor.compliantNominees.selector, proposalId + ), + abi.encode(compliantNominees) + ); + } + } + + function _castVoteForCompliantNominee( + uint256 proposalId, + address voter, + address nominee, + uint256 votes + ) internal { + // make sure the nomineeElectionGovernor says the nominee is compliant + _setCompliantNominee(proposalId, nominee, true); + + vm.prank(voter); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(nominee, votes) + }); + + // vm.clearMockedCalls(); + } + + function _propose(uint256 nomineeElectionIndex) internal returns (uint256) { + // we need to mock call to the nominee election governor + _setUpNomineeGovernorWithIndexInformation(nomineeElectionIndex); + + // we need to mock getPastVotes for the nominee election governor + _mockGetPastVotes({account: address(initParams.nomineeElectionGovernor), votes: 0}); + + vm.prank(address(initParams.nomineeElectionGovernor)); + governor.proposeFromNomineeElectionGovernor(nomineeElectionIndex); + + // vm.clearMockedCalls(); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) = governor.getProposeArgs(nomineeElectionIndex); + + return uint256( + keccak256(abi.encode(targets, values, calldatas, keccak256(bytes(description)))) + ); + } + + function _setUpNomineeGovernorWithIndexInformation(uint256 electionIndex) internal { + // electionCount() returns 1 + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor( + payable(address(initParams.nomineeElectionGovernor)) + ).electionCount.selector + ), + abi.encode(electionIndex + 1) + ); + + // mock call to electionIndexToDescription + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector( + ElectionGovernor(payable(address(initParams.nomineeElectionGovernor))) + .electionIndexToDescription + .selector, + electionIndex + ), + abi.encode(_electionIndexToDescription(electionIndex)) + ); + + // mock call to currentCohort + vm.mockCall( + address(initParams.nomineeElectionGovernor), + abi.encodeWithSelector(SecurityCouncilNomineeElectionGovernor.currentCohort.selector), + abi.encode(electionIndex % 2) + ); + } + + function _electionIndexToDescription(uint256 electionIndex) + internal + pure + returns (string memory) + { + return + string.concat("Security Council Election #", StringsUpgradeable.toString(electionIndex)); + } + + function _deployGovernor() internal returns (SecurityCouncilMemberElectionGovernor) { + return SecurityCouncilMemberElectionGovernor( + payable( + new TransparentUpgradeableProxy( + address(new SecurityCouncilMemberElectionGovernor()), + proxyAdmin, + bytes("") + ) + ) + ); + } +} diff --git a/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol b/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol new file mode 100644 index 00000000..f38de6c6 --- /dev/null +++ b/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol @@ -0,0 +1,892 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import "../../util/TestUtil.sol"; + +import "../../../src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol"; +import "../../../src/security-council-mgmt/Common.sol"; + +contract SecurityCouncilNomineeElectionGovernorTest is Test { + SecurityCouncilNomineeElectionGovernor governor; + + uint256 cohortSize = 6; + + SecurityCouncilNomineeElectionGovernor.InitParams initParams = + SecurityCouncilNomineeElectionGovernor.InitParams({ + firstNominationStartDate: Date({year: 2030, month: 1, day: 1, hour: 0}), + nomineeVettingDuration: 1 days, + nomineeVetter: address(0x11), + securityCouncilManager: ISecurityCouncilManager(address(0x22)), + securityCouncilMemberElectionGovernor: ISecurityCouncilMemberElectionGovernor( + payable(address(0x33)) + ), + token: IVotesUpgradeable(address(0x44)), + owner: address(0x55), + quorumNumeratorValue: 20, + votingPeriod: 1 days + }); + + address proxyAdmin = address(0x66); + address proposer = address(0x77); + + function setUp() public { + governor = _deployGovernor(); + + vm.etch(address(initParams.securityCouncilManager), "0x23"); + vm.etch(address(initParams.securityCouncilMemberElectionGovernor), "0x34"); + + governor.initialize(initParams); + + vm.warp(1_689_281_541); // july 13, 2023 + + _mockGetPastVotes({account: 0x00000000000000000000000000000000000A4B86, votes: 0}); + _mockGetPastTotalSupply(1_000_000_000e18); + _mockCohortSize(cohortSize); + } + + function testProperInitialization() public { + assertEq(governor.nomineeVettingDuration(), initParams.nomineeVettingDuration); + assertEq(governor.nomineeVetter(), initParams.nomineeVetter); + assertEq( + address(governor.securityCouncilManager()), address(initParams.securityCouncilManager) + ); + assertEq( + address(governor.securityCouncilMemberElectionGovernor()), + address(initParams.securityCouncilMemberElectionGovernor) + ); + assertEq(address(governor.token()), address(initParams.token)); + assertEq(governor.owner(), initParams.owner); + // assertEq(governor.quorumNumeratorValue(), initParams.quorumNumeratorValue); + assertEq(governor.votingPeriod(), initParams.votingPeriod); + // assertEq(governor.firstNominationStartDate(), initParams.firstNominationStartDate); + (uint256 year, uint256 month, uint256 day, uint256 hour) = + governor.firstNominationStartDate(); + assertEq(year, initParams.firstNominationStartDate.year); + assertEq(month, initParams.firstNominationStartDate.month); + assertEq(day, initParams.firstNominationStartDate.day); + assertEq(hour, initParams.firstNominationStartDate.hour); + assertEq(governor.proposalThreshold(), 0); + assertEq(uint256(governor.currentCohort()), uint256(Cohort.FIRST)); + assertEq(uint256(governor.otherCohort()), uint256(Cohort.SECOND)); + } + + function testInvalidInit() public { + SecurityCouncilNomineeElectionGovernor.InitParams memory invalidParams = initParams; + invalidParams.firstNominationStartDate = Date({year: 2022, month: 1, day: 1, hour: 0}); + SecurityCouncilNomineeElectionGovernor governorInvalid = _deployGovernor(); + + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorTiming.StartDateTooEarly.selector, + 1_640_995_200, + block.timestamp + ) + ); + governorInvalid.initialize(invalidParams); + + invalidParams.firstNominationStartDate = Date({year: 2000, month: 13, day: 1, hour: 0}); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorTiming.InvalidStartDate.selector, + invalidParams.firstNominationStartDate.year, + invalidParams.firstNominationStartDate.month, + invalidParams.firstNominationStartDate.day, + invalidParams.firstNominationStartDate.hour + ) + ); + governorInvalid.initialize(invalidParams); + + SecurityCouncilNomineeElectionGovernor.InitParams memory invalidQuorumParams = initParams; + invalidQuorumParams.quorumNumeratorValue = 19; + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.QuorumNumeratorTooLow.selector, 19 + ) + ); + governorInvalid.initialize(invalidQuorumParams); + } + + function testCreateElection() public { + // we need to mock getPastVotes for the proposer + _mockGetPastVotes({account: address(this), votes: 0}); + + // we should not be able to create election before first nomination start date + uint256 expectedStartTimestamp = + _datePlusMonthsToTimestamp(initParams.firstNominationStartDate, 0); + vm.warp(expectedStartTimestamp - 1); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.CreateTooEarly.selector, + block.timestamp, + expectedStartTimestamp + ) + ); + governor.createElection(); + + // we should be able to create an election at the timestamp + assertEq(governor.electionCount(), 0); + assertEq(uint256(governor.currentCohort()), uint256(Cohort.FIRST)); + assertEq(uint256(governor.otherCohort()), uint256(Cohort.SECOND)); + vm.warp(expectedStartTimestamp); + uint256 firstProposalId = governor.createElection(); + assertEq(governor.electionCount(), 1); + assertEq(uint256(governor.currentCohort()), uint256(Cohort.FIRST)); + assertEq(uint256(governor.otherCohort()), uint256(Cohort.SECOND)); + + // if there has been one election created, the nominee gov will (should) call memberGov.state() to check if the previous election has been executed + // here we mock it to be not Executed and expect that createElection will revert + _mockMemberGovState(IGovernorUpgradeable.ProposalState.Succeeded); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.LastMemberElectionNotExecuted.selector, + firstProposalId + ) + ); + governor.createElection(); + + // now mock state to be executed so we can run other tests + _mockMemberGovState(IGovernorUpgradeable.ProposalState.Executed); + + // we should not be able to create another election before 6 months have passed + expectedStartTimestamp = _datePlusMonthsToTimestamp(initParams.firstNominationStartDate, 6); + vm.warp(expectedStartTimestamp - 1); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.CreateTooEarly.selector, + block.timestamp, + expectedStartTimestamp + ) + ); + governor.createElection(); + + // we should be able to create an election at the timestamp + vm.warp(expectedStartTimestamp); + governor.createElection(); + assertEq(governor.electionCount(), 2); + assertEq(uint256(governor.currentCohort()), uint256(Cohort.SECOND)); + assertEq(uint256(governor.otherCohort()), uint256(Cohort.FIRST)); + } + + function testAddContender() public { + // test invalid proposal id + vm.prank(_contender(0)); + vm.expectRevert("Governor: unknown proposal id"); + governor.addContender(0); + + // make a valid proposal + uint256 proposalId = _propose(); + + // test in other cohort + _mockCohortIncludes(Cohort.SECOND, _contender(0), true); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.AccountInOtherCohort.selector, + Cohort.SECOND, + _contender(0) + ) + ); + vm.prank(_contender(0)); + governor.addContender(proposalId); + + // should fail if the proposal is not active + _mockCohortIncludes(Cohort.SECOND, _contender(0), false); + vm.roll(governor.proposalDeadline(proposalId) + 1); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.ProposalNotActive.selector, + IGovernorUpgradeable.ProposalState.Succeeded + ) + ); + vm.prank(_contender(0)); + governor.addContender(proposalId); + + // should succeed if not in other cohort and proposal is active + vm.roll(governor.proposalDeadline(proposalId)); + assertTrue(governor.state(proposalId) == IGovernorUpgradeable.ProposalState.Active); + vm.prank(_contender(0)); + governor.addContender(proposalId); + + // check that it correctly mutated the state + assertTrue(governor.isContender(proposalId, _contender(0))); + + // adding again should fail + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.AlreadyContender.selector, _contender(0) + ) + ); + vm.prank(_contender(0)); + governor.addContender(proposalId); + } + + function testSetNomineeVetter() public { + // should only be callable by owner + vm.expectRevert("Governor: onlyGovernance"); + governor.setNomineeVetter(address(137)); + + // should succeed if called by owner + vm.prank(initParams.owner); + governor.relay( + address(governor), + 0, + abi.encodeWithSelector(governor.setNomineeVetter.selector, address(137)) + ); + assertEq(governor.nomineeVetter(), address(137)); + } + + function testRelay() public { + // make sure relay can only be called by owner + vm.expectRevert("Ownable: caller is not the owner"); + governor.relay(address(0), 0, new bytes(0)); + + // make sure relay can be called by owner, and that we can call an onlyGovernance function + vm.prank(initParams.owner); + governor.relay( + address(governor), 0, abi.encodeWithSelector(governor.setVotingPeriod.selector, 121_212) + ); + assertEq(governor.votingPeriod(), 121_212); + } + + function testExcludeNominee() public { + uint256 proposalId = _propose(); + + // should fail if called by non-nominee vetter + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.OnlyNomineeVetter.selector + ) + ); + governor.excludeNominee(proposalId, address(0)); + + // should fail if called with invalid proposal id + vm.prank(initParams.nomineeVetter); + vm.expectRevert("Governor: unknown proposal id"); + governor.excludeNominee(0, _contender(0)); + + // should fail if called while proposal is active + uint256 vettingDeadline = governor.proposalVettingDeadline(proposalId); + vm.roll(governor.proposalDeadline(proposalId) - 1); // state is active + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.ProposalNotSucceededState.selector, + 1 // active + ) + ); + governor.excludeNominee(proposalId, _contender(0)); + + // should fail if called after the vetting period has elapsed + vm.roll(governor.proposalDeadline(proposalId) + governor.nomineeVettingDuration() + 1); + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.ProposalNotInVettingPeriod.selector, + block.number, + vettingDeadline + ) + ); + governor.excludeNominee(proposalId, _contender(0)); + + // should fail if the account is not a nominee + vm.roll(governor.proposalDeadline(proposalId) + 1); + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.NotNominee.selector, _contender(0) + ) + ); + governor.excludeNominee(proposalId, _contender(0)); + + // should succeed if called by nominee vetter, proposal is in vetting period, and account is a nominee + // make sure the account is a nominee + vm.roll(governor.proposalDeadline(proposalId)); + _addContender(proposalId, _contender(0)); + _mockGetPastVotes(_voter(0), governor.quorum(proposalId)); + _castVoteForContender(proposalId, _voter(0), _contender(0), governor.quorum(proposalId)); + + // roll to the end of voting (into the vetting period) and exclude the nominee + vm.roll(governor.proposalDeadline(proposalId) + 1); + vm.prank(initParams.nomineeVetter); + governor.excludeNominee(proposalId, _contender(0)); + + // make sure state is correct + assertTrue(governor.isExcluded(proposalId, _contender(0))); + assertEq(governor.excludedNomineeCount(proposalId), 1); + + // should fail if contender is excluded twice + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.NomineeAlreadyExcluded.selector, + _contender(0) + ) + ); + governor.excludeNominee(proposalId, _contender(0)); + } + + function testIncludeNominee() public { + // going to skip cases checking the onlyNomineeVetterInVettingPeriod modifier + + uint256 proposalId = _propose(); + + // create a nominee + vm.roll(governor.proposalDeadline(proposalId)); + _addContender(proposalId, _contender(0)); + _mockGetPastVotes(_voter(0), governor.quorum(proposalId)); + _castVoteForContender(proposalId, _voter(0), _contender(0), governor.quorum(proposalId)); + + // should fail if called by non nominee vetter + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.OnlyNomineeVetter.selector + ) + ); + governor.includeNominee(proposalId, _contender(0)); + + // should fail if state is not Succeeded + vm.roll(governor.proposalDeadline(proposalId)); + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.ProposalNotSucceededState.selector, + 1 // active + ) + ); + governor.includeNominee(proposalId, _contender(0)); + + // should fail if the account is already a nominee + vm.roll(governor.proposalDeadline(proposalId) + 1); + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorCountingUpgradeable + .NomineeAlreadyAdded + .selector, + _contender(0) + ) + ); + governor.includeNominee(proposalId, _contender(0)); + + // should fail if the account is part of the other cohort + _mockCohortIncludes(Cohort.SECOND, _contender(1), true); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.AccountInOtherCohort.selector, + Cohort.SECOND, + _contender(1) + ) + ); + vm.prank(initParams.nomineeVetter); + governor.includeNominee(proposalId, _contender(1)); + + // should succeed if the account is not a nominee, we havent reached the target nominee count, and the account is not a member of the opposite cohort + // should succeed even if past the vetting deadline + _mockCohortIncludes(Cohort.SECOND, _contender(1), false); + vm.roll(governor.proposalVettingDeadline(proposalId) + 1); + vm.prank(initParams.nomineeVetter); + governor.includeNominee(proposalId, _contender(1)); + + // make sure state is correct + assertTrue(governor.isNominee(proposalId, _contender(1))); + assertEq(governor.nomineeCount(proposalId), 2); + + // make sure that we can't add more nominees than the target count + for (uint8 i = 0; i < cohortSize - 2; i++) { + _mockCohortIncludes(Cohort.SECOND, _contender(i + 2), false); + vm.prank(initParams.nomineeVetter); + governor.includeNominee(proposalId, _contender(i + 2)); + } + _mockCohortIncludes(Cohort.SECOND, _contender(uint8(cohortSize)), false); + vm.prank(initParams.nomineeVetter); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.CompliantNomineeTargetHit.selector, + cohortSize, + cohortSize + ) + ); + governor.includeNominee(proposalId, _contender(uint8(cohortSize))); + + vm.prank(initParams.nomineeVetter); + vm.expectRevert(abi.encodeWithSelector(ZeroAddress.selector)); + governor.includeNominee(proposalId, address(0)); + } + + function testExecute() public { + uint256 proposalId = _propose(); + + uint256 electionIndex = governor.electionCount() - 1; + + // should fail if called during vetting period + vm.roll(governor.proposalDeadline(proposalId) + 1); + _execute( + electionIndex, + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.ProposalInVettingPeriod.selector, + block.number, + governor.proposalVettingDeadline(proposalId) + ) + ); + + // should fail if there aren't enough compliant nominees + // make some but not enough + for (uint8 i = 0; i < cohortSize - 1; i++) { + _mockCohortIncludes(Cohort.SECOND, _contender(i), false); + vm.prank(initParams.nomineeVetter); + governor.includeNominee(proposalId, _contender(i)); + } + + vm.roll(governor.proposalVettingDeadline(proposalId) + 1); + _execute( + electionIndex, + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.InsufficientCompliantNomineeCount.selector, + cohortSize - 1, + cohortSize + ) + ); + + // should call the member election governor if there are enough compliant nominees + vm.roll(governor.proposalVettingDeadline(proposalId)); + _mockCohortIncludes(Cohort.SECOND, _contender(uint8(cohortSize - 1)), false); + vm.prank(initParams.nomineeVetter); + governor.includeNominee(proposalId, _contender(uint8(cohortSize - 1))); + + vm.roll(governor.proposalVettingDeadline(proposalId) + 1); + + // mock the return value as a different proposal id + vm.mockCall(address(initParams.securityCouncilMemberElectionGovernor), "", abi.encode(100)); + _execute( + electionIndex, + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernor.ProposalIdMismatch.selector, proposalId, 100 + ) + ); + + vm.mockCall( + address(initParams.securityCouncilMemberElectionGovernor), "", abi.encode(proposalId) + ); + vm.expectCall( + address(initParams.securityCouncilMemberElectionGovernor), + abi.encodeWithSelector( + initParams + .securityCouncilMemberElectionGovernor + .proposeFromNomineeElectionGovernor + .selector, + electionIndex + ) + ); + _execute(electionIndex, ""); + + assertEq(uint256(governor.currentCohort()), uint256(Cohort.FIRST)); + assertEq(uint256(governor.otherCohort()), uint256(Cohort.SECOND)); + } + + function testCountVote() public { + uint256 proposalId = _propose(); + + // mock some votes for the whole test here + _mockGetPastVotes(_voter(0), governor.quorum(proposalId) * 2); + + // make sure params is 64 bytes long + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorCountingUpgradeable + .UnexpectedParamsLength + .selector, + 32 + ) + ); + vm.prank(_voter(0)); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(_contender(0)) + }); + + // cannot vote for a contender who hasn't added themself + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorCountingUpgradeable + .NotEligibleContender + .selector, + _contender(0) + ) + ); + _castVoteForContender(proposalId, _voter(0), _contender(0), 1); + + // can vote for a contender who has added themself + _addContender(proposalId, _contender(0)); + _castVoteForContender(proposalId, _voter(0), _contender(0), 1); + + // check state + assertEq(governor.votesUsed(proposalId, _voter(0)), 1); + assertEq(governor.votesReceived(proposalId, _contender(0)), 1); + assertTrue(governor.hasVoted(proposalId, _voter(0))); + assertFalse(governor.isNominee(proposalId, _contender(0))); + + // push the candidate over the line, make sure that any excess votes aren't used + _castVoteForContender( + proposalId, _voter(0), _contender(0), governor.quorum(proposalId) + 100 + ); + assertEq(governor.votesUsed(proposalId, _voter(0)), governor.quorum(proposalId)); + assertEq(governor.votesReceived(proposalId, _contender(0)), governor.quorum(proposalId)); + assertTrue(governor.isNominee(proposalId, _contender(0))); + assertEq(governor.nomineeCount(proposalId), 1); + + // make sure that we can't vote for a nominee + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorCountingUpgradeable + .NomineeAlreadyAdded + .selector, + _contender(0) + ) + ); + _castVoteForContender(proposalId, _voter(0), _contender(0), 1); + + // make sure we can't use more votes than we have + _addContender(proposalId, _contender(1)); + _addContender(proposalId, _contender(2)); + _castVoteForContender(proposalId, _voter(0), _contender(1), governor.quorum(proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorCountingUpgradeable + .InsufficientTokens + .selector, + 1, + governor.quorum(proposalId) * 2, + governor.quorum(proposalId) * 2 + ) + ); + _castVoteForContender(proposalId, _voter(0), _contender(2), 1); + } + + bytes32 private constant _TYPE_HASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + bytes32 private constant _NAME_HASH = keccak256(bytes("SecurityCouncilNomineeElectionGovernor")); + bytes32 private constant _VERSION_HASH = keccak256(bytes("1")); + bytes32 public constant EXTENDED_BALLOT_TYPEHASH = + keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); + + function _hashTypedDataV4(bytes32 structHash, address targetAddress) + internal + view + virtual + returns (bytes32) + { + bytes32 domainHash = keccak256( + abi.encode(_TYPE_HASH, _NAME_HASH, _VERSION_HASH, block.chainid, targetAddress) + ); + return ECDSAUpgradeable.toTypedDataHash(domainHash, structHash); + } + + function create712Hash( + uint256 proposalId, + uint8 support, + string memory reason, + bytes memory params, + address targetAddress + ) public view returns (bytes32) { + return _hashTypedDataV4( + keccak256( + abi.encode( + EXTENDED_BALLOT_TYPEHASH, + proposalId, + support, + keccak256(bytes(reason)), + keccak256(params) + ) + ), + targetAddress + ); + } + + function testCastBySig() public { + uint256 proposalId = _propose(); + uint256 voterPrivKey = 0x4173fa62f15e8a9363d4dc11b951722b264fa38fbec64c0f6f14fc1e63f7edd4; + address voterAddress = vm.addr(voterPrivKey); + + _addContender(proposalId, _contender(0)); + + // make sure the voter has enough votes + _mockGetPastVotes({ + account: voterAddress, + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + bytes32 dataHash = + create712Hash(proposalId, 1, "a", abi.encode(_contender(0), 10), address(governor)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(voterPrivKey, dataHash); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(voterAddress); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "a", + params: abi.encode(_contender(0), 10), + v: v, + r: r, + s: s + }); + + bytes32 dataHash2 = + create712Hash(proposalId, 1, "b", abi.encode(_contender(0), 10), address(governor)); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(voterPrivKey, dataHash2); + + vm.prank(voterAddress); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "b", + params: abi.encode(_contender(0), 10), + v: v2, + r: r2, + s: s2 + }); + } + + function testCastBySigTwice() public { + uint256 proposalId = _propose(); + uint256 voterPrivKey = 0x4173fa62f15e8a9363d4dc11b951722b264fa38fbec64c0f6f14fc1e63f7edd3; + address voterAddress = vm.addr(voterPrivKey); + + _addContender(proposalId, _contender(0)); + + // make sure the voter has enough votes + _mockGetPastVotes({ + account: voterAddress, + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + bytes32 dataHash = + create712Hash(proposalId, 1, "a", abi.encode(_contender(0), 10), address(governor)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(voterPrivKey, dataHash); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(voterAddress); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "a", + params: abi.encode(_contender(0), 10), + v: v, + r: r, + s: s + }); + + vm.prank(voterAddress); + vm.expectRevert( + abi.encodeWithSelector( + ElectionGovernor.VoteAlreadyCast.selector, + voterAddress, + proposalId, + keccak256(abi.encodePacked(dataHash, voterAddress)) + ) + ); + governor.castVoteWithReasonAndParamsBySig({ + proposalId: proposalId, + support: 1, + reason: "a", + params: abi.encode(_contender(0), 10), + v: v, + r: r, + s: s + }); + } + + function testProposeFails() public { + vm.expectRevert( + abi.encodeWithSelector(SecurityCouncilNomineeElectionGovernor.ProposeDisabled.selector) + ); + governor.propose(new address[](1), new uint[](1), new bytes[](1), ""); + } + + function testCastVoteReverts() public { + vm.expectRevert(SecurityCouncilNomineeElectionGovernor.CastVoteDisabled.selector); + governor.castVote(10, 0); + + vm.expectRevert(SecurityCouncilNomineeElectionGovernor.CastVoteDisabled.selector); + governor.castVoteWithReason(10, 0, ""); + + vm.expectRevert(SecurityCouncilNomineeElectionGovernor.CastVoteDisabled.selector); + governor.castVoteBySig(10, 0, 1, bytes32(uint256(0x20)), bytes32(uint256(0x21))); + } + + function testForceSupport() public { + uint256 proposalId = _propose(); + + _addContender(proposalId, _contender(0)); + + _mockGetPastVotes({ + account: _voter(0), + blockNumber: governor.proposalSnapshot(proposalId), + votes: 100 + }); + + vm.roll(governor.proposalSnapshot(proposalId) + 1); + vm.prank(_voter(0)); + vm.expectRevert( + abi.encodeWithSelector( + SecurityCouncilNomineeElectionGovernorCountingUpgradeable.InvalidSupport.selector, 2 + ) + ); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 2, + reason: "", + params: abi.encode(_contender(0), 100) + }); + } + + // helpers + + function _voter(uint8 i) internal pure returns (address) { + return address(uint160(0x1100 + i)); + } + + function _contender(uint8 i) internal pure returns (address) { + return address(uint160(0x2200 + i)); + } + + function _datePlusMonthsToTimestamp(Date memory date, uint256 months) + internal + pure + returns (uint256) + { + return DateTimeLib.dateTimeToTimestamp({ + year: date.year, + month: date.month + months, + day: date.day, + hour: date.hour, + minute: 0, + second: 0 + }); + } + + function _mockGetPastVotes(address account, uint256 votes, uint256 blockNumber) internal { + vm.mockCall( + address(initParams.token), + abi.encodeWithSelector(initParams.token.getPastVotes.selector, account, blockNumber), + abi.encode(votes) + ); + } + + function _mockGetPastVotes(address account, uint256 votes) internal { + vm.mockCall( + address(initParams.token), + abi.encodeWithSelector(initParams.token.getPastVotes.selector, account), + abi.encode(votes) + ); + } + + function _mockGetPastTotalSupply(uint256 amount) internal { + vm.mockCall( + address(initParams.token), + abi.encodeWithSelector(initParams.token.getPastTotalSupply.selector), + abi.encode(amount) + ); + } + + function _mockCohortIncludes(Cohort cohort, address member, bool ans) internal { + vm.mockCall( + address(initParams.securityCouncilManager), + abi.encodeWithSelector( + initParams.securityCouncilManager.cohortIncludes.selector, cohort, member + ), + abi.encode(ans) + ); + } + + function _mockCohortSize(uint256 count) internal { + vm.mockCall( + address(initParams.securityCouncilManager), + abi.encodeWithSelector(initParams.securityCouncilManager.cohortSize.selector), + abi.encode(count) + ); + + assertEq(initParams.securityCouncilManager.cohortSize(), count); + } + + /// @dev Mocks the state of the governor contract (for all proposal ids) + function _mockMemberGovState(IGovernorUpgradeable.ProposalState state) internal { + vm.mockCall( + address(initParams.securityCouncilMemberElectionGovernor), + abi.encodeWithSelector(IGovernorUpgradeable.state.selector), + abi.encode(state) + ); + } + + function _execute(uint256 electionIndex, bytes memory revertMsg) internal { + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) = governor.getProposeArgs(electionIndex); + if (revertMsg.length != 0) { + vm.expectRevert(revertMsg); + } + governor.execute(targets, values, calldatas, keccak256(bytes(description))); + } + + function _addContender(uint256 proposalId, address contender) internal { + _mockCohortIncludes(Cohort.SECOND, contender, false); + + vm.prank(contender); + governor.addContender(proposalId); + } + + function _castVoteForContender( + uint256 proposalId, + address voter, + address contender, + uint256 votes + ) internal { + vm.prank(voter); + governor.castVoteWithReasonAndParams({ + proposalId: proposalId, + support: 1, + reason: "", + params: abi.encode(contender, votes) + }); + } + + function _propose() internal returns (uint256) { + // mock the member gov state to be executed + _mockMemberGovState(IGovernorUpgradeable.ProposalState.Executed); + + // we need to mock getPastVotes for the proposer + _mockGetPastVotes({account: address(proposer), votes: 0}); + + vm.warp(_datePlusMonthsToTimestamp(initParams.firstNominationStartDate, 0)); + + vm.prank(proposer); + uint256 proposalId = governor.createElection(); + + vm.roll(block.number + 1); + + return proposalId; + } + + function _deployGovernor() internal returns (SecurityCouncilNomineeElectionGovernor) { + return SecurityCouncilNomineeElectionGovernor( + payable( + new TransparentUpgradeableProxy( + address(new SecurityCouncilNomineeElectionGovernor()), + proxyAdmin, + bytes("") + ) + ) + ); + } +} diff --git a/test/security-council-mgmt/governors/TopNomineesGas.t.sol b/test/security-council-mgmt/governors/TopNomineesGas.t.sol new file mode 100644 index 00000000..0c362eb4 --- /dev/null +++ b/test/security-council-mgmt/governors/TopNomineesGas.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; +import "../../../src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol"; +import "../../../src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol"; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @notice This contract tests gas usage of SecurityCouncilMemberElectionGovernor.topNominees() +contract TopNomineesGasTest is Test { + SecurityCouncilMemberElectionGovernor memberGov; + SecurityCouncilNomineeElectionGovernor nomineeGov; + + SecurityCouncilNomineeElectionGovernor.InitParams nomineeInitParams = + SecurityCouncilNomineeElectionGovernor.InitParams({ + firstNominationStartDate: Date({year: 2030, month: 1, day: 1, hour: 0}), + nomineeVettingDuration: 1 days, + nomineeVetter: address(0x11), + securityCouncilManager: ISecurityCouncilManager(address(0x22)), + securityCouncilMemberElectionGovernor: ISecurityCouncilMemberElectionGovernor( + payable(address(0x33)) + ), + token: IVotesUpgradeable(address(0x44)), + owner: address(0x55), + quorumNumeratorValue: 10_000 / N, + votingPeriod: 1 days + }); + + struct MemberInitParams { + ISecurityCouncilNomineeElectionGovernor nomineeElectionGovernor; + ISecurityCouncilManager securityCouncilManager; + IVotesUpgradeable token; + address owner; + uint256 votingPeriod; + uint256 maxNominees; + uint256 fullWeightDuration; + } + + SecurityCouncilMemberElectionGovernor governor; + + uint256 fullWeightDuration = 0.5 days; + + address proxyAdmin = address(0x1111); + address voter = address(0x2222); + + uint256 proposalId; + + uint16 constant N = 500; + + function setUp() public { + memberGov = SecurityCouncilMemberElectionGovernor( + payable(_deployProxy(address(new SecurityCouncilMemberElectionGovernor()))) + ); + nomineeGov = SecurityCouncilNomineeElectionGovernor( + payable(_deployProxy(address(new SecurityCouncilNomineeElectionGovernor()))) + ); + + // we need to etch code onto each contract parameter + _dummyEtch(address(nomineeInitParams.securityCouncilManager)); + _dummyEtch(address(nomineeInitParams.token)); + + memberGov.initialize({ + _nomineeElectionGovernor: nomineeGov, + _securityCouncilManager: nomineeInitParams.securityCouncilManager, + _token: nomineeInitParams.token, + _owner: nomineeInitParams.owner, + _votingPeriod: nomineeInitParams.votingPeriod, + _fullWeightDuration: fullWeightDuration + }); + + nomineeInitParams.securityCouncilMemberElectionGovernor = memberGov; + nomineeGov.initialize(nomineeInitParams); + + // mock stuff + _mockGetPastVotes(voter, 1_000_000_000e18); + _mockGetPastVotes({account: 0x00000000000000000000000000000000000A4B86, votes: 0}); + _mockGetPastVotes(address(nomineeGov), 0); + _mockGetPastTotalSupply(1_000_000_000e18); + _mockCohortSize(6); + + // start a nominee election + vm.warp(_datePlusMonthsToTimestamp(nomineeInitParams.firstNominationStartDate, 0)); + vm.prank(voter); + proposalId = nomineeGov.createElection(); + + // return; + // vote for N nominees + vm.roll(nomineeGov.proposalSnapshot(proposalId) + 1); + uint256 quorum = nomineeGov.quorum(proposalId); + for (uint16 i = 0; i < N; i++) { + _mockCohortIncludes(Cohort.SECOND, _nominee(i), false); + + vm.prank(_nominee(i)); + nomineeGov.addContender(proposalId); + + vm.prank(voter); + nomineeGov.castVoteWithReasonAndParams( + proposalId, 1, "test", abi.encode(_nominee(i), quorum) + ); + } + + assertEq(nomineeGov.compliantNominees(proposalId).length, N); + assertEq(nomineeGov.compliantNominees(proposalId)[N - 1], _nominee(N - 1)); + + // start the member election + vm.roll(nomineeGov.proposalVettingDeadline(proposalId) + 1); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) = nomineeGov.getProposeArgs(0); + nomineeGov.execute(targets, values, calldatas, keccak256(bytes(description))); + + // vote for the N nominees to create worst case ordering (ascending order of weight) + vm.roll(memberGov.proposalSnapshot(proposalId) + 1); + for (uint16 i = 0; i < N; i++) { + vm.prank(voter); + memberGov.castVoteWithReasonAndParams( + proposalId, 1, "test", abi.encode(_nominee(i), i + 1) + ); + } + + // call topNominees() in a test case to accurately measure gas + } + + function testTopNomineesGas() public { + uint256 g = gasleft(); + memberGov.topNominees(proposalId); + g -= gasleft(); + + assertLt(g, uint256(N) * 10_000); + } + + function _nominee(uint16 i) internal pure returns (address) { + return address(uint160(uint256(0x3300) + i)); + } + + function _dummyEtch(address x) internal { + vm.etch(x, hex"1234"); + } + + function _deployProxy(address impl) internal returns (address) { + return address( + new TransparentUpgradeableProxy( + impl, + proxyAdmin, + bytes("") + ) + ); + } + + function _mockGetPastVotes(address account, uint256 votes) internal { + vm.mockCall( + address(nomineeInitParams.token), + abi.encodeWithSelector(nomineeInitParams.token.getPastVotes.selector, account), + abi.encode(votes) + ); + } + + function _mockGetPastTotalSupply(uint256 amount) internal { + vm.mockCall( + address(nomineeInitParams.token), + abi.encodeWithSelector(nomineeInitParams.token.getPastTotalSupply.selector), + abi.encode(amount) + ); + } + + function _mockCohortIncludes(Cohort cohort, address member, bool ans) internal { + vm.mockCall( + address(nomineeInitParams.securityCouncilManager), + abi.encodeWithSelector( + nomineeInitParams.securityCouncilManager.cohortIncludes.selector, cohort, member + ), + abi.encode(ans) + ); + } + + function _mockCohortSize(uint256 count) internal { + vm.mockCall( + address(nomineeInitParams.securityCouncilManager), + abi.encodeWithSelector(nomineeInitParams.securityCouncilManager.cohortSize.selector), + abi.encode(count) + ); + + assertEq(nomineeInitParams.securityCouncilManager.cohortSize(), count); + } + + function _datePlusMonthsToTimestamp(Date memory date, uint256 months) + internal + pure + returns (uint256) + { + return DateTimeLib.dateTimeToTimestamp({ + year: date.year, + month: date.month + months, + day: date.day, + hour: date.hour, + minute: 0, + second: 0 + }); + } +} diff --git a/test/util/DeployGnosisWithModule.sol b/test/util/DeployGnosisWithModule.sol new file mode 100644 index 00000000..efa75b35 --- /dev/null +++ b/test/util/DeployGnosisWithModule.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "@gnosis.pm/safe-contracts/contracts/GnosisSafeL2.sol"; +import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol"; +import "forge-std/Test.sol"; + +contract DeployGnosisWithModule is Test { + function deploySafe(address[] memory _owners, uint256 _threshold, address _module) + public + returns (address safeAddress) + { + GnosisSafeL2 safeLogic = new GnosisSafeL2(); + GnosisSafeProxyFactory safeProxyFactory = new GnosisSafeProxyFactory(); + + GnosisSafeProxy safeProxy = safeProxyFactory.createProxy(address(safeLogic), "0x"); + GnosisSafeL2 safe = GnosisSafeL2(payable(address(safeProxy))); + safe.setup( + _owners, _threshold, address(0), "0x", address(0), address(0), 0, payable(address(0)) + ); + if (_module != address(0)) { + vm.prank(address(safe)); + safe.enableModule(_module); + } + return address(safe); + } +} diff --git a/test/util/InboxMock.sol b/test/util/InboxMock.sol index b28ef99d..e582fead 100644 --- a/test/util/InboxMock.sol +++ b/test/util/InboxMock.sol @@ -1,11 +1,12 @@ import "../../src/L1ArbitrumTimelock.sol"; contract InboxMock is IInboxSubmissionFee { + address l2ToL1SenderMock = address(0); uint256 public msgNum = 1; - address public bridge; + address private mbridge; constructor(address _bridge) { - bridge = _bridge; + mbridge = _bridge; } /// @dev msg.value sent to the inbox isn't high enough @@ -36,6 +37,21 @@ contract InboxMock is IInboxSubmissionFee { return (1400 + 6 * dataLength) * (baseFee == 0 ? block.basefee : baseFee); } + struct RetryableTicket { + address from; + address to; + uint256 l2CallValue; + uint256 value; + uint256 maxSubmissionCost; + address excessFeeRefundAddress; + address callValueRefundAddress; + uint256 gasLimit; + uint256 maxFeePerGas; + bytes data; + } + + RetryableTicket[] public retryableTickets; + function createRetryableTicket( address to, uint256 l2CallValue, @@ -72,6 +88,45 @@ contract InboxMock is IInboxSubmissionFee { revert InsufficientSubmissionCost(submissionFee, maxSubmissionCost); } + retryableTickets.push( + RetryableTicket({ + from: msg.sender, + to: to, + l2CallValue: l2CallValue, + value: msg.value, + maxSubmissionCost: maxSubmissionCost, + excessFeeRefundAddress: excessFeeRefundAddress, + callValueRefundAddress: callValueRefundAddress, + gasLimit: gasLimit, + maxFeePerGas: maxFeePerGas, + data: data + }) + ); + return msgNum++; } + + function getRetryableTicket(uint256 index) external view returns (RetryableTicket memory) { + return retryableTickets[index]; + } + + function bridge() external view returns (IBridge) { + if (mbridge != address(0)) { + return IBridge(mbridge); + } else { + return IBridge(address(this)); + } + } + + function activeOutbox() external view returns (address) { + return address(this); + } + + function setL2ToL1Sender(address sender) external { + l2ToL1SenderMock = sender; + } + + function l2ToL1Sender() external view returns (address) { + return l2ToL1SenderMock; + } } diff --git a/test/util/MockArbSys.sol b/test/util/MockArbSys.sol new file mode 100644 index 00000000..fb073712 --- /dev/null +++ b/test/util/MockArbSys.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +contract ArbSysMock { + event ArbSysL2ToL1Tx(address from, address to, uint256 value, bytes indexed data); + + uint256 counter; + + function sendTxToL1(address destination, bytes calldata calldataForL1) + external + payable + returns (uint256 exitNum) + { + exitNum = counter; + counter = exitNum + 1; + emit ArbSysL2ToL1Tx(msg.sender, destination, msg.value, calldataForL1); + return exitNum; + } +} diff --git a/test/util/TestUtil.sol b/test/util/TestUtil.sol index 8f0aece4..e63aa15c 100644 --- a/test/util/TestUtil.sol +++ b/test/util/TestUtil.sol @@ -3,10 +3,69 @@ pragma solidity 0.8.16; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@gnosis.pm/safe-contracts/contracts/GnosisSafeL2.sol"; + +contract StubContract {} library TestUtil { function deployProxy(address logic) public returns (address) { ProxyAdmin pa = new ProxyAdmin(); return address(new TransparentUpgradeableProxy(address(logic), address(pa), "")); } + + function deployStubContract() public returns (address) { + return address(new StubContract()); + } + + ///@notice assumes each address array has no repeated elements (i.e., as is the enforced for gnosis safe owners) + function areUniqueAddressArraysEqual(address[] memory array1, address[] memory array2) + public + pure + returns (bool) + { + if (array1.length != array2.length) { + return false; + } + + for (uint256 i = 0; i < array1.length; i++) { + bool found = false; + for (uint256 j = 0; j < array2.length; j++) { + if (array1[i] == array2[j]) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + + return true; + } + + function randomUint240s(uint256 length, uint256 seed) public pure returns (uint240[] memory) { + uint240[] memory arr = new uint240[](length); + for (uint256 i = 0; i < length; i++) { + arr[i] = uint240(uint256(keccak256(abi.encode(seed, i)))); + } + return arr; + } + + function randomAddresses(uint256 length, uint256 seed) public pure returns (address[] memory) { + uint240[] memory arr = randomUint240s(length, seed); + address[] memory addresses = new address[](length); + for (uint256 i = 0; i < length; i++) { + addresses[i] = address(uint160(arr[i])); + } + return addresses; + } + + function indexOf(address[] memory array, address element) public pure returns (uint256) { + for (uint256 i = 0; i < array.length; i++) { + if (array[i] == element) { + return i; + } + } + return type(uint256).max; + } } diff --git a/yarn.lock b/yarn.lock index 6c8452af..e0de3737 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,6 +49,42 @@ optionalDependencies: "@openzeppelin/upgrades-core" "^1.7.6" +"@chainsafe/as-sha256@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9" + integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg== + +"@chainsafe/persistent-merkle-tree@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz#4c9ee80cc57cd3be7208d98c40014ad38f36f7ff" + integrity sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + +"@chainsafe/persistent-merkle-tree@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz#2b4a62c9489a5739dedd197250d8d2f5427e9f63" + integrity sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + +"@chainsafe/ssz@^0.10.0": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.10.2.tgz#c782929e1bb25fec66ba72e75934b31fd087579e" + integrity sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + "@chainsafe/persistent-merkle-tree" "^0.5.0" + +"@chainsafe/ssz@^0.9.2": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497" + integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ== + dependencies: + "@chainsafe/as-sha256" "^0.3.1" + "@chainsafe/persistent-merkle-tree" "^0.4.2" + case "^1.6.3" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" @@ -56,7 +92,45 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.0-beta.146", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.7.0": +"@ethereumjs/common@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.5.0.tgz#ec61551b31bef7a69d1dc634d8932468866a4268" + integrity sha512-DEHjW6e38o+JmB/NO3GZBpW4lpaiBpkFgXF6jLcJ6gETBYpEyaA5nTimsWBUJR3Vmtm/didUEbNjajskugZORg== + dependencies: + crc-32 "^1.2.0" + ethereumjs-util "^7.1.1" + +"@ethereumjs/common@^2.5.0": + version "2.6.5" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.5.tgz#0a75a22a046272579d91919cb12d84f2756e8d30" + integrity sha512-lRyVQOeCDaIVtgfbowla32pzeDv2Obr8oR8Put5RdUBNRGr1VGPGQNGP6elWIpgK3YdpzqTOh4GyUGOureVeeA== + dependencies: + crc-32 "^1.2.0" + ethereumjs-util "^7.1.5" + +"@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/tx@3.3.2": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.2.tgz#348d4624bf248aaab6c44fec2ae67265efe3db00" + integrity sha512-6AaJhwg4ucmwTvw/1qLaZUX5miWrwZ4nLOUsKyb/HtzS3BMw/CasKhdi1ims9mBKeK9sOJCH4qGKOBGyJCeeog== + dependencies: + "@ethereumjs/common" "^2.5.0" + ethereumjs-util "^7.1.2" + +"@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.0-beta.146", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -144,7 +218,7 @@ dependencies: "@ethersproject/bignumber" "^5.7.0" -"@ethersproject/contracts@5.7.0": +"@ethersproject/contracts@5.7.0", "@ethersproject/contracts@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz" integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== @@ -247,7 +321,7 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.2": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2": version "5.7.2" resolved "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -310,7 +384,7 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/solidity@5.7.0": +"@ethersproject/solidity@5.7.0", "@ethersproject/solidity@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz" integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== @@ -331,7 +405,7 @@ "@ethersproject/constants" "^5.7.0" "@ethersproject/logger" "^5.7.0" -"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": +"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.6.2", "@ethersproject/transactions@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz" integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== @@ -398,6 +472,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@gnosis.pm/safe-contracts@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-contracts/-/safe-contracts-1.3.0.tgz#316741a7690d8751a1f701538cfc9ec80866eedc" + integrity sha512-1p+1HwGvxGUVzVkFjNzglwHrLNA67U/axP0Ct85FzzH8yhGJb4t9jDjPYocVMzLorDoWAfKicGy1akPY9jXRVw== + "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" @@ -427,11 +506,23 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/hashes@1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz" integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== +"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/hashes@~1.1.1": version "1.1.3" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz" @@ -463,6 +554,19 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/ethereumjs-block@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz#6f89664f55febbd723195b6d0974773d29ee133d" + integrity sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + ethereum-cryptography "0.1.3" + ethers "^5.7.1" + "@nomicfoundation/ethereumjs-block@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz" @@ -475,6 +579,25 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-blockchain@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz#80e0bd3535bfeb9baa29836b6f25123dab06a726" + integrity sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-ethash" "3.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + abstract-level "^1.0.3" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + level "^8.0.0" + lru-cache "^5.1.1" + memory-level "^1.0.0" + "@nomicfoundation/ethereumjs-blockchain@^6.0.0": version "6.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz" @@ -493,6 +616,14 @@ lru-cache "^5.1.1" memory-level "^1.0.0" +"@nomicfoundation/ethereumjs-common@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz#4702d82df35b07b5407583b54a45bf728e46a2f0" + integrity sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g== + dependencies: + "@nomicfoundation/ethereumjs-util" "9.0.1" + crc-32 "^1.2.0" + "@nomicfoundation/ethereumjs-common@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz" @@ -501,6 +632,18 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" crc-32 "^1.2.0" +"@nomicfoundation/ethereumjs-ethash@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz#65ca494d53e71e8415c9a49ef48bc921c538fc41" + integrity sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + abstract-level "^1.0.3" + bigint-crypto-utils "^3.0.23" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-ethash@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz" @@ -513,6 +656,20 @@ bigint-crypto-utils "^3.0.23" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-evm@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz#f35681e203363f69ce2b3d3bf9f44d4e883ca1f1" + integrity sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ== + dependencies: + "@ethersproject/providers" "^5.7.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + mcl-wasm "^0.7.1" + rustbn.js "~0.2.0" + "@nomicfoundation/ethereumjs-evm@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz" @@ -527,11 +684,28 @@ mcl-wasm "^0.7.1" rustbn.js "~0.2.0" +"@nomicfoundation/ethereumjs-rlp@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz#0b30c1cf77d125d390408e391c4bb5291ef43c28" + integrity sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ== + "@nomicfoundation/ethereumjs-rlp@^4.0.0", "@nomicfoundation/ethereumjs-rlp@^4.0.0-beta.2": version "4.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz" integrity sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw== +"@nomicfoundation/ethereumjs-statemanager@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz#8824a97938db4471911e2d2f140f79195def5935" + integrity sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + ethers "^5.7.1" + js-sdsl "^4.1.4" + "@nomicfoundation/ethereumjs-statemanager@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz" @@ -545,6 +719,17 @@ ethereum-cryptography "0.1.3" functional-red-black-tree "^1.0.1" +"@nomicfoundation/ethereumjs-trie@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz#662c55f6b50659fd4b22ea9f806a7401cafb7717" + integrity sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + "@types/readable-stream" "^2.3.13" + ethereum-cryptography "0.1.3" + readable-stream "^3.6.0" + "@nomicfoundation/ethereumjs-trie@^5.0.0": version "5.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz" @@ -555,6 +740,18 @@ ethereum-cryptography "0.1.3" readable-stream "^3.6.0" +"@nomicfoundation/ethereumjs-tx@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz#7629dc2036b4a33c34e9f0a592b43227ef4f0c7d" + integrity sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w== + dependencies: + "@chainsafe/ssz" "^0.9.2" + "@ethersproject/providers" "^5.7.2" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-tx@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz" @@ -565,6 +762,15 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-util@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz#530cda8bae33f8b5020a8f199ed1d0a2ce48ec89" + integrity sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA== + dependencies: + "@chainsafe/ssz" "^0.10.0" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-util@^8.0.0": version "8.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz" @@ -573,6 +779,25 @@ "@nomicfoundation/ethereumjs-rlp" "^4.0.0-beta.2" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-vm@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz#7d035e0993bcad10716c8b36e61dfb87fa3ca05f" + integrity sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-blockchain" "7.0.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-evm" "2.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-statemanager" "2.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + debug "^4.3.3" + ethereum-cryptography "0.1.3" + mcl-wasm "^0.7.1" + rustbn.js "~0.2.0" + "@nomicfoundation/ethereumjs-vm@^6.0.0": version "6.0.0" resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz" @@ -607,6 +832,13 @@ deep-eql "^4.0.1" ordinal "^1.0.3" +"@nomicfoundation/hardhat-foundry@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.0.2.tgz#281627e4872bba47c1cd4fcf47fff2259df8d5c6" + integrity sha512-NbOrtIKcss51h+1h/TaZkCxOn9KaGbLd6/l75V4X7oDhonGmMWevwrKTNMHC90wCATDkV+B37jmn3fGbWCs+Vg== + dependencies: + chalk "^2.4.2" + "@nomicfoundation/hardhat-network-helpers@^1.0.6": version "1.0.6" resolved "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.6.tgz" @@ -749,6 +981,39 @@ proper-lockfile "^4.1.1" solidity-ast "^0.4.15" +"@safe-global/protocol-kit@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-1.2.0.tgz#5501fa0e2a1b4ad03cd5a4ee12bb9e799e1dd5a7" + integrity sha512-drU2uK30AZ4tqI/9ER7PGMD/lZp/5B9T02t+noTk7WF9Xb7HxskJd8GNU01KE55oyH31Y0AfXaE68H/f9lYa4A== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/solidity" "^5.7.0" + "@safe-global/safe-deployments" "^1.26.0" + ethereumjs-util "^7.1.5" + semver "^7.5.4" + web3 "^1.8.1" + web3-core "^1.8.1" + web3-utils "^1.8.1" + +"@safe-global/safe-core-sdk-types@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-2.2.0.tgz#2e34a5089035719e9a92a0bc6aa181c2edb0f108" + integrity sha512-vVG9qQnUYx+Xwsbuqraq25MPJX1I1aV1P81ZnHZa1lEMU7stqYWAmykUm/mvqsm8+AsvEB/wBKlFjbFJ/duzoA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/contracts" "^5.7.0" + "@safe-global/safe-deployments" "^1.26.0" + web3-core "^1.8.1" + web3-utils "^1.8.1" + +"@safe-global/safe-deployments@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.26.0.tgz#b83615b3b5a66e736e08f8ecf2801ed988e9e007" + integrity sha512-Tw89O4/paT19ieMoiWQbqRApb0Bef/DxweS9rxodXAM5EQModkbyFXGZca+YxXE67sLvWjLr2jJUOxwze8mhGw== + dependencies: + semver "^7.3.7" + "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz" @@ -763,6 +1028,15 @@ "@noble/secp256k1" "~1.6.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== + dependencies: + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" + "@scure/base" "~1.1.0" + "@scure/bip39@1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz" @@ -771,6 +1045,14 @@ "@noble/hashes" "~1.1.1" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz" @@ -839,6 +1121,11 @@ "@sentry/types" "5.30.0" tslib "^1.9.3" +"@sindresorhus/is@^4.0.0", "@sindresorhus/is@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + "@solidity-parser/parser@^0.14.0", "@solidity-parser/parser@^0.14.1": version "0.14.3" resolved "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.3.tgz" @@ -853,6 +1140,20 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + +"@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" + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz" @@ -900,13 +1201,23 @@ dependencies: "@types/node" "*" -"@types/bn.js@^5.1.0": +"@types/bn.js@^5.1.0", "@types/bn.js@^5.1.1": version "5.1.1" resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz" integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== dependencies: "@types/node" "*" +"@types/cacheable-request@^6.0.1", "@types/cacheable-request@^6.0.2": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + "@types/chai-as-promised@^7.1.3": version "7.1.5" resolved "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz" @@ -941,6 +1252,18 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/http-cache-semantics@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" + integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + "@types/lru-cache@^5.1.0": version "5.1.1" resolved "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz" @@ -966,6 +1289,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== +"@types/node@^12.12.6": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + "@types/node@^8.0.0": version "8.10.66" resolved "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz" @@ -988,6 +1316,21 @@ resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== +"@types/readable-stream@^2.3.13": + version "2.3.15" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.15.tgz#3d79c9ceb1b6a57d5f6e6976f489b9b5384321ae" + integrity sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + +"@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/secp256k1@^4.0.1": version "4.0.3" resolved "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz" @@ -1044,6 +1387,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abortcontroller-polyfill@^1.7.3: + version "1.7.5" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" + integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ== + abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz" @@ -1057,6 +1405,14 @@ abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3: module-error "^1.0.1" queue-microtask "^1.2.3" +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" @@ -1213,6 +1569,11 @@ array-back@^4.0.1, array-back@^4.0.2: resolved "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz" integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -1268,6 +1629,11 @@ async-eventemitter@^0.2.4: dependencies: async "^2.4.0" +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async@1.x: version "1.5.2" resolved "https://registry.npmjs.org/async/-/async-1.5.2.tgz" @@ -1304,6 +1670,11 @@ audit-ci@^6.6.1: semver "^7.0.0" yargs "^17.0.0" +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" @@ -1327,7 +1698,7 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-x@^3.0.2: +base-x@^3.0.2, base-x@^3.0.8: version "3.0.9" resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz" integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== @@ -1370,6 +1741,11 @@ bigint-mod-arith@^3.1.0: resolved "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.1.2.tgz" integrity sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ== +bignumber.js@^9.0.0: + version "9.1.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" + integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== + bignumber.js@^9.0.1: version "9.1.0" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz" @@ -1394,12 +1770,17 @@ blakejs@^1.1.0: resolved "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz" integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + bn.js@4.11.6: version "4.11.6" resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz" integrity sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA== -bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9: +bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -1409,6 +1790,42 @@ bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.16.0: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" @@ -1494,12 +1911,17 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer-to-arraybuffer@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz#6064a40fa76eb43c723aba9ef8f6e1216d10511a" + integrity sha512-3dthu5CYiVB1DEJp61FtApNnNndTckcqe4pFcLdvHtrpG+kcyekCJKg4MRiDcFW7A6AODnXB9U4dwQiCW5kzJQ== + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== -buffer@^5.2.1, buffer@^5.5.0: +buffer@^5.0.5, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -1515,6 +1937,13 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bufferutil@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" + integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + dependencies: + node-gyp-build "^4.3.0" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" @@ -1527,6 +1956,29 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-lookup@^6.0.4: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz#0330a543471c61faa4e9035db583aad753b36385" + integrity sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww== + +cacheable-request@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" + integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -1545,6 +1997,11 @@ camelcase@^6.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +case@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" + integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== + caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" @@ -1672,7 +2129,7 @@ chokidar@3.5.3, chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.1: +chownr@^1.1.1, chownr@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -1682,6 +2139,17 @@ ci-info@^2.0.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +cids@^0.7.1: + version "0.7.5" + resolved "https://registry.yarnpkg.com/cids/-/cids-0.7.5.tgz#60a08138a99bfb69b6be4ceb63bfef7a396b28b2" + integrity sha512-zT7mPeghoWAu+ppn8+BS1tQ5qGmbMfB4AregnQjA/qHY3GC1m1ptI9GkWNlgeu38r7CuRdXB47uY2XgAYt6QVA== + dependencies: + buffer "^5.5.0" + class-is "^1.1.0" + multibase "~0.6.0" + multicodec "^1.0.0" + multihashes "~0.4.15" + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz" @@ -1690,6 +2158,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +class-is@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/class-is/-/class-is-1.1.0.tgz#9d3c0fba0440d211d843cec3dedfa48055005825" + integrity sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw== + classic-level@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/classic-level/-/classic-level-1.2.0.tgz" @@ -1743,6 +2216,13 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -1834,6 +2314,27 @@ concat-stream@^1.6.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-hash@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/content-hash/-/content-hash-2.5.2.tgz#bbc2655e7c21f14fd3bfc7b7d4bfe6e454c9e211" + integrity sha512-FvIQKy0S1JaWV10sMsA7TRx8bpU+pqPkhbsfvOJAdjRXvYxEckAwQWGwtRjiaJfh+E0DvcWUGqcdjwMGFjsSdw== + dependencies: + cids "^0.7.1" + multicodec "^0.5.5" + multihashes "^0.4.15" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-svg-core@^0.6.4: version "0.6.4" resolved "https://registry.npmjs.org/convert-svg-core/-/convert-svg-core-0.6.4.tgz" @@ -1858,6 +2359,16 @@ convert-svg-to-png@^0.6.4: dependencies: convert-svg-core "^0.6.4" +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookie@^0.4.1: version "0.4.2" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" @@ -1873,6 +2384,14 @@ core-util-is@~1.0.0: resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.1: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + corser@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" @@ -1918,6 +2437,13 @@ cross-fetch@3.1.5: dependencies: node-fetch "2.6.7" +cross-fetch@^3.1.4: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" @@ -1959,6 +2485,14 @@ css-what@^6.1.0: resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" @@ -1971,6 +2505,13 @@ death@^1.1.0: resolved "https://registry.npmjs.org/death/-/death-1.1.0.tgz" integrity sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w== +debug@2.6.9, debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@3.2.6: version "3.2.6" resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" @@ -2002,6 +2543,25 @@ decamelize@^4.0.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +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@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" @@ -2026,6 +2586,11 @@ deep-is@~0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +defer-to-connect@^2.0.0, 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-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" @@ -2044,6 +2609,11 @@ depd@2.0.0: resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + detect-port@^1.3.0: version "1.5.1" resolved "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz" @@ -2095,6 +2665,11 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" +dom-walk@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" + integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== + domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" @@ -2134,7 +2709,12 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -elliptic@6.5.4, elliptic@^6.5.2, elliptic@^6.5.4: +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -2157,6 +2737,11 @@ emoji-regex@^8.0.0: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" @@ -2225,11 +2810,47 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-promise@^4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" @@ -2272,6 +2893,19 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eth-ens-namehash@2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf" + integrity sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw== + dependencies: + idna-uts46-hx "^2.3.1" + js-sha3 "^0.5.7" + eth-gas-reporter@^0.2.25: version "0.2.25" resolved "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.2.25.tgz" @@ -2293,6 +2927,27 @@ eth-gas-reporter@^0.2.25: sha1 "^1.1.1" sync-request "^6.0.0" +eth-lib@0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.2.8.tgz#b194058bef4b220ad12ea497431d6cb6aa0623c8" + integrity sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw== + dependencies: + bn.js "^4.11.6" + elliptic "^6.4.0" + xhr-request-promise "^0.1.2" + +eth-lib@^0.1.26: + version "0.1.29" + resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.1.29.tgz#0c11f5060d42da9f931eab6199084734f4dbd1d9" + integrity sha512-bfttrr3/7gG4E02HoWTDUcDDslN003OlOoBxk9virpAZQ1ja/jDgwkWB8QfJF7ojuEowrqy+lzp9VcJG7/k5bQ== + dependencies: + bn.js "^4.11.6" + elliptic "^6.4.0" + nano-json-stream-parser "^0.1.2" + servify "^0.1.12" + ws "^3.0.0" + xhr-request-promise "^0.1.2" + ethereum-bloom-filters@^1.0.6: version "1.0.10" resolved "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz" @@ -2331,6 +2986,16 @@ ethereum-cryptography@^1.0.3: "@scure/bip32" "1.1.0" "@scure/bip39" "1.1.0" +ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" + integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== + dependencies: + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + ethereumjs-abi@^0.6.8: version "0.6.8" resolved "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz" @@ -2352,7 +3017,7 @@ ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: ethjs-util "0.1.6" rlp "^2.2.3" -ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.4: +ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.4, ethereumjs-util@^7.1.5: version "7.1.5" resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz" integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== @@ -2378,7 +3043,7 @@ ethers@^4.0.40: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^5.1.0, ethers@^5.6.9, ethers@^5.7.2: +ethers@^5.1.0, ethers@^5.6.9, ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -2448,6 +3113,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" + integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -2461,6 +3131,50 @@ evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +express@^4.14.0: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" @@ -2539,6 +3253,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-replace@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz" @@ -2600,11 +3327,23 @@ follow-redirects@^1.0.0, follow-redirects@^1.12.1, follow-redirects@^1.14.9: resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data-encoder@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96" + integrity sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg== + form-data@^2.2.0: version "2.5.1" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz" @@ -2632,6 +3371,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + fp-ts@1.19.3: version "1.19.3" resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz" @@ -2642,6 +3386,11 @@ fp-ts@^1.0.0: resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.5.tgz" integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + from@^0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/from/-/from-0.1.7.tgz" @@ -2663,6 +3412,15 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" +fs-extra@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^7.0.0, fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz" @@ -2691,6 +3449,13 @@ fs-extra@^9.0.0, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-minipass@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + fs-readdir-recursive@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz" @@ -2772,6 +3537,11 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" +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-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" @@ -2888,6 +3658,14 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" +global@~4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" + integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== + dependencies: + min-document "^2.19.0" + process "^0.11.10" + globby@^10.0.1: version "10.0.2" resolved "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz" @@ -2902,6 +3680,49 @@ globby@^10.0.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.1.0" + resolved "https://registry.yarnpkg.com/got/-/got-12.1.0.tgz#099f3815305c682be4fd6b0ee0726d8e4c6b0af4" + integrity sha512-hBv2ty9QN2RdbJJMK3hesmSkFTjVIHyIDDbssCKnSmq62edGgImJWD10Eb1k77TiV1bxloxqcFAVK8+9pkhOig== + dependencies: + "@sindresorhus/is" "^4.6.0" + "@szmarczak/http-timer" "^5.0.1" + "@types/cacheable-request" "^6.0.2" + "@types/responselike" "^1.0.0" + cacheable-lookup "^6.0.4" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + form-data-encoder "1.7.1" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^2.0.0" + +got@^11.8.5: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" @@ -2946,23 +3767,23 @@ hardhat-gas-reporter@^1.0.9: eth-gas-reporter "^0.2.25" sha1 "^1.1.1" -hardhat@^2.12.0: - version "2.12.0" - resolved "https://registry.npmjs.org/hardhat/-/hardhat-2.12.0.tgz" - integrity sha512-mNJFbVG479HwOzxiaLxobyvED2M1aEAuPPYhEo1+88yicMDSTrU2JIS7vV+V0GSNQKaDoiHCmV6bcKjiljT/dQ== +hardhat@^2.12.6: + version "2.17.0" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.17.0.tgz#574478790fa4f4a45c5ccf162e82e54f36671749" + integrity sha512-CaEGa13tkJNe2/rdaBiive4pmdNShwxvdWVhr1zfb6aVpRhQt9VNO0l/UIBt/zzajz38ZFjvhfM2bj8LDXo9gw== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0" - "@nomicfoundation/ethereumjs-block" "^4.0.0" - "@nomicfoundation/ethereumjs-blockchain" "^6.0.0" - "@nomicfoundation/ethereumjs-common" "^3.0.0" - "@nomicfoundation/ethereumjs-evm" "^1.0.0" - "@nomicfoundation/ethereumjs-rlp" "^4.0.0" - "@nomicfoundation/ethereumjs-statemanager" "^1.0.0" - "@nomicfoundation/ethereumjs-trie" "^5.0.0" - "@nomicfoundation/ethereumjs-tx" "^4.0.0" - "@nomicfoundation/ethereumjs-util" "^8.0.0" - "@nomicfoundation/ethereumjs-vm" "^6.0.0" + "@nomicfoundation/ethereumjs-block" "5.0.1" + "@nomicfoundation/ethereumjs-blockchain" "7.0.1" + "@nomicfoundation/ethereumjs-common" "4.0.1" + "@nomicfoundation/ethereumjs-evm" "2.0.1" + "@nomicfoundation/ethereumjs-rlp" "5.0.1" + "@nomicfoundation/ethereumjs-statemanager" "2.0.1" + "@nomicfoundation/ethereumjs-trie" "6.0.1" + "@nomicfoundation/ethereumjs-tx" "5.0.1" + "@nomicfoundation/ethereumjs-util" "9.0.1" + "@nomicfoundation/ethereumjs-vm" "7.0.1" "@nomicfoundation/solidity-analyzer" "^0.1.0" "@sentry/node" "^5.18.1" "@types/bn.js" "^5.1.0" @@ -2990,7 +3811,6 @@ hardhat@^2.12.0: mnemonist "^0.38.0" mocha "^10.0.0" p-map "^4.0.0" - qs "^6.7.0" raw-body "^2.4.1" resolve "1.17.0" semver "^6.3.0" @@ -2998,7 +3818,7 @@ hardhat@^2.12.0: source-map-support "^0.5.13" stacktrace-parser "^0.1.10" tsort "0.0.1" - undici "^5.4.0" + undici "^5.14.0" uuid "^8.3.2" ws "^7.4.6" @@ -3175,6 +3995,11 @@ http-basic@^8.1.1: http-response-object "^3.0.1" parse-cache-control "^1.0.1" +http-cache-semantics@^4.0.0: + 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.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -3186,6 +4011,11 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-https@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" + integrity sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg== + http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" @@ -3230,6 +4060,22 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + +http2-wrapper@^2.1.10: + version "2.2.0" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.0.tgz#b80ad199d216b7d3680195077bd7b9060fa9d7f3" + integrity sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" @@ -3252,6 +4098,13 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idna-uts46-hx@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz#a1dc5c4df37eee522bf66d969cc980e00e8711f9" + integrity sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA== + dependencies: + punycode "2.1.0" + ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" @@ -3311,6 +4164,19 @@ io-ts@1.10.4: dependencies: fp-ts "^1.0.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" @@ -3338,7 +4204,7 @@ is-buffer@^2.0.5, is-buffer@~2.0.3: resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -3384,6 +4250,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-function@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" + integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== + +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -3447,7 +4325,14 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typedarray@~1.0.0: +is-typed-array@^1.1.3: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -3496,7 +4381,12 @@ js-graph-algorithms@^1.0.18: resolved "https://registry.npmjs.org/js-graph-algorithms/-/js-graph-algorithms-1.0.18.tgz" integrity sha512-Gu1wtWzXBzGeye/j9BuyplGHscwqKRZodp/0M1vyBc19RJpblSwKGu099KwwaTx9cRIV+Qupk8xUMfEiGfFqSA== -js-sha3@0.5.7: +js-sdsl@^4.1.4: + version "4.4.2" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" + integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== + +js-sha3@0.5.7, js-sha3@^0.5.7: version "0.5.7" resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz" integrity sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g== @@ -3534,6 +4424,11 @@ jsbn@~0.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== +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-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -3606,6 +4501,13 @@ keccak@^3.0.0, keccak@^3.0.2: node-gyp-build "^4.2.0" readable-stream "^3.6.0" +keyv@^4.0.0: + version "4.5.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" + integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" @@ -3736,6 +4638,16 @@ loupe@^2.3.1: dependencies: get-func-name "^2.0.0" +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +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-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" @@ -3784,6 +4696,11 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + memory-level@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/memory-level/-/memory-level-1.0.0.tgz" @@ -3798,11 +4715,26 @@ memorystream@^0.3.1: resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + merge2@^1.2.3, merge2@^1.3.0: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +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.2, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" @@ -3816,18 +4748,35 @@ mime-db@1.52.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" -mime@^1.6.0: +mime@1.6.0, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +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== + +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ== + dependencies: + dom-walk "^0.1.0" + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" @@ -3871,11 +4820,38 @@ minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minipass@^2.6.0, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mkdirp-promise@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1" + integrity sha512-Hepn5kb1lJPtVW84RFT40YG1OddBNTOVUZR2bzQUHc+Z03en8/3uX0+060JDhcEzyO08HmipsN9DcnFMxhIL9w== + dependencies: + mkdirp "*" + +mkdirp@*: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + mkdirp@0.5.5: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -3883,7 +4859,7 @@ mkdirp@0.5.5: dependencies: minimist "^1.2.5" -mkdirp@0.5.x, mkdirp@^0.5.6: +mkdirp@0.5.x, mkdirp@^0.5.5, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -3989,11 +4965,21 @@ mocha@^7.1.1: yargs-parser "13.1.2" yargs-unparser "1.6.0" +mock-fs@^4.1.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" + integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== + module-error@^1.0.1, module-error@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz" integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.1, ms@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" @@ -4009,6 +4995,51 @@ ms@2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multibase@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.7.0.tgz#1adfc1c50abe05eefeb5091ac0c2728d6b84581b" + integrity sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg== + dependencies: + base-x "^3.0.8" + buffer "^5.5.0" + +multibase@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.6.1.tgz#b76df6298536cc17b9f6a6db53ec88f85f8cc12b" + integrity sha512-pFfAwyTjbbQgNc3G7D48JkJxWtoJoBMaR4xQUOuB8RnCgRqaYmWNFeJTTvrJ2w51bjLq2zTby6Rqj9TQ9elSUw== + dependencies: + base-x "^3.0.8" + buffer "^5.5.0" + +multicodec@^0.5.5: + version "0.5.7" + resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-0.5.7.tgz#1fb3f9dd866a10a55d226e194abba2dcc1ee9ffd" + integrity sha512-PscoRxm3f+88fAtELwUnZxGDkduE2HD9Q6GHUOywQLjOGT/HAdhjLDYNZ1e7VR0s0TP0EwZ16LNUTFpoBGivOA== + dependencies: + varint "^5.0.0" + +multicodec@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-1.0.4.tgz#46ac064657c40380c28367c90304d8ed175a714f" + integrity sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg== + dependencies: + buffer "^5.6.0" + varint "^5.0.0" + +multihashes@^0.4.15, multihashes@~0.4.15: + version "0.4.21" + resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-0.4.21.tgz#dc02d525579f334a7909ade8a122dabb58ccfcb5" + integrity sha512-uVSvmeCWf36pU2nB4/1kzYZjsXD9vofZKpgudqkceYY5g2aZZXJ5r9lxuzoRLl1OAp28XljXsEJ/X/85ZsKmKw== + dependencies: + buffer "^5.5.0" + multibase "^0.7.0" + varint "^5.0.0" + +nano-json-stream-parser@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f" + integrity sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew== + nanoid@3.3.3: version "3.3.3" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz" @@ -4019,11 +5050,21 @@ napi-macros@~2.0.0: resolved "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz" integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.6.0: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" @@ -4056,6 +5097,13 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.6.12" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" + integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: version "4.5.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz" @@ -4083,6 +5131,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" @@ -4103,7 +5156,7 @@ oauth-sign@~0.9.0: resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.0: +object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -4153,6 +5206,20 @@ obliterator@^2.0.0: resolved "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz" integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== +oboe@2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/oboe/-/oboe-2.1.5.tgz#5554284c543a2266d7a38f17e073821fbde393cd" + integrity sha512-zRFWiF+FoicxEs3jNI/WYUrVEgA7DeET/InK0XQuudGHRg8iIob3cNPrJTKaz4004uaA9Pbe+Dwa8iluhjLZWA== + dependencies: + http-https "^1.0.0" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -4195,6 +5262,16 @@ os-tmpdir@~1.0.2: resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + +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.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" @@ -4266,6 +5343,11 @@ parse-cache-control@^1.0.1: resolved "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz" integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== +parse-headers@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.5.tgz#069793f9356a54008571eb7f9761153e6c770da9" + integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== + parse5-htmlparser2-tree-adapter@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz" @@ -4281,6 +5363,11 @@ parse5@^7.0.0: dependencies: entities "^4.4.0" +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + patch-package@^6.4.7: version "6.5.1" resolved "https://registry.npmjs.org/patch-package/-/patch-package-6.5.1.tgz" @@ -4331,6 +5418,11 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -4415,6 +5507,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" @@ -4436,6 +5533,14 @@ proper-lockfile@^4.1.1: retry "^0.12.0" signal-exit "^3.0.2" +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + proxy-from-env@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" @@ -4454,6 +5559,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" + integrity sha512-Yxz2kRwT90aPiWEMHVYnEf4+rhwF1tBmmZ4KepCP+Wkium9JxtWnUm1nqGwpiAHr/tnTSeHqr3wb++jgSkXjhA== + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" @@ -4477,7 +5587,7 @@ puppeteer@^13.7.0: unbzip2-stream "1.4.3" ws "8.5.0" -qs@^6.4.0, qs@^6.7.0: +qs@6.11.0, qs@^6.4.0, qs@^6.7.0: version "6.11.0" resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -4489,11 +5599,25 @@ qs@~6.5.2: resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" 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.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -4501,7 +5625,12 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -raw-body@^2.4.1: +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1, raw-body@^2.4.1: version "2.5.1" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== @@ -4511,6 +5640,16 @@ raw-body@^2.4.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + 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" + readable-stream@^2.2.2: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" @@ -4610,7 +5749,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.88.0: +request@^2.79.0, request@^2.88.0: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -4656,6 +5795,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resolve-alpn@^1.0.0, 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.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz" @@ -4682,6 +5826,13 @@ resolve@^1.1.6: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + retry@^0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" @@ -4745,7 +5896,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -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: +safe-buffer@5.2.1, 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.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4789,7 +5940,7 @@ scrypt-js@2.0.4: resolved "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz" integrity sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw== -scrypt-js@3.0.1, scrypt-js@^3.0.0: +scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== @@ -4818,7 +5969,7 @@ semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.0.0: +semver@^7.0.0, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -4832,6 +5983,25 @@ semver@^7.3.4: dependencies: lru-cache "^6.0.0" +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" @@ -4839,6 +6009,27 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +servify@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/servify/-/servify-0.1.12.tgz#142ab7bee1f1d033b66d0707086085b17c06db95" + integrity sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw== + dependencies: + body-parser "^1.16.0" + cors "^2.8.1" + express "^4.14.0" + request "^2.79.0" + xhr "^2.3.3" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" @@ -4922,6 +6113,20 @@ signal-exit@^3.0.2: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^2.7.0: + version "2.8.2" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.2.tgz#5708fb0919d440657326cd5fe7d2599d07705019" + integrity sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw== + dependencies: + decompress-response "^3.3.0" + once "^1.3.1" + simple-concat "^1.0.0" + slash@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz" @@ -5079,6 +6284,11 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== + string-format@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz" @@ -5220,6 +6430,23 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swarm-js@^0.1.40: + version "0.1.42" + resolved "https://registry.yarnpkg.com/swarm-js/-/swarm-js-0.1.42.tgz#497995c62df6696f6e22372f457120e43e727979" + integrity sha512-BV7c/dVlA3R6ya1lMlSSNPLYrntt0LUq4YMgy3iwpCIc6rZnS5W2wUoctarZ5pXlpKtxDDf9hNziEkcfrxdhqQ== + dependencies: + bluebird "^3.5.0" + buffer "^5.0.5" + eth-lib "^0.1.26" + fs-extra "^4.0.2" + got "^11.8.5" + mime-types "^2.1.16" + mkdirp-promise "^5.0.1" + mock-fs "^4.1.0" + setimmediate "^1.0.5" + tar "^4.0.2" + xhr-request "^1.0.1" + sync-request@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz" @@ -5278,6 +6505,19 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^4.0.2: + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" + then-request@^6.0.0: version "6.0.2" resolved "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz" @@ -5300,6 +6540,11 @@ through@2, "through@>=2.2.7 <3", through@^2.3.8, through@~2.3, through@~2.3.4: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +timed-out@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA== + tmp@0.0.33, tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -5427,6 +6672,24 @@ type-fest@^0.7.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typechain@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/typechain/-/typechain-8.1.0.tgz" @@ -5443,6 +6706,13 @@ typechain@^8.1.0: ts-command-line-args "^2.2.0" ts-essentials "^7.0.1" +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" @@ -5468,6 +6738,11 @@ uglify-js@^3.1.4: resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.3.tgz" integrity sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg== +ultron@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" + integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -5486,6 +6761,13 @@ unbzip2-stream@1.4.3: buffer "^5.2.1" through "^2.3.8" +undici@^5.14.0: + version "5.22.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" + integrity sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw== + dependencies: + busboy "^1.6.0" + undici@^5.4.0: version "5.11.0" resolved "https://registry.npmjs.org/undici/-/undici-5.11.0.tgz" @@ -5510,7 +6792,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -5527,6 +6809,18 @@ url-join@^4.0.1: resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== +url-set-query@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-set-query/-/url-set-query-1.0.0.tgz#016e8cfd7c20ee05cafe7795e892bd0702faa339" + integrity sha512-3AChu4NiXquPfeckE5R5cGdiHCMWJx1dwCWOmWIL4KHAziJNOFIYJlpGFeKDvwLPHovZRCxK3cYlwzqI9Vp+Gg== + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + utf8@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz" @@ -5537,6 +6831,22 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + uuid@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz" @@ -5552,11 +6862,26 @@ uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +varint@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" + integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + verror@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" @@ -5566,6 +6891,223 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +web3-bzz@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.10.1.tgz#29edb8e91e806a4cf69de0735b4cdc18b21fb4f0" + integrity sha512-0T2BTYm9mLPpnRJuXSS7PA39dTXCPj6a3/Qdee84Plm6WsSIl4aZooJ4YUMnlII8HjyzwiIzjnH7AEZrBcBu9w== + dependencies: + "@types/node" "^12.12.6" + got "12.1.0" + swarm-js "^0.1.40" + +web3-core-helpers@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.10.1.tgz#dd597adc758efe03b380f1423a4da3de1757530b" + integrity sha512-lgOgdiIyIIXxIVjEHjT8PC2CsjFvvBXfVF0Xq5SiRcPKj47B2F7uur0gPoPc6e6+kjo49qEqLlx6eZKOkCAR1A== + dependencies: + web3-eth-iban "1.10.1" + web3-utils "1.10.1" + +web3-core-method@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.10.1.tgz#decd9a11d95c199960477b297a45b8f135ac7770" + integrity sha512-QEqgMsagp6vs0GOlI4QHzZcsvzJs+Zp1Eo8uOZgosYoRfusklzfPmX4OYg4H6XyenCavgvmAkxw0g8y8hlLHiQ== + dependencies: + "@ethersproject/transactions" "^5.6.2" + web3-core-helpers "1.10.1" + web3-core-promievent "1.10.1" + web3-core-subscriptions "1.10.1" + web3-utils "1.10.1" + +web3-core-promievent@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.10.1.tgz#d20b1328d1ff8881acb8cf4b5749bb2218798b0e" + integrity sha512-ggInbRxkx0n0FVMU5GXx9pbTwq7rfF2DJ6J6AafifOC0P0269TbHfFKMlU7B5K5i6/VQxrsY9fBPf6am9DmQuw== + dependencies: + eventemitter3 "4.0.4" + +web3-core-requestmanager@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.10.1.tgz#f0c765ae811c1c0b24c7c8ede8e9a254bc520fce" + integrity sha512-hBHuKbh8PGrSs4vTg2EA7xM+BIDVOrmOZnK4I+KeWw8zZr6bmhhk8xkmtKo2/0fADAkvVqMiJwuZcpRr3DILnw== + dependencies: + util "^0.12.5" + web3-core-helpers "1.10.1" + web3-providers-http "1.10.1" + web3-providers-ipc "1.10.1" + web3-providers-ws "1.10.1" + +web3-core-subscriptions@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.10.1.tgz#1e19ef59f9844c31a85f1b9c17088a786029c118" + integrity sha512-6B7cA7lUwCAh7X55gTMfFkC9L8en3bddqFi+VNO9SO9af62t2L5xTb8pxZEFirIF4s4qKxKekLgZrRhpmlO3eA== + dependencies: + eventemitter3 "4.0.4" + web3-core-helpers "1.10.1" + +web3-core@1.10.1, web3-core@^1.8.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.1.tgz#a4cb471356c4a197654b61adc9d0b03357da4258" + integrity sha512-a45WF/e2VeSs17UTmmWhEaMDv/A+N6qchA7zepvdvwUGCZME39YWCmbsjAYjkq0btsXueOIBpS6fLuq5VoLkFg== + dependencies: + "@types/bn.js" "^5.1.1" + "@types/node" "^12.12.6" + bignumber.js "^9.0.0" + web3-core-helpers "1.10.1" + web3-core-method "1.10.1" + web3-core-requestmanager "1.10.1" + web3-utils "1.10.1" + +web3-eth-abi@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.1.tgz#4d64d1d272f35f7aaae7be9679493474e0af86c7" + integrity sha512-hk5NyeGweJYTjes7lBW7gtG7iYoN6HLt6E4FQDrHPdwZjwNmvzaOH9N8zMTCxNFXUlg0bzeTOzWwMA717a+4eg== + dependencies: + "@ethersproject/abi" "^5.6.3" + web3-utils "1.10.1" + +web3-eth-accounts@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.1.tgz#24960e68c1ff3aaa57b96195c151f21bf9b9a0c0" + integrity sha512-x8zevaF4FYOIZqR3fdzdeKPf1Ek/O3HFptYH42IucYI5bK+o6ORebDuOOaIZqrF/c8ijcjGoo+cUDN9/5jU6Cw== + dependencies: + "@ethereumjs/common" "2.5.0" + "@ethereumjs/tx" "3.3.2" + "@ethereumjs/util" "^8.1.0" + eth-lib "0.2.8" + scrypt-js "^3.0.1" + uuid "^9.0.0" + web3-core "1.10.1" + web3-core-helpers "1.10.1" + web3-core-method "1.10.1" + web3-utils "1.10.1" + +web3-eth-contract@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.1.tgz#8f8a514db8a1c81337e67a60054840edfc8c26bb" + integrity sha512-eRZItYq8LzSPOKqgkTaT1rRruXTNkjbeIe9Cs+VFx3+p/GHyUI1Rj4rfBXp1MBR6p4WK+oy05sB+FNugOYxe8Q== + dependencies: + "@types/bn.js" "^5.1.1" + web3-core "1.10.1" + web3-core-helpers "1.10.1" + web3-core-method "1.10.1" + web3-core-promievent "1.10.1" + web3-core-subscriptions "1.10.1" + web3-eth-abi "1.10.1" + web3-utils "1.10.1" + +web3-eth-ens@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.1.tgz#3064b77f2de2c0f77136479f8b388a48e3fb1a85" + integrity sha512-WtcLhYTBeoKj+CbuyG3JQWcQynOXmv/l5CB27C3hJ42WWPa/XfUAsDmPbJp3YkqUbK3lE6iLT2yzwQIHfqmd0g== + dependencies: + content-hash "^2.5.2" + eth-ens-namehash "2.0.8" + web3-core "1.10.1" + web3-core-helpers "1.10.1" + web3-core-promievent "1.10.1" + web3-eth-abi "1.10.1" + web3-eth-contract "1.10.1" + web3-utils "1.10.1" + +web3-eth-iban@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.10.1.tgz#8a40f16218cd774e965b72dc2ca36449f8fdb072" + integrity sha512-3n1ibzYIza9ac/iB/wEnzvnmut/u6g/x6WitxxdEMVUZshGqqnBv6HDVx25iO9TxWmala+GgmRKHnEMKCh74Yg== + dependencies: + bn.js "^5.2.1" + web3-utils "1.10.1" + +web3-eth-personal@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.10.1.tgz#ebeb86d4f812ef0a562129144b0ab10f0d07a0e7" + integrity sha512-Th4AEMbxUhH+GEqYpluWYBb+PszZ9GsdmsOhN8fo4aQHSKMfvyP+scqgOMqxK3rvobpSy/EZ6zdbAkinhoi55g== + dependencies: + "@types/node" "^12.12.6" + web3-core "1.10.1" + web3-core-helpers "1.10.1" + web3-core-method "1.10.1" + web3-net "1.10.1" + web3-utils "1.10.1" + +web3-eth@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.10.1.tgz#6a8b46e95e0df58afc06bb052aadca89448e5af6" + integrity sha512-EV/d/TFVZcB54wpx2ndFnApla+aztsBOpZkbDreHcETLN1v6XmXyKozo0gYoQMZElKZ6QRRPEFvDjPeXdA7DBw== + dependencies: + web3-core "1.10.1" + web3-core-helpers "1.10.1" + web3-core-method "1.10.1" + web3-core-subscriptions "1.10.1" + web3-eth-abi "1.10.1" + web3-eth-accounts "1.10.1" + web3-eth-contract "1.10.1" + web3-eth-ens "1.10.1" + web3-eth-iban "1.10.1" + web3-eth-personal "1.10.1" + web3-net "1.10.1" + web3-utils "1.10.1" + +web3-net@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.10.1.tgz#66c6a24c4b9b0bbd9603cc58decda346a8222365" + integrity sha512-06VgKyabOvj0mE7LkT1lY2A17sP32jpMAh2TniZ8ZgC3Dq36+C5LtrY17LgLSaModpvCPbpzPgbTlqB0xhssew== + dependencies: + web3-core "1.10.1" + web3-core-method "1.10.1" + web3-utils "1.10.1" + +web3-providers-http@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.10.1.tgz#cc35007edf1483b971652414c1d4d1820fbcd5c7" + integrity sha512-haHlG4Ig8VQdx+HdnJgJPpJwLWkAE1aXcacOfaGd2hnXPqVYRocwYqgZD/Q9pUq3u4rIZezhUaFXNRByzAfMsw== + dependencies: + abortcontroller-polyfill "^1.7.3" + cross-fetch "^3.1.4" + es6-promise "^4.2.8" + web3-core-helpers "1.10.1" + +web3-providers-ipc@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.10.1.tgz#8d1126745da4e273034bb79aae483612768cc612" + integrity sha512-eYrLoC2OEOlxHdsWjKpw3gwKQuQG6rcd3lc41S6cC6UpkR2pszkXUTpXVKTKFFT3eWgVAYIVz/lCeilbYLgw5A== + dependencies: + oboe "2.1.5" + web3-core-helpers "1.10.1" + +web3-providers-ws@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.10.1.tgz#b079f0a030ddc33b64a0692e7054fcfefe9fae43" + integrity sha512-ZCHGVH4YTVA5MCaOgmV0UJya7jTh4Vd0CFWiGqruha9/xF0fBZRYMm0awYcI9eDvVP0hRU/C9CeH5tj7UQBnTw== + dependencies: + eventemitter3 "4.0.4" + web3-core-helpers "1.10.1" + websocket "^1.0.32" + +web3-shh@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.10.1.tgz#ded94bf40bef752f52f0380f62386dd5ea00f0af" + integrity sha512-PoRfyM5NtHiQufxWDEgLhxpeDkkZos/ijjiT1IQafmD0iurMBxLU+k9OjRX2oblVyP3nPl1sSBQTYFe3b33JGA== + dependencies: + web3-core "1.10.1" + web3-core-method "1.10.1" + web3-core-subscriptions "1.10.1" + web3-net "1.10.1" + +web3-utils@1.10.1, web3-utils@^1.8.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.1.tgz#97532130d85358628bc0ff14d94b7e9449786983" + integrity sha512-r6iUUw/uMnNcWXjhRv33Nyrhxq3VGOPBXeSzxhOXIci4SvC/LPTpROY0uTrMX7ztKyODYrHp8WhTkEf+ZnHssw== + 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" + web3-utils@^1.3.6: version "1.8.0" resolved "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.0.tgz" @@ -5579,11 +7121,36 @@ web3-utils@^1.3.6: randombytes "^2.1.0" utf8 "3.0.0" +web3@^1.8.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.1.tgz#6435783cb2c0a8347f62b7b1a6ade431f19dce2a" + integrity sha512-Ry+teufg6GYwIlLijyVTzZmnP+pu55vBU6P7rwK/rZidsMhc3m1lA5UXxiUVzBYZ8dvzV6+dVvOh68RrwrsI1w== + dependencies: + web3-bzz "1.10.1" + web3-core "1.10.1" + web3-eth "1.10.1" + web3-eth-personal "1.10.1" + web3-net "1.10.1" + web3-shh "1.10.1" + web3-utils "1.10.1" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +websocket@^1.0.32: + version "1.0.34" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" + integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + whatwg-encoding@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" @@ -5615,6 +7182,17 @@ which-module@^2.0.0: resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.11, which-typed-array@^1.1.2: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which@1.3.1, which@^1.1.1, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" @@ -5692,16 +7270,60 @@ ws@8.5.0: resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== +ws@^3.0.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" + integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== + dependencies: + async-limiter "~1.0.0" + safe-buffer "~5.1.0" + ultron "~1.1.0" + ws@^7.4.6: version "7.5.9" resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +xhr-request-promise@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz#2d5f4b16d8c6c893be97f1a62b0ed4cf3ca5f96c" + integrity sha512-YUBytBsuwgitWtdRzXDDkWAXzhdGB8bYm0sSzMPZT7Z2MBjMSTHFsyCT1yCRATY+XC69DUrQraRAEgcoCRaIPg== + dependencies: + xhr-request "^1.1.0" + +xhr-request@^1.0.1, xhr-request@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/xhr-request/-/xhr-request-1.1.0.tgz#f4a7c1868b9f198723444d82dcae317643f2e2ed" + integrity sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA== + dependencies: + buffer-to-arraybuffer "^0.0.5" + object-assign "^4.1.1" + query-string "^5.0.1" + simple-get "^2.7.0" + timed-out "^4.0.1" + url-set-query "^1.0.0" + xhr "^2.0.4" + +xhr@^2.0.4, xhr@^2.3.3: + version "2.6.0" + resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" + integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA== + dependencies: + global "~4.4.0" + is-function "^1.0.1" + parse-headers "^2.0.0" + xtend "^4.0.0" + xmlhttprequest@1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz" integrity sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA== +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" @@ -5712,7 +7334,12 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^3.0.2: +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==