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

Commit

Permalink
feat: account multicall module (#100)
Browse files Browse the repository at this point in the history
**Motivation:**

We need a Rewards Claimer that allows multiple rewards to be claimed
with the Llama account as the caller.
This can be achieved through a generic `Account Multicall` module.

**Modifications:**

* `LlamaAccountMulticallFactory` - To deploy the account multicall
modules.
* `LlamaBaseAccountExtension` - Base Account Extension contract with
`onlyDelegateCall` modifier
* `LlamaAccountMulticallGuard` - Guard on top of Llama Account's execute
function.
* `LlamaAccountMulticallExtension` - A multicall extension for the Llama
Account with authorized targets and selectors
* `LlamaAccountMulticallStorage` - Account Multicall Storage contract
(To prevent storage collision with Llama Account while being
delegate-called)
* Account Multicall deploy scripts and input config.
* Tests

**Result:**

`Account Multicall` module.

**Gas Report:**

`forge test --match-test=test_Multicall --gas-report`

<img width="1251" alt="Screen Shot 2024-04-11 at 9 44 04 PM"
src="https://github.com/llamaxyz/llama-periphery/assets/27264227/ff4edbab-752f-42f1-b794-13f638cf907e">
  • Loading branch information
0xrajath authored Apr 18, 2024
1 parent 5d31377 commit d60ab2f
Show file tree
Hide file tree
Showing 19 changed files with 1,077 additions and 2 deletions.
21 changes: 19 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,31 @@ run-script script_name flags='' sig='' args='':
-vvvv {{flags}}
mv _test test

run-deploy-voting-module-script flags: (run-script 'DeployLlamaTokenVotingModule' flags '--sig "run(address,string)"' '$SCRIPT_DEPLOYER_ADDRESS "tokenVotingModuleConfig.json"')
# Token voting module

dry-run-deploy: (run-script 'DeployLlamaTokenVotingFactory')

deploy: (run-script 'DeployLlamaTokenVotingFactory' '--broadcast --verify --slow --build-info --build-info-path build_info')

verify: (run-script 'DeployLlamaTokenVotingFactory' '--verify --resume')

run-deploy-voting-module-script flags: (run-script 'DeployLlamaTokenVotingModule' flags '--sig "run(address,string)"' '$SCRIPT_DEPLOYER_ADDRESS "tokenVotingModuleConfig.json"')

dry-run-deploy-voting-module: (run-deploy-voting-module-script '')

deploy-voting-module: (run-deploy-voting-module-script '--broadcast --verify')

verify: (run-script 'DeployLlamaTokenVotingFactory' '--verify --resume')
# Account multicall module

dry-run-deploy-account-multicall-factory: (run-script 'DeployLlamaAccountMulticallFactory')

deploy-account-multicall-factory: (run-script 'DeployLlamaAccountMulticallFactory' '--broadcast --verify --slow --build-info --build-info-path build_info')

verify-account-multicall-factory: (run-script 'DeployLlamaAccountMulticallFactory' '--verify --resume')

run-deploy-account-multicall-module-script flags: (run-script 'DeployLlamaAccountMulticallModule' flags '--sig "run(address,string)"' '$SCRIPT_DEPLOYER_ADDRESS "accountMulticallConfig.json"')

dry-run-deploy-account-multicall-module: (run-deploy-account-multicall-module-script '')

deploy-account-multicall-module: (run-deploy-account-multicall-module-script '--broadcast --verify --slow')

21 changes: 21 additions & 0 deletions script/DeployLlamaAccountMulticallFactory.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {Script, stdJson} from "forge-std/Script.sol";

import {DeployUtils} from "script/DeployUtils.sol";

import {LlamaAccountMulticallFactory} from "src/account-multicall/LlamaAccountMulticallFactory.sol";

contract DeployLlamaAccountMulticallFactory is Script {
// Factory contracts.
LlamaAccountMulticallFactory accountMulticallFactory;

function run() public {
DeployUtils.print(string.concat("Deploying Llama account multicall factory to chain:", vm.toString(block.chainid)));

vm.broadcast();
accountMulticallFactory = new LlamaAccountMulticallFactory();
DeployUtils.print(string.concat(" LlamaAccountMulticallFactory: ", vm.toString(address(accountMulticallFactory))));
}
}
74 changes: 74 additions & 0 deletions script/DeployLlamaAccountMulticallModule.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {Script, stdJson} from "forge-std/Script.sol";

import {DeployUtils} from "script/DeployUtils.sol";

import {LlamaAccountMulticallExtension} from "src/account-multicall/LlamaAccountMulticallExtension.sol";
import {LlamaAccountMulticallFactory} from "src/account-multicall/LlamaAccountMulticallFactory.sol";
import {LlamaAccountMulticallGuard} from "src/account-multicall/LlamaAccountMulticallGuard.sol";
import {LlamaAccountMulticallStorage} from "src/account-multicall/LlamaAccountMulticallStorage.sol";

contract DeployLlamaAccountMulticallModule is Script {
using stdJson for string;

struct TargetSelectorAuthorizationInputs {
// Attributes need to be in alphabetical order so JSON decodes properly.
string comment;
bytes selector;
address target;
}

// Account multicall contracts.
LlamaAccountMulticallStorage accountMulticallStorage;
LlamaAccountMulticallExtension accountMulticallExtension;
LlamaAccountMulticallGuard accountMulticallGuard;

function run(address deployer, string memory configFile) public {
string memory jsonInput = DeployUtils.readScriptInput(configFile);

LlamaAccountMulticallFactory factory = LlamaAccountMulticallFactory(jsonInput.readAddress(".factory"));

DeployUtils.print(string.concat("Deploying Llama account multicall module to chain:", vm.toString(block.chainid)));

address llamaExecutor = jsonInput.readAddress(".llamaExecutor");
uint256 nonce = jsonInput.readUint(".nonce");
LlamaAccountMulticallStorage.TargetSelectorAuthorization[] memory data = readTargetSelectorAuthorizations(jsonInput);
LlamaAccountMulticallFactory.LlamaAccountMulticallConfig memory config =
LlamaAccountMulticallFactory.LlamaAccountMulticallConfig(llamaExecutor, nonce, data);

vm.broadcast(deployer);
(accountMulticallGuard, accountMulticallExtension, accountMulticallStorage) = factory.deploy(config);

DeployUtils.print("Successfully deployed a new Llama account multicall module");
DeployUtils.print(string.concat(" LlamaAccountMulticallGuard: ", vm.toString(address(accountMulticallGuard))));
DeployUtils.print(
string.concat(" LlamaAccountMulticallExtension: ", vm.toString(address(accountMulticallExtension)))
);
DeployUtils.print(
string.concat(" LlamaAccountMulticallStorage: ", vm.toString(address(accountMulticallStorage)))
);
}

function readTargetSelectorAuthorizations(string memory jsonInput)
internal
pure
returns (LlamaAccountMulticallStorage.TargetSelectorAuthorization[] memory)
{
bytes memory data = jsonInput.parseRaw(".initialTargetSelectorAuthorizations");

TargetSelectorAuthorizationInputs[] memory rawConfigs = abi.decode(data, (TargetSelectorAuthorizationInputs[]));

LlamaAccountMulticallStorage.TargetSelectorAuthorization[] memory configs =
new LlamaAccountMulticallStorage.TargetSelectorAuthorization[](rawConfigs.length);

for (uint256 i = 0; i < rawConfigs.length; i++) {
configs[i].target = rawConfigs[i].target;
configs[i].selector = bytes4(rawConfigs[i].selector);
configs[i].isAuthorized = true;
}

return configs;
}
}
13 changes: 13 additions & 0 deletions script/input/1/mockAccountMulticallConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"comment": "This is an account multicall deployment for Forge tests.",
"factory": "0x90193C961A926261B756D1E5bb255e67ff9498A1",
"llamaExecutor": "0xdAf00E9786cABB195a8a1Cf102730863aE94Dd75",
"nonce": 0,
"initialTargetSelectorAuthorizations": [
{
"comment": "DeadBeef.withdraw()",
"selector": "0x3ccfd60b",
"target": "0x00000000000000000000000000000000deadbeef"
}
]
}
13 changes: 13 additions & 0 deletions script/input/11155111/accountMulticallConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"comment": "This is an example account multicall deployment on Sepolia.",
"factory": "0x0000000000000000000000000000000000000000",
"llamaExecutor": "0x0000000000000000000000000000000000000000",
"nonce": 0,
"initialTargetSelectorAuthorizations": [
{
"comment": "Target::Selector",
"selector": "0x00000000",
"target": "0x0000000000000000000000000000000000000000"
}
]
}
59 changes: 59 additions & 0 deletions src/account-multicall/LlamaAccountMulticallExtension.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {LlamaAccountMulticallStorage} from "src/account-multicall/LlamaAccountMulticallStorage.sol";
import {LlamaBaseAccountExtension} from "src/common/LlamaBaseAccountExtension.sol";
import {LlamaUtils} from "src/lib/LlamaUtils.sol";

