diff --git a/script/DeployLlamaTokenVotingFactory.s.sol b/script/DeployLlamaTokenVotingFactory.s.sol index 3707714..66757fd 100644 --- a/script/DeployLlamaTokenVotingFactory.s.sol +++ b/script/DeployLlamaTokenVotingFactory.s.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.23; import {Script} from "forge-std/Script.sol"; import {DeployUtils} from "script/DeployUtils.sol"; +import {LlamaMessageBroadcaster} from "src/message-broadcaster/LlamaMessageBroadcaster.sol"; import {LlamaTokenGovernor} from "src/token-voting/LlamaTokenGovernor.sol"; import {LlamaTokenVotingFactory} from "src/token-voting/LlamaTokenVotingFactory.sol"; import {LlamaTokenAdapterVotesTimestamp} from "src/token-voting/token-adapters/LlamaTokenAdapterVotesTimestamp.sol"; @@ -16,6 +17,9 @@ contract DeployLlamaTokenVotingFactory is Script { // Factory contracts. LlamaTokenVotingFactory tokenVotingFactory; + // Llama Message Broadcaster. + LlamaMessageBroadcaster llamaMessageBroadcaster; + function run() public { DeployUtils.print( string.concat("Deploying Llama token voting factory and logic contracts to chain:", vm.toString(block.chainid)) @@ -34,5 +38,9 @@ contract DeployLlamaTokenVotingFactory is Script { DeployUtils.print( string.concat(" LlamaTokenAdapterVotesTimestamp: ", vm.toString(address(llamaTokenAdapterTimestampLogic))) ); + + vm.broadcast(); + llamaMessageBroadcaster = new LlamaMessageBroadcaster(); + DeployUtils.print(string.concat(" LlamaMessageBroadcaster: ", vm.toString(address(llamaMessageBroadcaster)))); } } diff --git a/src/message-broadcaster/LlamaMessageBroadcaster.sol b/src/message-broadcaster/LlamaMessageBroadcaster.sol new file mode 100644 index 0000000..a5df43f --- /dev/null +++ b/src/message-broadcaster/LlamaMessageBroadcaster.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ILlamaCore} from "src/interfaces/ILlamaCore.sol"; +import {ILlamaExecutor} from "src/interfaces/ILlamaExecutor.sol"; + +/// @title LlamaMessageBroadcaster +/// @author Llama (devsdosomething@llama.xyz) +/// @notice This contract enables Llama instances to broadcast an offchain message. +contract LlamaMessageBroadcaster { + /// @dev Emitted when a message is broadcast by a Llama instance. + event MessageBroadcasted(ILlamaExecutor indexed llamaExecutor, string message); + + /// @notice Broadcasts a message from a Llama instance. + /// @param message Message to be broadcasted. + function broadcastMessage(string calldata message) external { + ILlamaExecutor llamaExecutor = ILlamaExecutor(msg.sender); + // Duck testing to check if the caller is a Llama instance. + ILlamaCore(llamaExecutor.LLAMA_CORE()).actionsCount(); + emit MessageBroadcasted(llamaExecutor, message); + } +} diff --git a/test/message-broadcaster/LlamaMessageBroadcaster.t.sol b/test/message-broadcaster/LlamaMessageBroadcaster.t.sol new file mode 100644 index 0000000..27b33f7 --- /dev/null +++ b/test/message-broadcaster/LlamaMessageBroadcaster.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, console2} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {LlamaPeripheryTestSetup} from "test/LlamaPeripheryTestSetup.sol"; + +import {DeployLlamaTokenVotingFactory} from "script/DeployLlamaTokenVotingFactory.s.sol"; + +import {ILlamaExecutor} from "src/interfaces/ILlamaExecutor.sol"; +import {ILlamaPolicy} from "src/interfaces/ILlamaPolicy.sol"; +import {ActionInfo} from "src/lib/Structs.sol"; +import {LlamaMessageBroadcaster} from "src/message-broadcaster/LlamaMessageBroadcaster.sol"; + +contract LlamaMessageBroadcasterTest is LlamaPeripheryTestSetup, DeployLlamaTokenVotingFactory { + event MessageBroadcasted(ILlamaExecutor indexed llamaExecutor, string message); + + function setUp() public virtual override { + LlamaPeripheryTestSetup.setUp(); + + // Deploy the peripheral contracts + DeployLlamaTokenVotingFactory.run(); + } +} + +contract BroadcastMessage is LlamaMessageBroadcasterTest { + function test_BroadcastMessage() public { + string memory message = "Hello World!"; + vm.expectEmit(); + emit MessageBroadcasted(EXECUTOR, message); + vm.prank(address(EXECUTOR)); + llamaMessageBroadcaster.broadcastMessage(message); + } + + function test_BroadcastMessageFullActionLifecycle() public { + string memory message = "Hello World!"; + + // Giving Action Creator permission to call `LlamaMessageBroadcaster.broadcastMessage`. + vm.prank(address(EXECUTOR)); + POLICY.setRolePermission( + CORE_TEAM_ROLE, + ILlamaPolicy.PermissionData( + address(llamaMessageBroadcaster), LlamaMessageBroadcaster.broadcastMessage.selector, address(STRATEGY) + ), + true + ); + + // Create Action to broadcast message. + bytes memory data = abi.encodeCall(LlamaMessageBroadcaster.broadcastMessage, (message)); + vm.prank(coreTeam1); + uint256 actionId = CORE.createAction(CORE_TEAM_ROLE, STRATEGY, address(llamaMessageBroadcaster), 0, data, ""); + ActionInfo memory actionInfo = + ActionInfo(actionId, coreTeam1, CORE_TEAM_ROLE, STRATEGY, address(llamaMessageBroadcaster), 0, data); + + // Approval and auto-queue process. + vm.prank(coreTeam2); + CORE.castApproval(CORE_TEAM_ROLE, actionInfo, ""); + vm.prank(coreTeam3); + CORE.castApproval(CORE_TEAM_ROLE, actionInfo, ""); + vm.prank(coreTeam4); + CORE.castApproval(CORE_TEAM_ROLE, actionInfo, ""); + + // Execute Action. + vm.expectEmit(); + emit MessageBroadcasted(EXECUTOR, message); + CORE.executeAction(actionInfo); + } + + function test_RevertIf_CallerIsNotExecutor() public { + string memory message = "Hello World!"; + vm.expectRevert(); + llamaMessageBroadcaster.broadcastMessage(message); + } +}