Skip to content
This repository has been archived by the owner on Nov 27, 2024. It is now read-only.

feat: account guard #514

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions script/DeployLlamaFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)));
0xrajath marked this conversation as resolved.
Show resolved Hide resolved

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))));
}
}
113 changes: 113 additions & 0 deletions src/guards/LlamaAccountExecuteGuard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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 "./../interfaces/ILlamaActionGuard.sol";
0xrajath marked this conversation as resolved.
Show resolved Hide resolved
import {LlamaUtils} from "src/lib/LlamaUtils.sol";
import {ActionInfo} from "src/lib/Structs.sol";

/// @title Llama Account Execute Guard
/// @author Llama ([email protected])
/// @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 Llama account execute guard initialization configuration.
struct Config {
AuthorizedTargetConfig[] authorizedTargets; // The authorized targets and their call type.
}

/// @dev Authorized target configuration.
struct AuthorizedTargetConfig {
address target; // The target contract.
bool withDelegatecall; // Call type.
bool isAuthorized; // Is the target authorized.
}
0xrajath marked this conversation as resolved.
Show resolved Hide resolved

// =========================
// ======== 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 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;
Comment on lines +59 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion but do you think we should add selector allowlisting in this guard too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me think about it for a bit.

Copy link
Contributor Author

@0xrajath 0xrajath Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about it for a bit. I think it makes sense to add selector allowlisting in this guard too. But we can rework the mapping to not make it so deep.

We would have two mappings:

/// @notice A mapping of authorized selectors that can be called within a target.
mapping(address target => mapping(bytes4 selector => bool isAuthorized)) public authorizedSelectors;

/// @notice A mapping of targets and their call type.
mapping(address target => bool withDelegatecall) public targetCallType;


// ======================================================
// ======== 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(address _llamaExecutor, bytes memory config) external initializer returns (bool) {
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 {}
Comment on lines +103 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return true for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interfaces don't have a return value.


// ================================
// ======== 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);
}
}
}
55 changes: 55 additions & 0 deletions src/guards/LlamaActionGuardFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 ([email protected])
/// @notice This contract enables Llama instances to deploy action guards.
contract LlamaActionGuardFactory {
/// @dev Configuration of new Llama action guard.
struct LlamaActionGuardConfig {
address llamaExecutor; // The address of the Llama executor.
ILlamaActionGuardMinimalProxy actionGuardLogic; // The logic contract of the new action guard.
bytes initializationData; // The initialization data for the new action guard.
uint256 nonce; // The nonce of the new action guard.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea for the nonce is instances will want to deploy multiple guards with the same initialization data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the salt is (msg.sender, actionGuardConfig.llamaExecutor, actionGuardConfig.nonce). So the nonce will allow the same msg.sender to deploy multiple guards for the same executor (When it's deployed through an action, the msg.sender and executor will be the same, but this gives us the optionality to deploy on behalf of an instance as well).

Initialization data is not used in the salt since that data can be altered through setters in the guard later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively I could use something like name instead of a nonce. Just wanted something to differentiate it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added name in b4412bb

Copy link
Contributor Author

@0xrajath 0xrajath Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And kept nonce as well as an optionality to allow multiple deployments if necessary.

}

/// @dev Emitted when a new Llama action guard is created.
event LlamaActionGuardCreated(
address indexed deployer,
address indexed llamaExecutor,
ILlamaActionGuardMinimalProxy indexed actionGuardLogic,
ILlamaActionGuardMinimalProxy actionGuard,
bytes initializationData,
uint256 nonce,
uint256 chainId
);

/// @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.llamaExecutor, actionGuardConfig.nonce));
0xrajath marked this conversation as resolved.
Show resolved Hide resolved

// Deploy and initialize Llama action guard
actionGuard =
ILlamaActionGuardMinimalProxy(Clones.cloneDeterministic(address(actionGuardConfig.actionGuardLogic), salt));
actionGuard.initialize(actionGuardConfig.llamaExecutor, actionGuardConfig.initializationData);

emit LlamaActionGuardCreated(
msg.sender,
actionGuardConfig.llamaExecutor,
actionGuardConfig.actionGuardLogic,
actionGuard,
actionGuardConfig.initializationData,
actionGuardConfig.nonce,
block.chainid
);
}
}
19 changes: 19 additions & 0 deletions src/interfaces/ILlamaActionGuardMinimalProxy.sol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just edit ILlamaActionGuard? It's not used anywhere besides tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used in the LlamaCore which is why I didn't want to touch it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 ([email protected])
/// @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 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(address llamaExecutor, bytes memory config) external returns (bool);
}
Loading