/// @title Llama Account Multicall Extension
/// @author Llama (devsdosomething@llama.xyz)
/// @notice An account extension that can multicall on behalf of the Llama account.
/// @dev This contract should be delegatecalled from a Llama account.
contract LlamaAccountMulticallExtension is LlamaBaseAccountExtension {
/// @dev Struct to hold target data.
struct TargetData {
address target; // The target contract.
uint256 value; // The target call value.
bytes data; // The target call data.
}

/// @dev The call did not succeed.
/// @param index Index of the target data being called.
/// @param revertData Data returned by the called function.
error CallReverted(uint256 index, bytes revertData);

/// @dev Thrown if the target-selector is not authorized.
error UnauthorizedTargetSelector(address target, bytes4 selector);

/// @notice The Llama account multicall storage contract.
LlamaAccountMulticallStorage public immutable ACCOUNT_MULTICALL_STORAGE;

/// @dev Initializes the Llama account multicall extenstion.
constructor(LlamaAccountMulticallStorage accountMulticallStorage) {
ACCOUNT_MULTICALL_STORAGE = accountMulticallStorage;
}

/// @notice Multicalls on behalf of the Llama account.
/// @param targetData The target data to multicall.
/// @return returnData The return data from the target calls.
function multicall(TargetData[] memory targetData) external onlyDelegateCall returns (bytes[] memory returnData) {
uint256 length = targetData.length;
returnData = new bytes[](length);
for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) {
address target = targetData[i].target;
uint256 value = targetData[i].value;
bytes memory callData = targetData[i].data;
bytes4 selector = bytes4(callData);

// Check if the target-selector is authorized.
if (!ACCOUNT_MULTICALL_STORAGE.authorizedTargetSelectors(target, selector)) {
revert UnauthorizedTargetSelector(target, selector);
}

// Execute the call.
(bool success, bytes memory result) = target.call{value: value}(callData);
if (!success) revert CallReverted(i, result);
returnData[i] = result;
}
}
}
66 changes: 66 additions & 0 deletions src/account-multicall/LlamaAccountMulticallFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {LlamaAccountMulticallExtension} from "src/account-multicall/LlamaAccountMulticallExtension.sol";
import {LlamaAccountMulticallGuard} from "src/account-multicall/LlamaAccountMulticallGuard.sol";
import {LlamaAccountMulticallStorage} from "src/account-multicall/LlamaAccountMulticallStorage.sol";

