This repository has been archived by the owner on Nov 27, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: account multicall module (#100)
**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
Showing
19 changed files
with
1,077 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
Oops, something went wrong.