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());
+ }
+}