/// @title LlamaAccountMulticallFactory
/// @author Llama (devsdosomething@llama.xyz)
/// @notice This contract enables Llama instances to deploy an account multicall module.
contract LlamaAccountMulticallFactory {
/// @dev Configuration of new Llama account multicall module.
struct LlamaAccountMulticallConfig {
address llamaExecutor; // The address of the Llama executor.
uint256 nonce; // The nonce of the new account multicall module.
LlamaAccountMulticallStorage.TargetSelectorAuthorization[] data; // The target-selectors to authorize.
}

/// @dev Emitted when a new Llama account multicall module is created.
event LlamaAccountMulticallModuleCreated(
address indexed deployer,
address indexed llamaExecutor,
uint256 nonce,
address accountMulticallGuard,
address accountMulticallExtension,
address accountMulticallStorage,
uint256 chainId
);

/// @notice Deploys a new Llama account multicall module.
/// @param accountMulticallConfig The configuration of the new Llama account multicall module.
/// @return accountMulticallGuard The deployed account multicall guard.
/// @return accountMulticallExtension The deployed account multicall extension.
/// @return accountMulticallStorage The deployed account multicall storage.
function deploy(LlamaAccountMulticallConfig memory accountMulticallConfig)
external
returns (
LlamaAccountMulticallGuard accountMulticallGuard,
LlamaAccountMulticallExtension accountMulticallExtension,
LlamaAccountMulticallStorage accountMulticallStorage
)
{
bytes32 salt =
keccak256(abi.encodePacked(msg.sender, accountMulticallConfig.llamaExecutor, accountMulticallConfig.nonce));

// Deploy and initialize account multicall storage.
accountMulticallStorage = new LlamaAccountMulticallStorage{salt: salt}(accountMulticallConfig.llamaExecutor);
accountMulticallStorage.initializeAuthorizedTargetSelectors(accountMulticallConfig.data);

// Deploy account multicall extension.
accountMulticallExtension = new LlamaAccountMulticallExtension{salt: salt}(accountMulticallStorage);

// Deploy account multicall guard.
accountMulticallGuard = new LlamaAccountMulticallGuard{salt: salt}(accountMulticallExtension);

emit LlamaAccountMulticallModuleCreated(
msg.sender,
accountMulticallConfig.llamaExecutor,
accountMulticallConfig.nonce,
address(accountMulticallGuard),
address(accountMulticallExtension),
address(accountMulticallStorage),
block.chainid
);
}
}
44 changes: 44 additions & 0 deletions src/account-multicall/LlamaAccountMulticallGuard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {LlamaAccountMulticallExtension} from "src/account-multicall/LlamaAccountMulticallExtension.sol";
import {ILlamaActionGuard} from "src/interfaces/ILlamaActionGuard.sol";
import {ActionInfo} from "src/lib/Structs.sol";

