diff --git a/script/DeployLlamaFactory.s.sol b/script/DeployLlamaFactory.s.sol index 0c293e9ea..dba99d473 100644 --- a/script/DeployLlamaFactory.s.sol +++ b/script/DeployLlamaFactory.s.sol @@ -5,6 +5,8 @@ import {Script, stdJson} from "forge-std/Script.sol"; import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; import {LlamaAccountWithDelegation} from "src/accounts/LlamaAccountWithDelegation.sol"; +import {LlamaAccountExecuteGuard} from "src/guards/LlamaAccountExecuteGuard.sol"; +import {LlamaActionGuardFactory} from "src/guards/LlamaActionGuardFactory.sol"; import {LlamaCore} from "src/LlamaCore.sol"; import {LlamaFactory} from "src/LlamaFactory.sol"; import {LlamaLens} from "src/LlamaLens.sol"; @@ -33,9 +35,11 @@ contract DeployLlamaFactory is Script { LlamaAccountWithDelegation accountWithDelegationLogic; LlamaPolicy policyLogic; LlamaPolicyMetadata policyMetadataLogic; + LlamaAccountExecuteGuard accountExecuteGuardLogic; // Factory and lens contracts. LlamaFactory factory; + LlamaActionGuardFactory actionGuardFactory; LlamaLens lens; // Llama scripts @@ -141,5 +145,17 @@ contract DeployLlamaFactory is Script { DeployUtils.print( string.concat(" LlamaAccountTokenDelegationScript:", vm.toString(address(accountTokenDelegationScript))) ); + + vm.broadcast(); + (success,) = msg.sender.call(""); + DeployUtils.print(string.concat(" Self call succeeded? ", vm.toString(success))); + + vm.broadcast(); + actionGuardFactory = new LlamaActionGuardFactory(); + DeployUtils.print(string.concat(" LlamaActionGuardFactory:", vm.toString(address(actionGuardFactory)))); + + vm.broadcast(); + accountExecuteGuardLogic = new LlamaAccountExecuteGuard(); + DeployUtils.print(string.concat(" LlamaAccountExecuteGuardLogic:", vm.toString(address(accountExecuteGuardLogic)))); } } diff --git a/src/guards/LlamaAccountExecuteGuard.sol b/src/guards/LlamaAccountExecuteGuard.sol new file mode 100644 index 000000000..fa73c2bd2 --- /dev/null +++ b/src/guards/LlamaAccountExecuteGuard.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; + +import {ILlamaActionGuardMinimalProxy} from "src/interfaces/ILlamaActionGuardMinimalProxy.sol"; +import {ILlamaActionGuard} from "src/interfaces/ILlamaActionGuard.sol"; +import {LlamaUtils} from "src/lib/LlamaUtils.sol"; +import {ActionInfo} from "src/lib/Structs.sol"; + +/// @title Llama Account Execute Guard +/// @author Llama (devsdosomething@llama.xyz) +/// @notice A guard that only allows authorized targets to be called from a Llama account's execute function. +/// @dev This guard should be used to protect the `execute` function in the `LlamaAccount` contract +contract LlamaAccountExecuteGuard is ILlamaActionGuardMinimalProxy, Initializable { + // ========================= + // ======== Structs ======== + // ========================= + + /// @dev Authorized target configuration. + struct AuthorizedTargetConfig { + address target; // The target contract. + bool withDelegatecall; // Call type. + bool isAuthorized; // Is the target authorized. + } + + /// @dev Llama account execute guard initialization configuration. + struct Config { + AuthorizedTargetConfig[] authorizedTargets; // The authorized targets and their call type. + } + + // ========================= + // ======== Errors ======== + // ========================= + + /// @dev Only callable by a Llama instance's executor. + error OnlyLlama(); + + /// @dev Thrown if the target with call type is not authorized. + error UnauthorizedTarget(address target, bool withDelegatecall); + + // ========================= + // ======== Events ======== + // ========================= + + /// @notice Emitted when a target with call type is authorized. + event TargetAuthorized(address indexed target, bool indexed withDelegatecall, bool isAuthorized); + + // =================================== + // ======== Storage Variables ======== + // =================================== + + /// @notice Name of this action guard. + string public name; + + /// @notice The Llama instance's executor. + address public llamaExecutor; + + /// @notice A mapping of authorized targets and their call type. + mapping(address target => mapping(bool withDelegatecall => bool isAuthorized)) public authorizedTargets; + + // ====================================================== + // ======== Contract Creation and Initialization ======== + // ====================================================== + + /// @dev This contract is deployed as a minimal proxy from the guard factory's `deploy` function. The + /// `_disableInitializers` locks the implementation (logic) contract, preventing any future initialization of it. + constructor() { + _disableInitializers(); + } + + /// @inheritdoc ILlamaActionGuardMinimalProxy + function initialize(string memory _name, address _llamaExecutor, bytes memory config) + external + initializer + returns (bool) + { + name = _name; + llamaExecutor = _llamaExecutor; + Config memory guardConfig = abi.decode(config, (Config)); + _setAuthorizedTargets(guardConfig.authorizedTargets); + return true; + } + + // ================================ + // ======== External Logic ======== + // ================================ + + /// @inheritdoc ILlamaActionGuard + function validateActionCreation(ActionInfo calldata actionInfo) external view { + // Decode the action calldata to get the LlamaAccount execute target and call type. + (address target, bool withDelegatecall,,) = abi.decode(actionInfo.data[4:], (address, bool, uint256, bytes)); + if (!authorizedTargets[target][withDelegatecall]) revert UnauthorizedTarget(target, withDelegatecall); + } + + /// @notice Allows the llama executor to set the authorized targets and their call type. + /// @param data The data to set the authorized targets and their call type. + function setAuthorizedTargets(AuthorizedTargetConfig[] memory data) external { + if (msg.sender != llamaExecutor) revert OnlyLlama(); + _setAuthorizedTargets(data); + } + + /// @inheritdoc ILlamaActionGuard + function validatePreActionExecution(ActionInfo calldata actionInfo) external pure {} + + /// @inheritdoc ILlamaActionGuard + function validatePostActionExecution(ActionInfo calldata actionInfo) external pure {} + + // ================================ + // ======== Internal Logic ======== + // ================================ + + /// @dev Sets the authorized targets and their call type. + function _setAuthorizedTargets(AuthorizedTargetConfig[] memory data) internal { + uint256 length = data.length; + for (uint256 i = 0; i < length; LlamaUtils.uncheckedIncrement(i)) { + authorizedTargets[data[i].target][data[i].withDelegatecall] = data[i].isAuthorized; + emit TargetAuthorized(data[i].target, data[i].withDelegatecall, data[i].isAuthorized); + } + } +} diff --git a/src/guards/LlamaActionGuardFactory.sol b/src/guards/LlamaActionGuardFactory.sol new file mode 100644 index 000000000..3f811dd33 --- /dev/null +++ b/src/guards/LlamaActionGuardFactory.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Clones} from "@openzeppelin/proxy/Clones.sol"; + +import {ILlamaActionGuardMinimalProxy} from "src/interfaces/ILlamaActionGuardMinimalProxy.sol"; + +/// @title LlamaActionGuardFactory +/// @author Llama (devsdosomething@llama.xyz) +/// @notice This contract enables Llama instances to deploy action guards. +contract LlamaActionGuardFactory { + /// @dev Configuration of new Llama action guard. + struct LlamaActionGuardConfig { + string name; // The name of the new action guard. + address llamaExecutor; // The address of the Llama executor. + uint256 nonce; // The nonce of the new action guard. + ILlamaActionGuardMinimalProxy actionGuardLogic; // The logic contract of the new action guard. + bytes initializationData; // The initialization data for the new action guard. + } + + /// @dev Emitted when a new Llama action guard is created. + event LlamaActionGuardCreated( + address indexed deployer, + string name, + address indexed llamaExecutor, + uint256 nonce, + ILlamaActionGuardMinimalProxy actionGuard, + ILlamaActionGuardMinimalProxy indexed actionGuardLogic, + bytes initializationData + ); + + /// @notice Deploys a new Llama action guard. + /// @param actionGuardConfig The configuration of the new Llama action guard. + /// @return actionGuard The address of the new action guard. + function deploy(LlamaActionGuardConfig memory actionGuardConfig) + external + returns (ILlamaActionGuardMinimalProxy actionGuard) + { + bytes32 salt = keccak256( + abi.encodePacked(msg.sender, actionGuardConfig.name, actionGuardConfig.llamaExecutor, actionGuardConfig.nonce) + ); + + // Deploy and initialize Llama action guard + actionGuard = + ILlamaActionGuardMinimalProxy(Clones.cloneDeterministic(address(actionGuardConfig.actionGuardLogic), salt)); + actionGuard.initialize( + actionGuardConfig.name, actionGuardConfig.llamaExecutor, actionGuardConfig.initializationData + ); + + emit LlamaActionGuardCreated( + msg.sender, + actionGuardConfig.name, + actionGuardConfig.llamaExecutor, + actionGuardConfig.nonce, + actionGuard, + actionGuardConfig.actionGuardLogic, + actionGuardConfig.initializationData + ); + } +} diff --git a/src/interfaces/ILlamaActionGuardMinimalProxy.sol b/src/interfaces/ILlamaActionGuardMinimalProxy.sol new file mode 100644 index 000000000..12734083e --- /dev/null +++ b/src/interfaces/ILlamaActionGuardMinimalProxy.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ILlamaActionGuard} from "src/interfaces/ILlamaActionGuard.sol"; + +/// @title Llama Action Guard Minimal Proxy Interface +/// @author Llama (devsdosomething@llama.xyz) +/// @notice This is the interface for minimal proxy action guards. +interface ILlamaActionGuardMinimalProxy is ILlamaActionGuard { + /// @notice Initializes a new clone of the action guard. + /// @dev This function is called by the `deploy` function in the `LlamaActionGuardFactory` contract. The `initializer` + /// modifier ensures that this function can be invoked at most once. + /// @param name The name of the new action guard. + /// @param llamaExecutor The address of the Llama executor. + /// @param config The guard configuration, encoded as bytes to support differing constructor arguments in + /// different guard logic contracts. + /// @return This return statement must be hardcoded to `true` to ensure that initializing an EOA + /// (like the zero address) will revert. + function initialize(string memory name, address llamaExecutor, bytes memory config) external returns (bool); +}