diff --git a/script/input/11155111/llamaInstanceConfig.json b/script/input/11155111/llamaInstanceConfig.json new file mode 100644 index 000000000..250239fc5 --- /dev/null +++ b/script/input/11155111/llamaInstanceConfig.json @@ -0,0 +1,77 @@ +{ + "comment": "The factory address is from https://gist.github.com/mds1/da90e8a4db922fd684dac88bb4c08f92#attempt-3. The strategy logic is the `LlamaRelativeQuantityQuorum` contract.", + "factory": "0xAFF71a204beD7342d21827a607496f5f1806777F", + "instanceName": "Llama", + "instanceColor": "#6A45EC", + "instanceLogo": "", + "strategyLogic": "0xdF0a44747120C1BE2B0b4bDC4B9759218dFA6379", + "accountLogic": "0x2b0C5DDD817cE1F3dACC0CA7613E2DF038d924C4", + "initialStrategies": [ + { + "approvalPeriod": 172800, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 691200, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 5000, + "minDisapprovalPct": 10100, + "queuingPeriod": 0 + }, + { + "approvalPeriod": 0, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 86400, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 0, + "minDisapprovalPct": 1000, + "queuingPeriod": 691200 + }, + { + "approvalPeriod": 0, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 100, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 0, + "minDisapprovalPct": 0, + "queuingPeriod": 0 + } + ], + "initialAccounts": [ + { + "name": "Treasury" + } + ], + "initialRoleDescriptions": ["Core Team"], + "initialRoleHolders": [ + { + "comment": "This assigns role #1 llamaAlice.", + "expiration": 18446744073709551615, + "policyholder": "0xCF468ee5E8eCfaCE78851da762E850Ed0Fc7E7A0", + "quantity": 1, + "role": 1 + }, + { + "comment": "This assigns role #1 llamaBob.", + "expiration": 18446744073709551615, + "policyholder": "0x9E2962bb94b767Dea9e52b5D73b71394d373CC60", + "quantity": 1, + "role": 1 + }, + { + "comment": "This assigns role #1 llamaCharlie.", + "expiration": 18446744073709551615, + "policyholder": "0x85Ff2f6d6D05AA7F94A0c69F2cEecEd6F21C5E6B", + "quantity": 1, + "role": 1 + } + ], + "initialRolePermissions": [] +} diff --git a/script/input/31337/llamaInstanceConfig.json b/script/input/31337/llamaInstanceConfig.json new file mode 100644 index 000000000..a8ba86e66 --- /dev/null +++ b/script/input/31337/llamaInstanceConfig.json @@ -0,0 +1,91 @@ +{ + "comment": "These addresses are what you get when you run the DeployLlamaFactory script test. The strategy logic is the LlamaRelativeQuantityQuorum contract.", + "factory": "0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809", + "instanceName": "Llama", + "instanceColor": "#6A45EC", + "instanceLogo": "", + "strategyLogic": "0x416C42991d05b31E9A6dC209e91AD22b79D87Ae6", + "accountLogic": "0xDB8cFf278adCCF9E9b5da745B44E754fC4EE3C76", + "initialStrategies": [ + { + "approvalPeriod": 172800, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 691200, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 5000, + "minDisapprovalPct": 10100, + "queuingPeriod": 0 + }, + { + "approvalPeriod": 0, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 86400, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 0, + "minDisapprovalPct": 1000, + "queuingPeriod": 691200 + }, + { + "approvalPeriod": 0, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 100, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 0, + "minDisapprovalPct": 10100, + "queuingPeriod": 0 + } + ], + "initialAccounts": [ + { + "name": "Treasury" + } + ], + "initialRoleDescriptions": ["Core Team"], + "initialRoleHolders": [ + { + "comment": "This assigns role #1 llamaAlice.", + "expiration": 18446744073709551615, + "policyholder": "0x412e8b18F7263B1cAFF489f3ccC873424E438101", + "quantity": 1, + "role": 1 + }, + { + "comment": "This assigns role #1 llamaBob.", + "expiration": 18446744073709551615, + "policyholder": "0xDF01c61Fbf37055Fb4ff895393b27857f412236F", + "quantity": 1, + "role": 1 + }, + { + "comment": "This assigns role #1 llamaCharlie.", + "expiration": 18446744073709551615, + "policyholder": "0xc484b0CB07a9585d17a047F9AE068c34F5A5686f", + "quantity": 1, + "role": 1 + }, + { + "comment": "This assigns role #1 llamaDale.", + "expiration": 18446744073709551615, + "policyholder": "0x134b822179CcC24b89Fe82fDd8F914a2b141f935", + "quantity": 1, + "role": 1 + }, + { + "comment": "This assigns role #1 llamaErica.", + "expiration": 18446744073709551615, + "policyholder": "0x4664246b589528C7d931b90988419F581943B20C", + "quantity": 1, + "role": 1 + } + ], + "initialRolePermissions": [] +} diff --git a/script/input/31337/mockProtocolInstanceConfig.json b/script/input/31337/mockProtocolInstanceConfig.json new file mode 100644 index 000000000..25a9033aa --- /dev/null +++ b/script/input/31337/mockProtocolInstanceConfig.json @@ -0,0 +1,89 @@ +{ + "comment": "These addresses and IDs are what you get when you run the DeployLlama script test. The initialRoleDescriptions are expected to match those in the DeployLlama script.", + "factory": "0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809", + "instanceName": "Mock Protocol", + "instanceColor": "#000000", + "instanceLogo": "", + "strategyLogic": "0x416C42991d05b31E9A6dC209e91AD22b79D87Ae6", + "accountLogic": "0xDB8cFf278adCCF9E9b5da745B44E754fC4EE3C76", + "initialStrategies": [ + { + "approvalPeriod": 172800, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 691200, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 7500, + "minDisapprovalPct": 10100, + "queuingPeriod": 0 + }, + { + "approvalPeriod": 0, + "approvalRole": 1, + "disapprovalRole": 1, + "expirationPeriod": 86400, + "forceApprovalRoles": [], + "forceDisapprovalRoles": [], + "isFixedLengthApprovalPeriod": false, + "minApprovalPct": 0, + "minDisapprovalPct": 1000, + "queuingPeriod": 691200 + } + ], + "initialAccounts": [ + { + "name": "MP Treasury" + }, + { + "name": "MP Grants" + } + ], + "initialRoleDescriptions": ["Core Team", "Governance Maintainer"], + "initialRoleHolders": [ + { + "comment": "This will assign role 1 to the address derived from `makeAddrAndKey('mockAlice')`. The role assignment is set to never expire (type(uint64).max) because this is the default. The quantity is likewise the default.", + "policyholder": "0xc1F59Bce24bc332e11f4CE8264bEfFA921b6F7B6", + "expiration": 18446744073709551615, + "quantity": 1, + "role": 1 + }, + { + "comment": "This will assign role 1 to the address derived from `makeAddrAndKey('mockBob')`. The role assignment is set to never expire (type(uint64).max) because this is the default. The quantity is likewise the default.", + "policyholder": "0xB3E5c14EfA79bc86BA6d5e24387Af93e3987b927", + "expiration": 18446744073709551615, + "quantity": 1, + "role": 1 + }, + { + "comment": "This will assign role 1 to the address derived from `makeAddrAndKey('mockCharlie')`. The role assignment is set to never expire (type(uint64).max) because this is the default. The quantity is likewise the default.", + "policyholder": "0xD3883689Cf8d4332eBc24c796E1032a8C28EA8c9", + "expiration": 18446744073709551615, + "quantity": 1, + "role": 1 + }, + { + "comment": "This will assign role 1 to the address derived from `makeAddrAndKey('mockDale')`. The role assignment is set to never expire (type(uint64).max) because this is the default. The quantity is likewise the default.", + "policyholder": "0x500DBc13A7f5FD80A264b3eF2a37bfb968AB309d", + "expiration": 18446744073709551615, + "quantity": 1, + "role": 1 + }, + { + "comment": "This will assign role 1 to the address derived from `makeAddrAndKey('mockErica')`. The role assignment is set to never expire (type(uint64).max) because this is the default. The quantity is likewise the default.", + "policyholder": "0xfe677EBFFd09a566E0E2245E57DbdE1469B2BB72", + "expiration": 18446744073709551615, + "quantity": 1, + "role": 1 + }, + { + "comment": "This will assign role 2 to llama's Llama instance's executor. The role assignment is set to never expire (type(uint64).max) because this is the default. The quantity is likewise the default.", + "policyholder": "0xC264c377642b946A57160d09FE5e35db06cbb526", + "expiration": 18446744073709551615, + "quantity": 1, + "role": 2 + } + ], + "initialRolePermissions": [] +} diff --git a/test/LlamaCore.t.sol b/test/LlamaCore.t.sol index 212bf3e80..86ceefe34 100644 --- a/test/LlamaCore.t.sol +++ b/test/LlamaCore.t.sol @@ -5,6 +5,7 @@ import {Test, console2, StdStorage, stdStorage} from "forge-std/Test.sol"; import {MockAccountLogicContract} from "test/mock/MockAccountLogicContract.sol"; import {MockActionGuard} from "test/mock/MockActionGuard.sol"; +import {MockAtomicActionExecutor} from "test/mock/MockAtomicActionExecutor.sol"; import {MockPoorlyImplementedAbsolutePeerReview} from "test/mock/MockPoorlyImplementedStrategy.sol"; import {MockProtocol} from "test/mock/MockProtocol.sol"; import {LlamaCoreSigUtils} from "test/utils/LlamaCoreSigUtils.sol"; @@ -859,6 +860,25 @@ contract CreateActionBySig is LlamaCoreTest { (v, r, s) = vm.sign(privateKey, digest); } + function createOffchainSignatureForInstantExecution(uint256 privateKey, ILlamaStrategy strategy) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + LlamaCoreSigUtils.CreateAction memory createAction = LlamaCoreSigUtils.CreateAction({ + role: uint8(Roles.ActionCreator), + strategy: address(strategy), + target: address(mockProtocol), + value: 0, + data: abi.encodeCall(MockProtocol.pause, (true)), + description: "", + policyholder: actionCreatorAaron, + nonce: 0 + }); + bytes32 digest = getCreateActionTypedDataHash(createAction); + (v, r, s) = vm.sign(privateKey, digest); + } + function createActionBySig(uint8 v, bytes32 r, bytes32 s) internal returns (uint256 actionId) { actionId = mpCore.createActionBySig( actionCreatorAaron, @@ -985,6 +1005,57 @@ contract CreateActionBySig is LlamaCoreTest { vm.expectRevert(LlamaCore.InvalidSignature.selector); createActionBySig(v, r, s); } + + function test_ActionCanBeCreatedQueuedAndExecutedInOneBlock() public { + // Create the instant execution strategy and assign the permission to `Roles.ActionCreator` + LlamaRelativeStrategyBase.Config[] memory newStrategies = new LlamaRelativeStrategyBase.Config[](1); + newStrategies[0] = LlamaRelativeStrategyBase.Config({ + approvalPeriod: 0, + queuingPeriod: 0, + expirationPeriod: 2 days, + isFixedLengthApprovalPeriod: false, + minApprovalPct: 0, + minDisapprovalPct: 10_001, + approvalRole: uint8(Roles.Approver), + disapprovalRole: uint8(Roles.Disapprover), + forceApprovalRoles: new uint8[](0), + forceDisapprovalRoles: new uint8[](0) + }); + ILlamaStrategy instantExecutionStrategy = lens.computeLlamaStrategyAddress( + address(relativeQuantityQuorumLogic), DeployUtils.encodeStrategy(newStrategies[0]), address(mpCore) + ); + PermissionData memory newPermissionData = + PermissionData(address(mockProtocol), PAUSE_SELECTOR, instantExecutionStrategy); + bytes memory data = abi.encodeCall(MockProtocol.pause, (true)); + + vm.startPrank(address(mpExecutor)); + mpCore.setStrategyLogicAuthorization(relativeQuantityQuorumLogic, true); + mpCore.createStrategies(relativeQuantityQuorumLogic, DeployUtils.encodeStrategyConfigs(newStrategies)); + mpPolicy.setRolePermission(uint8(Roles.ActionCreator), newPermissionData, true); + vm.stopPrank(); + + (uint8 v, bytes32 r, bytes32 s) = + createOffchainSignatureForInstantExecution(actionCreatorAaronPrivateKey, instantExecutionStrategy); + + MockAtomicActionExecutor mockAtomicActionExecutor = new MockAtomicActionExecutor(mpCore); + + mineBlock(); + + vm.expectEmit(); + emit ActionExecuted(0, address(mockAtomicActionExecutor), instantExecutionStrategy, actionCreatorAaron, bytes("")); + mockAtomicActionExecutor.createQueueAndExecute( + actionCreatorAaron, + uint8(Roles.ActionCreator), + instantExecutionStrategy, + address(mockProtocol), + 0, + data, + "", + v, + r, + s + ); + } } contract CancelAction is LlamaCoreTest { @@ -2633,6 +2704,52 @@ contract CreateStrategies is LlamaCoreTest { vm.expectRevert(); mpCore.createStrategies(ILlamaStrategy(address(0)), DeployUtils.encodeStrategyConfigs(newStrategies)); } + + function test_ActionCanBeCreatedQueuedAndExecutedInOneBlock() public { + LlamaRelativeStrategyBase.Config[] memory newStrategies = new LlamaRelativeStrategyBase.Config[](1); + + newStrategies[0] = LlamaRelativeStrategyBase.Config({ + approvalPeriod: 0, + queuingPeriod: 0, + expirationPeriod: 2 days, + isFixedLengthApprovalPeriod: false, + minApprovalPct: 0, + minDisapprovalPct: 10_001, + approvalRole: uint8(Roles.Approver), + disapprovalRole: uint8(Roles.Disapprover), + forceApprovalRoles: new uint8[](0), + forceDisapprovalRoles: new uint8[](0) + }); + + ILlamaStrategy strategyAddress = lens.computeLlamaStrategyAddress( + address(relativeQuantityQuorumLogic), DeployUtils.encodeStrategy(newStrategies[0]), address(mpCore) + ); + PermissionData memory newPermissionData = PermissionData(address(mockProtocol), PAUSE_SELECTOR, strategyAddress); + bytes memory data = abi.encodeCall(MockProtocol.pause, (true)); + + vm.startPrank(address(mpExecutor)); + mpCore.setStrategyLogicAuthorization(relativeQuantityQuorumLogic, true); + mpCore.createStrategies(relativeQuantityQuorumLogic, DeployUtils.encodeStrategyConfigs(newStrategies)); + mpPolicy.setRolePermission(uint8(Roles.ActionCreator), newPermissionData, true); + vm.stopPrank(); + + mineBlock(); + + uint256 preExecutionTimestamp = block.timestamp; + + vm.prank(actionCreatorAaron); + uint256 actionId = + mpCore.createAction(uint8(Roles.ActionCreator), strategyAddress, address(mockProtocol), 0, data, ""); + + ActionInfo memory actionInfo = ActionInfo( + actionId, actionCreatorAaron, uint8(Roles.ActionCreator), strategyAddress, address(mockProtocol), 0, data + ); + mpCore.queueAction(actionInfo); + mpCore.executeAction(actionInfo); + + uint256 postExecutionTimestamp = block.timestamp; + assertEq(postExecutionTimestamp, preExecutionTimestamp); + } } contract SetStrategyAuthorization is LlamaCoreTest { diff --git a/test/integrations/MultipleInstance.integrations.t.sol b/test/integrations/MultipleInstance.integrations.t.sol new file mode 100644 index 000000000..147a3a2d4 --- /dev/null +++ b/test/integrations/MultipleInstance.integrations.t.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; + +import {Vm} from "forge-std/Vm.sol"; + +import {MockInstanceUpdateScript} from "test/mock/MockInstanceUpdateScript.sol"; +import {MockInstanceUpdateVersion1} from "test/mock/MockInstanceUpdateVersion1.sol"; + +import {DeployLlamaFactory} from "script/DeployLlamaFactory.s.sol"; +import {DeployLlamaInstance} from "script/DeployLlamaInstance.s.sol"; + +import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; +import {LlamaCore} from "src/LlamaCore.sol"; +import {LlamaExecutor} from "src/LlamaExecutor.sol"; +import {LlamaPolicy} from "src/LlamaPolicy.sol"; +import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; + +contract MultipleInstanceTestSetup is DeployLlamaFactory, DeployLlamaInstance, Test { + event ApprovalCast(uint256 id, address indexed policyholder, uint8 indexed role, uint256 quantity, string reason); + event ActionCreated( + uint256 id, + address indexed creator, + uint8 role, + ILlamaStrategy indexed strategy, + address indexed target, + uint256 value, + bytes data, + string description + ); + + address LLAMA_INSTANCE_DEPLOYER = 0x3d9fEa8AeD0249990133132Bb4BC8d07C6a8259a; + + uint8 CORE_TEAM_ROLE = uint8(1); + uint8 GOVERNANCE_MAINTAINER_ROLE = uint8(2); + + address llamaAlice; + uint256 llamaAlicePrivateKey; + address llamaBob; + uint256 llamaBobPrivateKey; + address llamaCharlie; + uint256 llamaCharliePrivateKey; + address llamaDale; + uint256 llamaDalePrivateKey; + address llamaErica; + uint256 llamaEricaPrivateKey; + + address mockAlice; + uint256 mockAlicePrivateKey; + address mockBob; + uint256 mockBobPrivateKey; + address mockCharlie; + uint256 mockCharliePrivateKey; + address mockDale; + uint256 mockDalePrivateKey; + address mockErica; + uint256 mockEricaPrivateKey; + + LlamaCore llamaInstanceCore; + LlamaPolicy llamaInstancePolicy; + LlamaExecutor llamaInstanceExecutor; + + LlamaCore mockCore; + LlamaPolicy mockPolicy; + LlamaExecutor mockExecutor; + + ILlamaStrategy MOCK_VOTING_STRATEGY = ILlamaStrategy(0x225D6692B4DD673C6ad57B4800846341d027BC66); + ILlamaStrategy MOCK_OPTIMISTIC_STRATEGY = ILlamaStrategy(0xF7E4BB5159c3fdc50e1Ef6b80BD69988DD6f438d); + ILlamaStrategy LLAMA_VOTING_STRATEGY = ILlamaStrategy(0x881E25C4470136B1B2D64a4942b5346e41477fB6); + + MockInstanceUpdateScript mockInstanceUpdateScript; + MockInstanceUpdateVersion1 mockInstanceUpdateVersion1; + + function mineBlock() internal { + vm.roll(block.number + 1); + vm.warp(block.timestamp + 1); + } + + function setUp() public virtual { + // Setting up user addresses and private keys for Llama. + (llamaAlice, llamaAlicePrivateKey) = makeAddrAndKey("llamaAlice"); + (llamaBob, llamaBobPrivateKey) = makeAddrAndKey("llamaBob"); + (llamaCharlie, llamaCharliePrivateKey) = makeAddrAndKey("llamaCharlie"); + (llamaDale, llamaDalePrivateKey) = makeAddrAndKey("llamaDale"); + (llamaErica, llamaEricaPrivateKey) = makeAddrAndKey("llamaErica"); + + // Setting up user addresses and private keys for Mock. + (mockAlice, mockAlicePrivateKey) = makeAddrAndKey("mockAlice"); + (mockBob, mockBobPrivateKey) = makeAddrAndKey("mockBob"); + (mockCharlie, mockCharliePrivateKey) = makeAddrAndKey("mockCharlie"); + (mockDale, mockDalePrivateKey) = makeAddrAndKey("mockDale"); + (mockErica, mockEricaPrivateKey) = makeAddrAndKey("mockErica"); + + // Deploy the factory + DeployLlamaFactory.run(); + + // Deploy llama's Llama instance + DeployLlamaInstance.run(LLAMA_INSTANCE_DEPLOYER, "llamaInstanceConfig.json"); + llamaInstanceCore = core; + llamaInstancePolicy = llamaInstanceCore.policy(); + llamaInstanceExecutor = llamaInstanceCore.executor(); + + // Deploy mock protocol's Llama instance + DeployLlamaInstance.run(LLAMA_INSTANCE_DEPLOYER, "mockProtocolInstanceConfig.json"); + mockCore = core; + mockPolicy = mockCore.policy(); + mockExecutor = mockCore.executor(); + + mineBlock(); + + mockInstanceUpdateScript = new MockInstanceUpdateScript(); + + // In practice this can either happen as an initial action post deployment or we can normalize a post deployment + // configuration flow. + // This would work by deploying with an instant execution strategy and role holder which is an address under our + // control. This address would use its root authority to setup the instance and then remove itself from the system. + // The user could confirm that none of these root permissions are still active before transferring ownership. + vm.startPrank(address(mockExecutor)); + mockCore.setScriptAuthorization(address(mockInstanceUpdateScript), true); + mockPolicy.setRolePermission( + GOVERNANCE_MAINTAINER_ROLE, + PermissionData( + address(mockInstanceUpdateScript), + MockInstanceUpdateScript.authorizeScriptAndSetPermission.selector, + MOCK_VOTING_STRATEGY + ), + true + ); + mockPolicy.setRolePermission( + GOVERNANCE_MAINTAINER_ROLE, + PermissionData( + address(mockInstanceUpdateScript), + MockInstanceUpdateScript.authorizeScriptAndSetPermission.selector, + MOCK_OPTIMISTIC_STRATEGY + ), + true + ); + vm.stopPrank(); + + // Deploy the version 1 update script + mockInstanceUpdateVersion1 = new MockInstanceUpdateVersion1(); + + // Now that llama has permission to create actions for `mockInstanceUpdateScript`, it needs a permission in its own + // instance for calling createAction. + vm.prank(address(llamaInstanceExecutor)); + llamaInstancePolicy.setRolePermission( + uint8(1), PermissionData(address(mockCore), LlamaCore.createAction.selector, LLAMA_VOTING_STRATEGY), true + ); + } + + function _approveAction(LlamaCore _core, address _policyholder, ActionInfo memory actionInfo) public { + vm.expectEmit(); + emit ApprovalCast(actionInfo.id, _policyholder, uint8(1), 1, ""); + vm.prank(_policyholder); + _core.castApproval(uint8(1), actionInfo, ""); + } + + function createActionToAuthorizeScriptAndSetPermission(ILlamaStrategy strategyForMockInstance) + public + returns (ActionInfo memory) + { + PermissionData memory permissionData = PermissionData( + address(mockInstanceUpdateVersion1), MockInstanceUpdateVersion1.updateInstance.selector, strategyForMockInstance + ); + bytes memory actionData = abi.encodeCall(MockInstanceUpdateScript.authorizeScriptAndSetPermission, (permissionData)); + bytes memory data = abi.encodeCall( + LlamaCore.createAction, + (GOVERNANCE_MAINTAINER_ROLE, strategyForMockInstance, address(mockInstanceUpdateScript), 0, actionData, "") + ); + vm.prank(llamaAlice); + uint256 actionId = llamaInstanceCore.createAction(uint8(1), LLAMA_VOTING_STRATEGY, address(mockCore), 0, data, ""); + ActionInfo memory actionInfo = + ActionInfo(actionId, llamaAlice, uint8(1), LLAMA_VOTING_STRATEGY, address(mockCore), 0, data); + + mineBlock(); + + _approveAction(llamaInstanceCore, llamaBob, actionInfo); + _approveAction(llamaInstanceCore, llamaCharlie, actionInfo); + _approveAction(llamaInstanceCore, llamaDale, actionInfo); + + // Executing llama's action creates an action for the mock instance + vm.expectEmit(); + emit ActionCreated( + 0, + address(llamaInstanceExecutor), + GOVERNANCE_MAINTAINER_ROLE, + strategyForMockInstance, + address(mockInstanceUpdateScript), + 0, + actionData, + "" + ); + llamaInstanceCore.executeAction(actionInfo); + + return ActionInfo( + 0, + address(llamaInstanceExecutor), + GOVERNANCE_MAINTAINER_ROLE, + strategyForMockInstance, + address(mockInstanceUpdateScript), + 0, + actionData + ); + } +} + +contract MultipleInstanceTest is MultipleInstanceTestSetup { + function test_instanceCanDelegateUpdateRoleToOtherInstance() external { + // Action is created for mock instance to call `MockInstanceUpdateScript` + ActionInfo memory actionInfo = createActionToAuthorizeScriptAndSetPermission(MOCK_VOTING_STRATEGY); + + mineBlock(); + + _approveAction(mockCore, mockBob, actionInfo); + _approveAction(mockCore, mockCharlie, actionInfo); + _approveAction(mockCore, mockDale, actionInfo); + _approveAction(mockCore, mockErica, actionInfo); + + // Script is authorized and llama has permission to create an action for it. + mockCore.executeAction(actionInfo); + + PermissionData memory permissionData = PermissionData( + address(mockInstanceUpdateVersion1), MockInstanceUpdateVersion1.updateInstance.selector, MOCK_VOTING_STRATEGY + ); + bytes memory actionData = abi.encodeCall(MockInstanceUpdateVersion1.updateInstance, (permissionData)); + bytes memory data = abi.encodeCall( + LlamaCore.createAction, + (GOVERNANCE_MAINTAINER_ROLE, MOCK_VOTING_STRATEGY, address(mockInstanceUpdateVersion1), 0, actionData, "") + ); + vm.prank(llamaAlice); + uint256 actionId = llamaInstanceCore.createAction(uint8(1), LLAMA_VOTING_STRATEGY, address(mockCore), 0, data, ""); + actionInfo = ActionInfo(actionId, llamaAlice, uint8(1), LLAMA_VOTING_STRATEGY, address(mockCore), 0, data); + + mineBlock(); + + _approveAction(llamaInstanceCore, llamaBob, actionInfo); + _approveAction(llamaInstanceCore, llamaCharlie, actionInfo); + _approveAction(llamaInstanceCore, llamaDale, actionInfo); + + // Executing llama's action creates an action for the mock instance to call the update script. + vm.expectEmit(); + emit ActionCreated( + actionId, + address(llamaInstanceExecutor), + GOVERNANCE_MAINTAINER_ROLE, + MOCK_VOTING_STRATEGY, + address(mockInstanceUpdateVersion1), + 0, + actionData, + "" + ); + llamaInstanceCore.executeAction(actionInfo); + + actionInfo = ActionInfo( + actionId, + address(llamaInstanceExecutor), + GOVERNANCE_MAINTAINER_ROLE, + MOCK_VOTING_STRATEGY, + address(mockInstanceUpdateVersion1), + 0, + actionData + ); + + mineBlock(); + + _approveAction(mockCore, mockBob, actionInfo); + _approveAction(mockCore, mockCharlie, actionInfo); + _approveAction(mockCore, mockDale, actionInfo); + _approveAction(mockCore, mockErica, actionInfo); + + // Script is executed, unauthorized, and the permission is removed. + mockCore.executeAction(actionInfo); + bytes32 votingPermission = keccak256( + abi.encode( + PermissionData( + address(mockInstanceUpdateScript), + MockInstanceUpdateScript.authorizeScriptAndSetPermission.selector, + MOCK_VOTING_STRATEGY + ) + ) + ); + + bytes32 optimisticPermission = keccak256( + abi.encode( + PermissionData( + address(mockInstanceUpdateScript), + MockInstanceUpdateScript.authorizeScriptAndSetPermission.selector, + MOCK_OPTIMISTIC_STRATEGY + ) + ) + ); + + bytes32 upgradeScriptPermission = keccak256( + abi.encode( + PermissionData( + address(mockInstanceUpdateVersion1), MockInstanceUpdateVersion1.updateInstance.selector, MOCK_VOTING_STRATEGY + ) + ) + ); + + assertTrue(mockPolicy.hasPermissionId(address(llamaInstanceExecutor), GOVERNANCE_MAINTAINER_ROLE, votingPermission)); + assertTrue( + mockPolicy.hasPermissionId(address(llamaInstanceExecutor), GOVERNANCE_MAINTAINER_ROLE, optimisticPermission) + ); + assertTrue(mockCore.authorizedScripts(address(mockInstanceUpdateScript))); + + // Assert that upgrade script was executed + assertTrue(mockCore.authorizedStrategyLogics(ILlamaStrategy(0xBb2180ebd78ce97360503434eD37fcf4a1Df61c3))); + assertTrue(mockCore.authorizedStrategyLogics(ILlamaStrategy(0xd21060559c9beb54fC07aFd6151aDf6cFCDDCAeB))); + + // Assert that upgrade script unauthorized itself and removed the permission + assertFalse(mockCore.authorizedScripts(address(mockInstanceUpdateVersion1))); + assertFalse( + mockPolicy.hasPermissionId(address(llamaInstanceExecutor), GOVERNANCE_MAINTAINER_ROLE, upgradeScriptPermission) + ); + } +} diff --git a/test/mock/MockAtomicActionExecutor.sol b/test/mock/MockAtomicActionExecutor.sol new file mode 100644 index 000000000..9a78bf3a1 --- /dev/null +++ b/test/mock/MockAtomicActionExecutor.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; +import {ActionInfo} from "src/lib/Structs.sol"; +import {LlamaCore} from "src/LlamaCore.sol"; + +/// @dev A mock contract that can create, queue, and execute actions in a single function. +contract MockAtomicActionExecutor { + LlamaCore immutable CORE; + + constructor(LlamaCore _core) { + CORE = _core; + } + + function createQueueAndExecute( + address policyholder, + uint8 role, + ILlamaStrategy strategy, + address target, + uint256 value, + bytes calldata data, + string memory description, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 actionId) { + actionId = CORE.createActionBySig(policyholder, role, strategy, target, value, data, description, v, r, s); + + ActionInfo memory actionInfo = ActionInfo(actionId, policyholder, role, strategy, target, value, data); + CORE.queueAction(actionInfo); + CORE.executeAction(actionInfo); + } +} diff --git a/test/mock/MockInstanceUpdateScript.sol b/test/mock/MockInstanceUpdateScript.sol new file mode 100644 index 000000000..1be740c15 --- /dev/null +++ b/test/mock/MockInstanceUpdateScript.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {LlamaBaseScript} from "src/llama-scripts/LlamaBaseScript.sol"; +import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; +import {LlamaCore} from "src/LlamaCore.sol"; +import {LlamaExecutor} from "src/LlamaExecutor.sol"; +import {LlamaPolicy} from "src/LlamaPolicy.sol"; +import {PermissionData} from "src/lib/Structs.sol"; + +/// @dev This is a mock script because it hasn't been audited yet. +contract MockInstanceUpdateScript is LlamaBaseScript { + // ======================== + // ======== Errors ======== + // ======================== + + error InvalidStrategy(); + + function authorizeScriptAndSetPermission(PermissionData memory permissionData) external onlyDelegateCall { + uint8 GOVERNANCE_MAINTAINER_ROLE = 2; + ILlamaStrategy VOTING_STRATEGY = ILlamaStrategy(0x225D6692B4DD673C6ad57B4800846341d027BC66); + ILlamaStrategy OPTIMISTIC_STRATEGY = ILlamaStrategy(0xF7E4BB5159c3fdc50e1Ef6b80BD69988DD6f438d); + if (permissionData.strategy != OPTIMISTIC_STRATEGY && permissionData.strategy != VOTING_STRATEGY) { + revert InvalidStrategy(); + } + + (LlamaCore core, LlamaPolicy policy) = _context(); + core.setScriptAuthorization(permissionData.target, true); + policy.setRolePermission(GOVERNANCE_MAINTAINER_ROLE, permissionData, true); + } + + // ================================ + // ======== Internal Logic ======== + // ================================ + + /// @dev Get the core and policy contracts. + function _context() internal view returns (LlamaCore core, LlamaPolicy policy) { + core = LlamaCore(LlamaExecutor(address(this)).LLAMA_CORE()); + policy = LlamaPolicy(core.policy()); + } +} diff --git a/test/mock/MockInstanceUpdateVersion1.sol b/test/mock/MockInstanceUpdateVersion1.sol new file mode 100644 index 000000000..e1453bfb8 --- /dev/null +++ b/test/mock/MockInstanceUpdateVersion1.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {LlamaBaseScript} from "src/llama-scripts/LlamaBaseScript.sol"; +import {ILlamaStrategy} from "src/interfaces/ILlamaStrategy.sol"; +import {LlamaCore} from "src/LlamaCore.sol"; +import {LlamaExecutor} from "src/LlamaExecutor.sol"; +import {LlamaPolicy} from "src/LlamaPolicy.sol"; +import {PermissionData} from "src/lib/Structs.sol"; + +/// @dev Upgrade the llama instance calling this script to version 1. +contract MockInstanceUpdateVersion1 is LlamaBaseScript { + function updateInstance(PermissionData memory permissionData) external onlyDelegateCall { + (LlamaCore core, LlamaPolicy policy) = _context(); + // Authorize `LlamaAbsolutePeerReview` + core.setStrategyLogicAuthorization(ILlamaStrategy(0xBb2180ebd78ce97360503434eD37fcf4a1Df61c3), true); + // Authorize `LlamaRelativeUniqueHolderQuorum` + core.setStrategyLogicAuthorization(ILlamaStrategy(0xd21060559c9beb54fC07aFd6151aDf6cFCDDCAeB), true); + + // Unauthorize script after completion and remove permission from governance maintainer role. + core.setScriptAuthorization(SELF, false); + policy.setRolePermission(uint8(2), permissionData, false); + } + + // ================================ + // ======== Internal Logic ======== + // ================================ + + /// @dev Get the core and policy contracts. + function _context() internal view returns (LlamaCore core, LlamaPolicy policy) { + core = LlamaCore(LlamaExecutor(address(this)).LLAMA_CORE()); + policy = LlamaPolicy(core.policy()); + } +}