/// @title Llama Account Multicall Guard
/// @author Llama (devsdosomething@llama.xyz)
/// @notice A guard that only allows the `LlamaAccountMulticallExtension.multicall` to be delegate-called
/// @dev This guard should be used to protect the `execute` function in the `LlamaAccount` contract
contract LlamaAccountMulticallGuard is ILlamaActionGuard {
/// @dev Thrown if the call is not authorized.
error UnauthorizedCall(address target, bytes4 selector, bool withDelegatecall);

/// @notice The address of the Llama account multicall extension.
LlamaAccountMulticallExtension public immutable ACCOUNT_MULTICALL_EXTENSION;

/// @dev Initializes the Llama account multicall guard.
constructor(LlamaAccountMulticallExtension accountMulticallExtension) {
ACCOUNT_MULTICALL_EXTENSION = accountMulticallExtension;
}

/// @inheritdoc ILlamaActionGuard
function validateActionCreation(ActionInfo calldata actionInfo) external view {
// Decode the action calldata to get the LlamaAccount execute target, call type and call data.
(address target, bool withDelegatecall,, bytes memory data) =
abi.decode(actionInfo.data[4:], (address, bool, uint256, bytes));
bytes4 selector = bytes4(data);

// Check if the target is the Llama account multicall extension, selector is `multicall` and the call type is a
// delegatecall.
if (
target != address(ACCOUNT_MULTICALL_EXTENSION) || selector != LlamaAccountMulticallExtension.multicall.selector
|| !withDelegatecall
) revert UnauthorizedCall(target, selector, withDelegatecall);
}

/// @inheritdoc ILlamaActionGuard
function validatePreActionExecution(ActionInfo calldata actionInfo) external pure {}

/// @inheritdoc ILlamaActionGuard
function validatePostActionExecution(ActionInfo calldata actionInfo) external pure {}
}
Loading

0 comments on commit d60ab2f

Please sign in to comment.