From 64800c67f6b10c9d244e41af2167b98fbf3a9ace Mon Sep 17 00:00:00 2001 From: Austin Green Date: Fri, 24 Nov 2023 10:23:42 -0500 Subject: [PATCH 01/19] create delegation script and test case --- .../LlamaAccountTokenDelegationScript.sol | 15 ++++ .../LlamaAccountTokenDelegationScript.t.sol | 87 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/llama-scripts/LlamaAccountTokenDelegationScript.sol create mode 100644 test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol new file mode 100644 index 000000000..979eb8628 --- /dev/null +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {LlamaBaseScript} from "src/llama-scripts/LlamaBaseScript.sol"; +import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; + +/// @title Llama Account Token Delegation Script +/// @author Llama (devsdosomething@llama.xyz) +/// @notice A script that allows users to delegate governance tokens in their Llama accounts. +contract LlamaAccountTokenDelegationScript is LlamaBaseScript { + function delegateAccountTokenToExecutor(LlamaAccount account, address token) external onlyDelegateCall { + // TODO: add checks for isERC20Votes or isERC721Votes token and isLlamaAccount + account.execute(token, false, 0, abi.encodeWithSelector(0x5c19a95c, address(this))); + } +} diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol new file mode 100644 index 000000000..9c332ddfc --- /dev/null +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; +import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; + +import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; +import {Roles, LlamaTestSetup} from "test/utils/LlamaTestSetup.sol"; +import {LlamaAccountTokenDelegationScript} from "src/llama-scripts/LlamaAccountTokenDelegationScript.sol"; +import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; + +contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { + IERC20 public constant UNI = IERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); + address public constant UNI_WHALE = 0x878f0822A9e77c1dD7883E543747147Be8D63C3B; + uint256 public constant UNI_AMOUNT = 1000e18; + + address delegationScriptAddress; + + PermissionData public delegateTokensPermission; + + function setUp() public virtual override { + vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); + LlamaTestSetup.setUp(); + + deal(address(UNI), address(mpAccount1), UNI_AMOUNT); + + delegationScriptAddress = address(new LlamaAccountTokenDelegationScript()); + + delegateTokensPermission = PermissionData( + address(delegationScriptAddress), + LlamaAccountTokenDelegationScript.delegateAccountTokenToExecutor.selector, + mpStrategy1 + ); + + vm.startPrank(address(mpExecutor)); + mpCore.setScriptAuthorization(address(delegationScriptAddress), true); + mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokensPermission, true); + vm.stopPrank(); + } +} + +contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { + function executeDelegateTokensAction() internal { + bytes memory data = abi.encodeCall( + LlamaAccountTokenDelegationScript.delegateAccountTokenToExecutor, + (LlamaAccount(payable(address(mpAccount1))), address(UNI)) + ); + + vm.prank(actionCreatorAaron); + uint256 actionId = + mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data, ""); + + ActionInfo memory actionInfo = ActionInfo( + actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data + ); + vm.warp(block.timestamp + 1); + + vm.prank(approverAdam); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + vm.prank(approverAlicia); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + + vm.warp(block.timestamp + 6 days); + + mpCore.queueAction(actionInfo); + + vm.warp(block.timestamp + 5 days); + + mpCore.executeAction(actionInfo); + } + + function test_TokensDelegatedToExecutor() external { + // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + + executeDelegateTokensAction(); + + // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); + } +} From e44a4f7dd59d7474ea1326fa6554a43be554903d Mon Sep 17 00:00:00 2001 From: Austin Green Date: Fri, 24 Nov 2023 10:31:57 -0500 Subject: [PATCH 02/19] update forge fmt --- script/DeployLlamaFactory.s.sol | 6 +----- test/LlamaFactory.t.sol | 6 +----- test/strategies/absolute/LlamaAbsoluteStrategyBase.t.sol | 3 +-- test/strategies/relative/LlamaRelativeHolderQuorum.t.sol | 6 ++---- test/strategies/relative/LlamaRelativeQuantityQuorum.t.sol | 6 ++---- .../relative/LlamaRelativeUniqueHolderQuorum.t.sol | 6 ++---- test/utils/SolarrayLlama.sol | 2 +- 7 files changed, 10 insertions(+), 25 deletions(-) diff --git a/script/DeployLlamaFactory.s.sol b/script/DeployLlamaFactory.s.sol index 69208982a..9e1362851 100644 --- a/script/DeployLlamaFactory.s.sol +++ b/script/DeployLlamaFactory.s.sol @@ -66,11 +66,7 @@ contract DeployLlamaFactory is Script { DeployUtils.print(string.concat(" LlamaPolicyMetadataLogic:", vm.toString(address(policyMetadataLogic)))); vm.broadcast(); - factory = new LlamaFactory( - coreLogic, - policyLogic, - policyMetadataLogic - ); + factory = new LlamaFactory(coreLogic, policyLogic, policyMetadataLogic); DeployUtils.print(string.concat(" LlamaFactory:", vm.toString(address(factory)))); vm.broadcast(); diff --git a/test/LlamaFactory.t.sol b/test/LlamaFactory.t.sol index 94b02447e..f7fb1f766 100644 --- a/test/LlamaFactory.t.sol +++ b/test/LlamaFactory.t.sol @@ -52,11 +52,7 @@ contract LlamaFactoryTest is LlamaTestSetup { contract Constructor is LlamaFactoryTest { function deployLlamaFactory() internal returns (LlamaFactory) { - return new LlamaFactory( - coreLogic, - policyLogic, - policyMetadataLogic - ); + return new LlamaFactory(coreLogic, policyLogic, policyMetadataLogic); } function test_SetsLlamaCoreLogicAddress() public { diff --git a/test/strategies/absolute/LlamaAbsoluteStrategyBase.t.sol b/test/strategies/absolute/LlamaAbsoluteStrategyBase.t.sol index 1c3ee1ebf..37df0c3bf 100644 --- a/test/strategies/absolute/LlamaAbsoluteStrategyBase.t.sol +++ b/test/strategies/absolute/LlamaAbsoluteStrategyBase.t.sol @@ -1001,8 +1001,7 @@ contract GetDisapprovalQuantityAt is LlamaAbsoluteStrategyBaseTest { DEFAULT_APPROVALS, DEFAULT_DISAPPROVALS, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); diff --git a/test/strategies/relative/LlamaRelativeHolderQuorum.t.sol b/test/strategies/relative/LlamaRelativeHolderQuorum.t.sol index 3f3da6ed4..2644484e2 100644 --- a/test/strategies/relative/LlamaRelativeHolderQuorum.t.sol +++ b/test/strategies/relative/LlamaRelativeHolderQuorum.t.sol @@ -243,8 +243,7 @@ contract GetApprovalQuantityAt is LlamaRelativeHolderQuorumTest { 4000, 2000, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); @@ -418,8 +417,7 @@ contract GetDisapprovalQuantityAt is LlamaRelativeHolderQuorumTest { 4000, 2000, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); diff --git a/test/strategies/relative/LlamaRelativeQuantityQuorum.t.sol b/test/strategies/relative/LlamaRelativeQuantityQuorum.t.sol index 098de9e92..f2e9b8567 100644 --- a/test/strategies/relative/LlamaRelativeQuantityQuorum.t.sol +++ b/test/strategies/relative/LlamaRelativeQuantityQuorum.t.sol @@ -245,8 +245,7 @@ contract GetApprovalQuantityAt is LlamaRelativeQuantityQuorumTest { 4000, 2000, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); @@ -424,8 +423,7 @@ contract GetDisapprovalQuantityAt is LlamaRelativeQuantityQuorumTest { 4000, 2000, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); diff --git a/test/strategies/relative/LlamaRelativeUniqueHolderQuorum.t.sol b/test/strategies/relative/LlamaRelativeUniqueHolderQuorum.t.sol index 787d6269a..8a7f4ca0a 100644 --- a/test/strategies/relative/LlamaRelativeUniqueHolderQuorum.t.sol +++ b/test/strategies/relative/LlamaRelativeUniqueHolderQuorum.t.sol @@ -243,8 +243,7 @@ contract GetApprovalQuantityAt is LlamaRelativeHolderQuorumTest { 4000, 2000, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); @@ -473,8 +472,7 @@ contract GetDisapprovalQuantityAt is LlamaRelativeHolderQuorumTest { 4000, 2000, new uint8[](0), - new - uint8[](0) + new uint8[](0) ); vm.warp(_timestamp); diff --git a/test/utils/SolarrayLlama.sol b/test/utils/SolarrayLlama.sol index 843841bcf..39fd1a5ca 100644 --- a/test/utils/SolarrayLlama.sol +++ b/test/utils/SolarrayLlama.sol @@ -212,7 +212,7 @@ library SolarrayLlama { { uint256 length1 = arr1.length; uint256 length2 = arr2.length; - newArr = new RoleDescription[](length1+ length2); + newArr = new RoleDescription[](length1 + length2); for (uint256 i = 0; i < length1;) { newArr[i] = arr1[i]; unchecked { From 07d7ac4b7035ce687f3dd3b487e0a3529d7802a0 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Sat, 25 Nov 2023 10:46:44 -0500 Subject: [PATCH 03/19] add acct logic with delegation --- src/accounts/LlamaAccountWithDelegation.sol | 379 ++++++++++++++++++ .../LlamaAccountTokenDelegationScript.sol | 1 - 2 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 src/accounts/LlamaAccountWithDelegation.sol diff --git a/src/accounts/LlamaAccountWithDelegation.sol b/src/accounts/LlamaAccountWithDelegation.sol new file mode 100644 index 000000000..7f22a1a48 --- /dev/null +++ b/src/accounts/LlamaAccountWithDelegation.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; +import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import {IERC721} from "@openzeppelin/token/ERC721/IERC721.sol"; +import {ERC721Holder} from "@openzeppelin/token/ERC721/utils/ERC721Holder.sol"; +import {IERC1155} from "@openzeppelin/token/ERC1155/IERC1155.sol"; +import {ERC1155Holder} from "@openzeppelin/token/ERC1155/utils/ERC1155Holder.sol"; +import {Address} from "@openzeppelin/utils/Address.sol"; + +import {ILlamaAccount} from "src/interfaces/ILlamaAccount.sol"; +import {LlamaUtils} from "src/lib/LlamaUtils.sol"; +import {LlamaCore} from "src/LlamaCore.sol"; + +/// @title Llama Account With Delegation +/// @author Llama (devsdosomething@llama.xyz) +/// @notice This contract is the standard LlamaAccount with an additional delegate and batch delegate function for +/// voting tokens. +contract LlamaAccountWithDelegation is ILlamaAccount, ERC721Holder, ERC1155Holder, Initializable { + using SafeERC20 for IERC20; + using Address for address payable; + + // ========================= + // ======== Structs ======== + // ========================= + + /// @dev Llama account initialization configuration. + struct Config { + string name; // Name of the Llama account. + } + + /// @dev Data for sending native tokens to recipients. + struct NativeTokenData { + address payable recipient; // Recipient of the native tokens. + uint256 amount; // Amount of native tokens to send. + } + + /// @dev Data for sending ERC20 tokens to recipients. + struct ERC20Data { + IERC20 token; // The ERC20 token to transfer. + address recipient; // The address to transfer the token to. + uint256 amount; // The amount of tokens to transfer. + } + + /// @dev Data for sending ERC721 tokens to recipients. + struct ERC721Data { + IERC721 token; // The ERC721 token to transfer. + address recipient; // The address to transfer the token to. + uint256 tokenId; // The tokenId of the token to transfer. + } + + /// @dev Data for operator allowance for ERC721 transfers. + struct ERC721OperatorData { + IERC721 token; // The ERC721 token to transfer. + address recipient; // The address to transfer the token to. + bool approved; // Whether to approve or revoke allowance. + } + + /// @dev Data for sending ERC1155 tokens to recipients. + struct ERC1155Data { + IERC1155 token; // The ERC1155 token to transfer. + address recipient; // The address to transfer the token to. + uint256 tokenId; // The tokenId of the token to transfer. + uint256 amount; // The amount of tokens to transfer. + bytes data; // The data to pass to the ERC1155 token. + } + + /// @dev Data for batch sending ERC1155 tokens to recipients. + struct ERC1155BatchData { + IERC1155 token; // The ERC1155 token to transfer. + address recipient; // The address to transfer the token to. + uint256[] tokenIds; // The tokenId of the token to transfer. + uint256[] amounts; // The amount of tokens to transfer. + bytes data; // The data to pass to the ERC1155 token. + } + + /// @dev Data for operator allowance for ERC1155 transfers. + struct ERC1155OperatorData { + IERC1155 token; // The ERC1155 token to transfer. + address recipient; // The address to transfer the token to. + bool approved; // Whether to approve or revoke allowance. + } + + /// @dev Data for delegating voting tokens to delegatees. + struct TokenDelegateData { + IVotes token; // The voting token to delegate. + address delegatee; // The address being delegated to. + } + + // ====================================== + // ======== Errors and Modifiers ======== + // ====================================== + + /// @dev Only callable by a Llama instance's executor. + error OnlyLlama(); + + /// @dev Recipient cannot be the 0 address. + error ZeroAddressNotAllowed(); + + /// @dev External call failed. + /// @param result Data returned by the called function. + error FailedExecution(bytes result); + + /// @dev Slot 0 cannot be changed as a result of delegatecalls. + error Slot0Changed(); + + /// @dev Value cannot be sent with delegatecalls. + error CannotDelegatecallWithValue(); + + /// @dev Checks that the caller is the Llama executor and reverts if not. + modifier onlyLlama() { + if (msg.sender != llamaExecutor) revert OnlyLlama(); + _; + } + + // =================================== + // ======== Storage Variables ======== + // =================================== + + /// @notice The Llama instance's executor. + /// @dev We intentionally put this before the `name` so it's packed with the `Initializable` + /// storage variables, that way we can only check one slot before and after a delegatecall. + address public llamaExecutor; + + /// @notice Name of the Llama account. + string public name; + + // ====================================================== + // ======== Contract Creation and Initialization ======== + // ====================================================== + + /// @dev This contract is deployed as a minimal proxy from the core's `_deployAccounts` function. The + /// `_disableInitializers` locks the implementation (logic) contract, preventing any future initialization of it. + constructor() { + _disableInitializers(); + } + + /// @inheritdoc ILlamaAccount + function initialize(bytes memory config) external initializer returns (bool) { + llamaExecutor = address(LlamaCore(msg.sender).executor()); + Config memory accountConfig = abi.decode(config, (Config)); + name = accountConfig.name; + + return true; + } + + // =========================================== + // ======== External and Public Logic ======== + // =========================================== + + // -------- Native Token -------- + + /// @notice Enables the Llama account to receive native tokens. + receive() external payable {} + + /// @notice Transfer native tokens to a recipient. + /// @param nativeTokenData The `amount` and `recipient` of the native token transfer. + function transferNativeToken(NativeTokenData calldata nativeTokenData) public onlyLlama { + if (nativeTokenData.recipient == address(0)) revert ZeroAddressNotAllowed(); + nativeTokenData.recipient.sendValue(nativeTokenData.amount); + } + + /// @notice Batch transfer native tokens to a recipient. + /// @param nativeTokenData The `amounts` and `recipients` for the native token transfers. + function batchTransferNativeToken(NativeTokenData[] calldata nativeTokenData) external onlyLlama { + uint256 length = nativeTokenData.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + transferNativeToken(nativeTokenData[i]); + } + } + + // -------- ERC20 Token -------- + + /// @notice Transfer ERC20 tokens to a recipient. + /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 transfer. + function transferERC20(ERC20Data calldata erc20Data) public onlyLlama { + if (erc20Data.recipient == address(0)) revert ZeroAddressNotAllowed(); + erc20Data.token.safeTransfer(erc20Data.recipient, erc20Data.amount); + } + + /// @notice Batch transfer ERC20 tokens to recipients. + /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 transfers. + function batchTransferERC20(ERC20Data[] calldata erc20Data) external onlyLlama { + uint256 length = erc20Data.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + transferERC20(erc20Data[i]); + } + } + + /// @notice Approve an ERC20 allowance for a recipient. + /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 approval. + function approveERC20(ERC20Data calldata erc20Data) public onlyLlama { + erc20Data.token.safeApprove(erc20Data.recipient, erc20Data.amount); + } + + /// @notice Batch approve ERC20 allowances for recipients. + /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 approvals. + function batchApproveERC20(ERC20Data[] calldata erc20Data) external onlyLlama { + uint256 length = erc20Data.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + approveERC20(erc20Data[i]); + } + } + + // -------- ERC721 Token -------- + + /// @notice Transfer an ERC721 token to a recipient. + /// @param erc721Data The `token`, `recipient`, and `tokenId` of the ERC721 transfer. + function transferERC721(ERC721Data calldata erc721Data) public onlyLlama { + if (erc721Data.recipient == address(0)) revert ZeroAddressNotAllowed(); + erc721Data.token.transferFrom(address(this), erc721Data.recipient, erc721Data.tokenId); + } + + /// @notice Batch transfer ERC721 tokens to recipients. + /// @param erc721Data The `token`, `recipient`, and `tokenId` of the ERC721 transfers. + function batchTransferERC721(ERC721Data[] calldata erc721Data) external onlyLlama { + uint256 length = erc721Data.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + transferERC721(erc721Data[i]); + } + } + + /// @notice Approve a recipient to transfer an ERC721. + /// @param erc721Data The `token`, `recipient`, and `tokenId` of the ERC721 approval. + function approveERC721(ERC721Data calldata erc721Data) public onlyLlama { + erc721Data.token.approve(erc721Data.recipient, erc721Data.tokenId); + } + + /// @notice Batch approve recipients to transfer ERC721s. + /// @param erc721Data The `token`, `recipient`, and `tokenId` for the ERC721 approvals. + function batchApproveERC721(ERC721Data[] calldata erc721Data) external onlyLlama { + uint256 length = erc721Data.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + approveERC721(erc721Data[i]); + } + } + + /// @notice Approve an operator for ERC721 transfers. + /// @param erc721OperatorData The `token`, `recipient`, and `approved` boolean for the ERC721 operator approval. + function approveOperatorERC721(ERC721OperatorData calldata erc721OperatorData) public onlyLlama { + erc721OperatorData.token.setApprovalForAll(erc721OperatorData.recipient, erc721OperatorData.approved); + } + + /// @notice Batch approve operators for ERC721 transfers. + /// @param erc721OperatorData The `token`, `recipient`, and `approved` booleans for the ERC721 operator approvals. + function batchApproveOperatorERC721(ERC721OperatorData[] calldata erc721OperatorData) external onlyLlama { + uint256 length = erc721OperatorData.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + approveOperatorERC721(erc721OperatorData[i]); + } + } + + // -------- ERC1155 Token -------- + + /// @notice Transfer ERC1155 tokens to a recipient. + /// @param erc1155Data The data of the ERC1155 transfer. + function transferERC1155(ERC1155Data calldata erc1155Data) external onlyLlama { + if (erc1155Data.recipient == address(0)) revert ZeroAddressNotAllowed(); + erc1155Data.token.safeTransferFrom( + address(this), erc1155Data.recipient, erc1155Data.tokenId, erc1155Data.amount, erc1155Data.data + ); + } + + /// @notice Batch transfer ERC1155 tokens of a single ERC1155 collection to recipients. + /// @param erc1155BatchData The data of the ERC1155 batch transfer. + function batchTransferSingleERC1155(ERC1155BatchData calldata erc1155BatchData) public onlyLlama { + if (erc1155BatchData.recipient == address(0)) revert ZeroAddressNotAllowed(); + erc1155BatchData.token.safeBatchTransferFrom( + address(this), + erc1155BatchData.recipient, + erc1155BatchData.tokenIds, + erc1155BatchData.amounts, + erc1155BatchData.data + ); + } + + /// @notice Batch transfer ERC1155 tokens of multiple ERC1155 collections to recipients. + /// @param erc1155BatchData The data of the ERC1155 batch transfers. + function batchTransferMultipleERC1155(ERC1155BatchData[] calldata erc1155BatchData) external onlyLlama { + uint256 length = erc1155BatchData.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + batchTransferSingleERC1155(erc1155BatchData[i]); + } + } + + /// @notice Grant an ERC1155 operator allowance to recipients. + /// @param erc1155OperatorData The data of the ERC1155 operator allowance. + function approveOperatorERC1155(ERC1155OperatorData calldata erc1155OperatorData) public onlyLlama { + erc1155OperatorData.token.setApprovalForAll(erc1155OperatorData.recipient, erc1155OperatorData.approved); + } + + /// @notice Batch approve ERC1155 operator allowances to recipients. + /// @param erc1155OperatorData The data of the ERC1155 operator allowances. + function batchApproveOperatorERC1155(ERC1155OperatorData[] calldata erc1155OperatorData) external onlyLlama { + uint256 length = erc1155OperatorData.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + approveOperatorERC1155(erc1155OperatorData[i]); + } + } + + // -------- Voting Tokens -------- + + /// @notice Delegate voting tokens to a delegatee. + /// @param tokenDelegateData The `token` and `delegatee` for the delegation. + function delegateToken(TokenDelegateData calldata tokenDelegateData) public onlyLlama { + tokenDelegateData.token.delegate(tokenDelegateData.delegatee); + } + + /// @notice Batch delegate multiple voting tokens to delegatees. + /// @param tokenDelegateData The `token` and `delegatee` for the delegations. + function batchDelegateToken(TokenDelegateData[] calldata tokenDelegateData) external onlyLlama { + uint256 length = tokenDelegateData.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + delegateToken(tokenDelegateData[i]); + } + } + + // -------- Generic Execution -------- + + /// @notice Execute arbitrary calls from the Llama Account. + /// @dev Be careful and intentional while assigning permissions to a policyholder that can create an action to call + /// this function, especially while using the delegatecall functionality as it can lead to arbitrary code execution in + /// the context of this Llama account. + /// @param target The address of the contract to call. + /// @param withDelegatecall Whether to use delegatecall or call. + /// @param value The amount of ETH to send with the call, taken from the Llama Account. + /// @param callData The calldata to pass to the contract. + /// @return The result of the call. + function execute(address target, bool withDelegatecall, uint256 value, bytes calldata callData) + external + onlyLlama + returns (bytes memory) + { + bool success; + bytes memory result; + + if (withDelegatecall) { + if (value > 0) revert CannotDelegatecallWithValue(); + + // Whenever we're executing arbitrary code in the context of this account, we want to ensure + // that none of the storage in this contract changes, as this could let someone who sneaks in + // a malicious (or buggy) target to take ownership of this contract. Slot 0 contains all + // relevant storage variables for security, so we check the value before and after execution + // to make sure it's unchanged. The contract name starts in slot 1, but it's not as important + // if that's changed (and it can be changed back), so to save gas we don't check the name. + // The storage layout of this contract is below: + // + // | Variable Name | Type | Slot | Offset | Bytes | + // |---------------|---------|------|--------|-------| + // | _initialized | uint8 | 0 | 0 | 1 | + // | _initializing | bool | 0 | 1 | 1 | + // | llamaExecutor | address | 0 | 2 | 20 | + // | name | string | 1 | 0 | 32 | + + bytes32 originalStorage = _readSlot0(); + (success, result) = target.delegatecall(callData); + if (originalStorage != _readSlot0()) revert Slot0Changed(); + } else { + (success, result) = target.call{value: value}(callData); + } + + if (!success) revert FailedExecution(result); + return result; + } + + // ================================ + // ======== Internal Logic ======== + // ================================ + + /// @dev Reads slot 0 from storage, used to check that storage hasn't changed after delegatecall. + function _readSlot0() internal view returns (bytes32 slot0) { + assembly { + slot0 := sload(0) + } + } +} diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol index 979eb8628..a789c9e69 100644 --- a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -9,7 +9,6 @@ import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; /// @notice A script that allows users to delegate governance tokens in their Llama accounts. contract LlamaAccountTokenDelegationScript is LlamaBaseScript { function delegateAccountTokenToExecutor(LlamaAccount account, address token) external onlyDelegateCall { - // TODO: add checks for isERC20Votes or isERC721Votes token and isLlamaAccount account.execute(token, false, 0, abi.encodeWithSelector(0x5c19a95c, address(this))); } } From 083a6ac4dc6a04669bd41b3e6a22db47417c15f9 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 17:58:37 -0500 Subject: [PATCH 04/19] script --- src/accounts/LlamaAccountWithDelegation.sol | 334 +----------------- .../LlamaAccountTokenDelegationScript.sol | 23 +- 2 files changed, 22 insertions(+), 335 deletions(-) diff --git a/src/accounts/LlamaAccountWithDelegation.sol b/src/accounts/LlamaAccountWithDelegation.sol index 7f22a1a48..90e77af8e 100644 --- a/src/accounts/LlamaAccountWithDelegation.sol +++ b/src/accounts/LlamaAccountWithDelegation.sol @@ -1,133 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; -import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; -import {IERC721} from "@openzeppelin/token/ERC721/IERC721.sol"; -import {ERC721Holder} from "@openzeppelin/token/ERC721/utils/ERC721Holder.sol"; -import {IERC1155} from "@openzeppelin/token/ERC1155/IERC1155.sol"; -import {ERC1155Holder} from "@openzeppelin/token/ERC1155/utils/ERC1155Holder.sol"; import {Address} from "@openzeppelin/utils/Address.sol"; -import {ILlamaAccount} from "src/interfaces/ILlamaAccount.sol"; +import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; import {LlamaUtils} from "src/lib/LlamaUtils.sol"; -import {LlamaCore} from "src/LlamaCore.sol"; /// @title Llama Account With Delegation /// @author Llama (devsdosomething@llama.xyz) /// @notice This contract is the standard LlamaAccount with an additional delegate and batch delegate function for /// voting tokens. -contract LlamaAccountWithDelegation is ILlamaAccount, ERC721Holder, ERC1155Holder, Initializable { - using SafeERC20 for IERC20; +contract LlamaAccountWithDelegation is LlamaAccount { using Address for address payable; - // ========================= - // ======== Structs ======== - // ========================= - - /// @dev Llama account initialization configuration. - struct Config { - string name; // Name of the Llama account. - } - - /// @dev Data for sending native tokens to recipients. - struct NativeTokenData { - address payable recipient; // Recipient of the native tokens. - uint256 amount; // Amount of native tokens to send. - } - - /// @dev Data for sending ERC20 tokens to recipients. - struct ERC20Data { - IERC20 token; // The ERC20 token to transfer. - address recipient; // The address to transfer the token to. - uint256 amount; // The amount of tokens to transfer. - } - - /// @dev Data for sending ERC721 tokens to recipients. - struct ERC721Data { - IERC721 token; // The ERC721 token to transfer. - address recipient; // The address to transfer the token to. - uint256 tokenId; // The tokenId of the token to transfer. - } - - /// @dev Data for operator allowance for ERC721 transfers. - struct ERC721OperatorData { - IERC721 token; // The ERC721 token to transfer. - address recipient; // The address to transfer the token to. - bool approved; // Whether to approve or revoke allowance. - } - - /// @dev Data for sending ERC1155 tokens to recipients. - struct ERC1155Data { - IERC1155 token; // The ERC1155 token to transfer. - address recipient; // The address to transfer the token to. - uint256 tokenId; // The tokenId of the token to transfer. - uint256 amount; // The amount of tokens to transfer. - bytes data; // The data to pass to the ERC1155 token. - } - - /// @dev Data for batch sending ERC1155 tokens to recipients. - struct ERC1155BatchData { - IERC1155 token; // The ERC1155 token to transfer. - address recipient; // The address to transfer the token to. - uint256[] tokenIds; // The tokenId of the token to transfer. - uint256[] amounts; // The amount of tokens to transfer. - bytes data; // The data to pass to the ERC1155 token. - } - - /// @dev Data for operator allowance for ERC1155 transfers. - struct ERC1155OperatorData { - IERC1155 token; // The ERC1155 token to transfer. - address recipient; // The address to transfer the token to. - bool approved; // Whether to approve or revoke allowance. - } - /// @dev Data for delegating voting tokens to delegatees. struct TokenDelegateData { IVotes token; // The voting token to delegate. address delegatee; // The address being delegated to. } - // ====================================== - // ======== Errors and Modifiers ======== - // ====================================== - - /// @dev Only callable by a Llama instance's executor. - error OnlyLlama(); - - /// @dev Recipient cannot be the 0 address. - error ZeroAddressNotAllowed(); - - /// @dev External call failed. - /// @param result Data returned by the called function. - error FailedExecution(bytes result); - - /// @dev Slot 0 cannot be changed as a result of delegatecalls. - error Slot0Changed(); - - /// @dev Value cannot be sent with delegatecalls. - error CannotDelegatecallWithValue(); - - /// @dev Checks that the caller is the Llama executor and reverts if not. - modifier onlyLlama() { - if (msg.sender != llamaExecutor) revert OnlyLlama(); - _; - } - - // =================================== - // ======== Storage Variables ======== - // =================================== - - /// @notice The Llama instance's executor. - /// @dev We intentionally put this before the `name` so it's packed with the `Initializable` - /// storage variables, that way we can only check one slot before and after a delegatecall. - address public llamaExecutor; - - /// @notice Name of the Llama account. - string public name; - // ====================================================== // ======== Contract Creation and Initialization ======== // ====================================================== @@ -138,169 +30,6 @@ contract LlamaAccountWithDelegation is ILlamaAccount, ERC721Holder, ERC1155Holde _disableInitializers(); } - /// @inheritdoc ILlamaAccount - function initialize(bytes memory config) external initializer returns (bool) { - llamaExecutor = address(LlamaCore(msg.sender).executor()); - Config memory accountConfig = abi.decode(config, (Config)); - name = accountConfig.name; - - return true; - } - - // =========================================== - // ======== External and Public Logic ======== - // =========================================== - - // -------- Native Token -------- - - /// @notice Enables the Llama account to receive native tokens. - receive() external payable {} - - /// @notice Transfer native tokens to a recipient. - /// @param nativeTokenData The `amount` and `recipient` of the native token transfer. - function transferNativeToken(NativeTokenData calldata nativeTokenData) public onlyLlama { - if (nativeTokenData.recipient == address(0)) revert ZeroAddressNotAllowed(); - nativeTokenData.recipient.sendValue(nativeTokenData.amount); - } - - /// @notice Batch transfer native tokens to a recipient. - /// @param nativeTokenData The `amounts` and `recipients` for the native token transfers. - function batchTransferNativeToken(NativeTokenData[] calldata nativeTokenData) external onlyLlama { - uint256 length = nativeTokenData.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - transferNativeToken(nativeTokenData[i]); - } - } - - // -------- ERC20 Token -------- - - /// @notice Transfer ERC20 tokens to a recipient. - /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 transfer. - function transferERC20(ERC20Data calldata erc20Data) public onlyLlama { - if (erc20Data.recipient == address(0)) revert ZeroAddressNotAllowed(); - erc20Data.token.safeTransfer(erc20Data.recipient, erc20Data.amount); - } - - /// @notice Batch transfer ERC20 tokens to recipients. - /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 transfers. - function batchTransferERC20(ERC20Data[] calldata erc20Data) external onlyLlama { - uint256 length = erc20Data.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - transferERC20(erc20Data[i]); - } - } - - /// @notice Approve an ERC20 allowance for a recipient. - /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 approval. - function approveERC20(ERC20Data calldata erc20Data) public onlyLlama { - erc20Data.token.safeApprove(erc20Data.recipient, erc20Data.amount); - } - - /// @notice Batch approve ERC20 allowances for recipients. - /// @param erc20Data The `token`, `recipient`, and `amount` for the ERC20 approvals. - function batchApproveERC20(ERC20Data[] calldata erc20Data) external onlyLlama { - uint256 length = erc20Data.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - approveERC20(erc20Data[i]); - } - } - - // -------- ERC721 Token -------- - - /// @notice Transfer an ERC721 token to a recipient. - /// @param erc721Data The `token`, `recipient`, and `tokenId` of the ERC721 transfer. - function transferERC721(ERC721Data calldata erc721Data) public onlyLlama { - if (erc721Data.recipient == address(0)) revert ZeroAddressNotAllowed(); - erc721Data.token.transferFrom(address(this), erc721Data.recipient, erc721Data.tokenId); - } - - /// @notice Batch transfer ERC721 tokens to recipients. - /// @param erc721Data The `token`, `recipient`, and `tokenId` of the ERC721 transfers. - function batchTransferERC721(ERC721Data[] calldata erc721Data) external onlyLlama { - uint256 length = erc721Data.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - transferERC721(erc721Data[i]); - } - } - - /// @notice Approve a recipient to transfer an ERC721. - /// @param erc721Data The `token`, `recipient`, and `tokenId` of the ERC721 approval. - function approveERC721(ERC721Data calldata erc721Data) public onlyLlama { - erc721Data.token.approve(erc721Data.recipient, erc721Data.tokenId); - } - - /// @notice Batch approve recipients to transfer ERC721s. - /// @param erc721Data The `token`, `recipient`, and `tokenId` for the ERC721 approvals. - function batchApproveERC721(ERC721Data[] calldata erc721Data) external onlyLlama { - uint256 length = erc721Data.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - approveERC721(erc721Data[i]); - } - } - - /// @notice Approve an operator for ERC721 transfers. - /// @param erc721OperatorData The `token`, `recipient`, and `approved` boolean for the ERC721 operator approval. - function approveOperatorERC721(ERC721OperatorData calldata erc721OperatorData) public onlyLlama { - erc721OperatorData.token.setApprovalForAll(erc721OperatorData.recipient, erc721OperatorData.approved); - } - - /// @notice Batch approve operators for ERC721 transfers. - /// @param erc721OperatorData The `token`, `recipient`, and `approved` booleans for the ERC721 operator approvals. - function batchApproveOperatorERC721(ERC721OperatorData[] calldata erc721OperatorData) external onlyLlama { - uint256 length = erc721OperatorData.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - approveOperatorERC721(erc721OperatorData[i]); - } - } - - // -------- ERC1155 Token -------- - - /// @notice Transfer ERC1155 tokens to a recipient. - /// @param erc1155Data The data of the ERC1155 transfer. - function transferERC1155(ERC1155Data calldata erc1155Data) external onlyLlama { - if (erc1155Data.recipient == address(0)) revert ZeroAddressNotAllowed(); - erc1155Data.token.safeTransferFrom( - address(this), erc1155Data.recipient, erc1155Data.tokenId, erc1155Data.amount, erc1155Data.data - ); - } - - /// @notice Batch transfer ERC1155 tokens of a single ERC1155 collection to recipients. - /// @param erc1155BatchData The data of the ERC1155 batch transfer. - function batchTransferSingleERC1155(ERC1155BatchData calldata erc1155BatchData) public onlyLlama { - if (erc1155BatchData.recipient == address(0)) revert ZeroAddressNotAllowed(); - erc1155BatchData.token.safeBatchTransferFrom( - address(this), - erc1155BatchData.recipient, - erc1155BatchData.tokenIds, - erc1155BatchData.amounts, - erc1155BatchData.data - ); - } - - /// @notice Batch transfer ERC1155 tokens of multiple ERC1155 collections to recipients. - /// @param erc1155BatchData The data of the ERC1155 batch transfers. - function batchTransferMultipleERC1155(ERC1155BatchData[] calldata erc1155BatchData) external onlyLlama { - uint256 length = erc1155BatchData.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - batchTransferSingleERC1155(erc1155BatchData[i]); - } - } - - /// @notice Grant an ERC1155 operator allowance to recipients. - /// @param erc1155OperatorData The data of the ERC1155 operator allowance. - function approveOperatorERC1155(ERC1155OperatorData calldata erc1155OperatorData) public onlyLlama { - erc1155OperatorData.token.setApprovalForAll(erc1155OperatorData.recipient, erc1155OperatorData.approved); - } - - /// @notice Batch approve ERC1155 operator allowances to recipients. - /// @param erc1155OperatorData The data of the ERC1155 operator allowances. - function batchApproveOperatorERC1155(ERC1155OperatorData[] calldata erc1155OperatorData) external onlyLlama { - uint256 length = erc1155OperatorData.length; - for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - approveOperatorERC1155(erc1155OperatorData[i]); - } - } - // -------- Voting Tokens -------- /// @notice Delegate voting tokens to a delegatee. @@ -317,63 +46,4 @@ contract LlamaAccountWithDelegation is ILlamaAccount, ERC721Holder, ERC1155Holde delegateToken(tokenDelegateData[i]); } } - - // -------- Generic Execution -------- - - /// @notice Execute arbitrary calls from the Llama Account. - /// @dev Be careful and intentional while assigning permissions to a policyholder that can create an action to call - /// this function, especially while using the delegatecall functionality as it can lead to arbitrary code execution in - /// the context of this Llama account. - /// @param target The address of the contract to call. - /// @param withDelegatecall Whether to use delegatecall or call. - /// @param value The amount of ETH to send with the call, taken from the Llama Account. - /// @param callData The calldata to pass to the contract. - /// @return The result of the call. - function execute(address target, bool withDelegatecall, uint256 value, bytes calldata callData) - external - onlyLlama - returns (bytes memory) - { - bool success; - bytes memory result; - - if (withDelegatecall) { - if (value > 0) revert CannotDelegatecallWithValue(); - - // Whenever we're executing arbitrary code in the context of this account, we want to ensure - // that none of the storage in this contract changes, as this could let someone who sneaks in - // a malicious (or buggy) target to take ownership of this contract. Slot 0 contains all - // relevant storage variables for security, so we check the value before and after execution - // to make sure it's unchanged. The contract name starts in slot 1, but it's not as important - // if that's changed (and it can be changed back), so to save gas we don't check the name. - // The storage layout of this contract is below: - // - // | Variable Name | Type | Slot | Offset | Bytes | - // |---------------|---------|------|--------|-------| - // | _initialized | uint8 | 0 | 0 | 1 | - // | _initializing | bool | 0 | 1 | 1 | - // | llamaExecutor | address | 0 | 2 | 20 | - // | name | string | 1 | 0 | 32 | - - bytes32 originalStorage = _readSlot0(); - (success, result) = target.delegatecall(callData); - if (originalStorage != _readSlot0()) revert Slot0Changed(); - } else { - (success, result) = target.call{value: value}(callData); - } - - if (!success) revert FailedExecution(result); - return result; - } - - // ================================ - // ======== Internal Logic ======== - // ================================ - - /// @dev Reads slot 0 from storage, used to check that storage hasn't changed after delegatecall. - function _readSlot0() internal view returns (bytes32 slot0) { - assembly { - slot0 := sload(0) - } - } } diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol index a789c9e69..5b062db8e 100644 --- a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -6,9 +6,26 @@ import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; /// @title Llama Account Token Delegation Script /// @author Llama (devsdosomething@llama.xyz) -/// @notice A script that allows users to delegate governance tokens in their Llama accounts. +/// @notice A script that leverages the `LlamaAccount` arbitrary execute function to allow users to delegate governance +/// tokens from their Llama accounts. contract LlamaAccountTokenDelegationScript is LlamaBaseScript { - function delegateAccountTokenToExecutor(LlamaAccount account, address token) external onlyDelegateCall { - account.execute(token, false, 0, abi.encodeWithSelector(0x5c19a95c, address(this))); + /// @notice Token contract must be called not delegatecalled. + bool internal constant WITH_DELEGATECALL = false; + + /// @notice `msg.value` is 0. + uint256 internal constant VALUE = 0; + + /// @notice The function selector of the `IVotes` `delegate(address)` function. + bytes4 internal constant DELEGATE_SELECTOR = 0x5c19a95c; + + function delegateTokenFromAccount(LlamaAccount account, address token, address delegatee) external onlyDelegateCall { + account.execute(token, WITH_DELEGATECALL, VALUE, abi.encodeWithSelector(DELEGATE_SELECTOR, delegatee)); + } + + function delegateTokensFromAccount(LlamaAccount account, address[] tokens, address delegatee) + external + onlyDelegateCall + { + account.execute(token, WITH_DELEGATECALL, VALUE, abi.encodeWithSelector(DELEGATE_SELECTOR, delegatee)); } } From 72fa9aa4cc62a385e3f359efcf5d45635e6925ad Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 18:28:15 -0500 Subject: [PATCH 05/19] finish contracts --- src/accounts/LlamaAccountWithDelegation.sol | 9 +++- .../LlamaAccountTokenDelegationScript.sol | 46 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/accounts/LlamaAccountWithDelegation.sol b/src/accounts/LlamaAccountWithDelegation.sol index 90e77af8e..2c7a39aaf 100644 --- a/src/accounts/LlamaAccountWithDelegation.sol +++ b/src/accounts/LlamaAccountWithDelegation.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.19; import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; -import {Address} from "@openzeppelin/utils/Address.sol"; import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; import {LlamaUtils} from "src/lib/LlamaUtils.sol"; @@ -12,7 +11,9 @@ import {LlamaUtils} from "src/lib/LlamaUtils.sol"; /// @notice This contract is the standard LlamaAccount with an additional delegate and batch delegate function for /// voting tokens. contract LlamaAccountWithDelegation is LlamaAccount { - using Address for address payable; + // ========================== + // ========= Structs ======== + // ========================== /// @dev Data for delegating voting tokens to delegatees. struct TokenDelegateData { @@ -30,6 +31,10 @@ contract LlamaAccountWithDelegation is LlamaAccount { _disableInitializers(); } + // =========================================== + // ======== External and Public Logic ======== + // =========================================== + // -------- Voting Tokens -------- /// @notice Delegate voting tokens to a delegatee. diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol index 5b062db8e..ce47bdf33 100644 --- a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -1,14 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.19; -import {LlamaBaseScript} from "src/llama-scripts/LlamaBaseScript.sol"; +import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; + import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; +import {LlamaUtils} from "src/lib/LlamaUtils.sol"; +import {LlamaBaseScript} from "src/llama-scripts/LlamaBaseScript.sol"; /// @title Llama Account Token Delegation Script /// @author Llama (devsdosomething@llama.xyz) -/// @notice A script that leverages the `LlamaAccount` arbitrary execute function to allow users to delegate governance +/// @notice A script that leverages the Llama account arbitrary execute function to allow users to delegate governance /// tokens from their Llama accounts. contract LlamaAccountTokenDelegationScript is LlamaBaseScript { + // ========================== + // ========= Structs ======== + // ========================== + + /// @dev Data for delegating voting tokens from an account to a delegatee. + struct AccountTokenDelegateData { + LlamaAccount account; // The account being delegated from. + IVotes token; // The voting token to delegate. + address delegatee; // The address being delegated to. + } + + // ============================= + // ========= Constants ========= + // ============================= + /// @notice Token contract must be called not delegatecalled. bool internal constant WITH_DELEGATECALL = false; @@ -18,14 +36,30 @@ contract LlamaAccountTokenDelegationScript is LlamaBaseScript { /// @notice The function selector of the `IVotes` `delegate(address)` function. bytes4 internal constant DELEGATE_SELECTOR = 0x5c19a95c; - function delegateTokenFromAccount(LlamaAccount account, address token, address delegatee) external onlyDelegateCall { - account.execute(token, WITH_DELEGATECALL, VALUE, abi.encodeWithSelector(DELEGATE_SELECTOR, delegatee)); + // ======================================== + // ======= Delegate token functions ======= + // ======================================== + + /// @notice Delegate voting tokens from an account to a delegatee. + /// @param account The Llama account that holds the voting tokens. + /// @param token The address of the token contract. + /// @param delegatee The address of the delegatee. + function delegateTokenFromAccount(LlamaAccount account, IVotes token, address delegatee) public onlyDelegateCall { + account.execute(address(token), WITH_DELEGATECALL, VALUE, abi.encodeWithSelector(DELEGATE_SELECTOR, delegatee)); } - function delegateTokensFromAccount(LlamaAccount account, address[] tokens, address delegatee) + /// @notice Delegate multiple voting tokens from an account to a delegatee. + /// @param accountTokenDelegateData The Llama account that holds the voting tokens, the token address, and the + /// delegatee address. + function delegateTokensFromAccount(AccountTokenDelegateData[] calldata accountTokenDelegateData) external onlyDelegateCall { - account.execute(token, WITH_DELEGATECALL, VALUE, abi.encodeWithSelector(DELEGATE_SELECTOR, delegatee)); + uint256 length = accountTokenDelegateData.length; + for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { + delegateTokenFromAccount( + accountTokenDelegateData[i].account, accountTokenDelegateData[i].token, accountTokenDelegateData[i].delegatee + ); + } } } From 3a4bae740fd60d2ec99b3268140fef27b88e7636 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 18:47:13 -0500 Subject: [PATCH 06/19] testing --- .../LlamaAccountTokenDelegationScript.sol | 18 +++++++++++------ .../LlamaAccountTokenDelegationScript.t.sol | 20 ++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol index ce47bdf33..171286b94 100644 --- a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -41,11 +41,15 @@ contract LlamaAccountTokenDelegationScript is LlamaBaseScript { // ======================================== /// @notice Delegate voting tokens from an account to a delegatee. - /// @param account The Llama account that holds the voting tokens. - /// @param token The address of the token contract. - /// @param delegatee The address of the delegatee. - function delegateTokenFromAccount(LlamaAccount account, IVotes token, address delegatee) public onlyDelegateCall { - account.execute(address(token), WITH_DELEGATECALL, VALUE, abi.encodeWithSelector(DELEGATE_SELECTOR, delegatee)); + /// @param accountTokenDelegateData A struct that contains the Llama account that holds the voting tokens, the address + /// of the token contract, and the address of the delegatee. + function delegateTokenFromAccount(AccountTokenDelegateData memory accountTokenDelegateData) public onlyDelegateCall { + accountTokenDelegateData.account.execute( + address(accountTokenDelegateData.token), + WITH_DELEGATECALL, + VALUE, + abi.encodeWithSelector(DELEGATE_SELECTOR, accountTokenDelegateData.delegatee) + ); } /// @notice Delegate multiple voting tokens from an account to a delegatee. @@ -58,7 +62,9 @@ contract LlamaAccountTokenDelegationScript is LlamaBaseScript { uint256 length = accountTokenDelegateData.length; for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { delegateTokenFromAccount( - accountTokenDelegateData[i].account, accountTokenDelegateData[i].token, accountTokenDelegateData[i].delegatee + AccountTokenDelegateData( + accountTokenDelegateData[i].account, accountTokenDelegateData[i].token, accountTokenDelegateData[i].delegatee + ) ); } } diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index 9c332ddfc..2df88c9b9 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -31,9 +31,7 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { delegationScriptAddress = address(new LlamaAccountTokenDelegationScript()); delegateTokensPermission = PermissionData( - address(delegationScriptAddress), - LlamaAccountTokenDelegationScript.delegateAccountTokenToExecutor.selector, - mpStrategy1 + address(delegationScriptAddress), LlamaAccountTokenDelegationScript.delegateTokenFromAccount.selector, mpStrategy1 ); vm.startPrank(address(mpExecutor)); @@ -44,11 +42,10 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { } contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { - function executeDelegateTokensAction() internal { - bytes memory data = abi.encodeCall( - LlamaAccountTokenDelegationScript.delegateAccountTokenToExecutor, - (LlamaAccount(payable(address(mpAccount1))), address(UNI)) - ); + function executeDelegateTokensAction( + LlamaAccountTokenDelegationScript.AccountTokenDelegateData memory tokenDelegateData + ) internal { + bytes memory data = abi.encodeCall(LlamaAccountTokenDelegationScript.delegateTokenFromAccount, (tokenDelegateData)); vm.prank(actionCreatorAaron); uint256 actionId = @@ -78,7 +75,12 @@ contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); - executeDelegateTokensAction(); + LlamaAccountTokenDelegationScript.AccountTokenDelegateData memory tokenDelegateData = + LlamaAccountTokenDelegationScript.AccountTokenDelegateData( + LlamaAccount(payable(address(mpAccount1))), IVotes(address(UNI)), address(mpExecutor) + ); + + executeDelegateTokensAction(tokenDelegateData); // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); From ee922769e3059985319f6eb5e02980c0b3f061ef Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 19:36:33 -0500 Subject: [PATCH 07/19] tests --- .../LlamaAccountTokenDelegationScript.t.sol | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index 2df88c9b9..42dcc8288 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -20,6 +20,7 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { address delegationScriptAddress; + PermissionData public delegateTokenPermission; PermissionData public delegateTokensPermission; function setUp() public virtual override { @@ -27,22 +28,30 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { LlamaTestSetup.setUp(); deal(address(UNI), address(mpAccount1), UNI_AMOUNT); + deal(address(UNI), address(mpAccount2), UNI_AMOUNT); delegationScriptAddress = address(new LlamaAccountTokenDelegationScript()); - delegateTokensPermission = PermissionData( + delegateTokenPermission = PermissionData( address(delegationScriptAddress), LlamaAccountTokenDelegationScript.delegateTokenFromAccount.selector, mpStrategy1 ); + delegateTokensPermission = PermissionData( + address(delegationScriptAddress), + LlamaAccountTokenDelegationScript.delegateTokensFromAccount.selector, + mpStrategy1 + ); + vm.startPrank(address(mpExecutor)); mpCore.setScriptAuthorization(address(delegationScriptAddress), true); + mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokenPermission, true); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokensPermission, true); vm.stopPrank(); } } contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { - function executeDelegateTokensAction( + function executeDelegateTokenAction( LlamaAccountTokenDelegationScript.AccountTokenDelegateData memory tokenDelegateData ) internal { bytes memory data = abi.encodeCall(LlamaAccountTokenDelegationScript.delegateTokenFromAccount, (tokenDelegateData)); @@ -70,6 +79,34 @@ contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { mpCore.executeAction(actionInfo); } + function executeDelegateTokensAction( + LlamaAccountTokenDelegationScript.AccountTokenDelegateData[] memory tokenDelegateData + ) internal { + bytes memory data = abi.encodeCall(LlamaAccountTokenDelegationScript.delegateTokensFromAccount, (tokenDelegateData)); + + vm.prank(actionCreatorAaron); + uint256 actionId = + mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data, ""); + + ActionInfo memory actionInfo = ActionInfo( + actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data + ); + vm.warp(block.timestamp + 1); + + vm.prank(approverAdam); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + vm.prank(approverAlicia); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + + vm.warp(block.timestamp + 6 days); + + mpCore.queueAction(actionInfo); + + vm.warp(block.timestamp + 5 days); + + mpCore.executeAction(actionInfo); + } + function test_TokensDelegatedToExecutor() external { // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); @@ -80,10 +117,55 @@ contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { LlamaAccount(payable(address(mpAccount1))), IVotes(address(UNI)), address(mpExecutor) ); - executeDelegateTokensAction(tokenDelegateData); + executeDelegateTokenAction(tokenDelegateData); // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); } + + function test_TokensDelegatedToAnyAddress(address delegatee) external { + // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + + LlamaAccountTokenDelegationScript.AccountTokenDelegateData memory tokenDelegateData = + LlamaAccountTokenDelegationScript.AccountTokenDelegateData( + LlamaAccount(payable(address(mpAccount1))), IVotes(address(UNI)), delegatee + ); + + executeDelegateTokenAction(tokenDelegateData); + + // After the action executes the account should still have 1,000 UNI tokens but the delegate is the delegatee + // address + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee); + } + + function test_DelegateTokensFromAccounts(address delegatee1, address delegatee2) external { + // Assert that the accounts have 1,000 UNI tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(UNI.balanceOf(address(mpAccount2)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount2)), address(0)); + + LlamaAccountTokenDelegationScript.AccountTokenDelegateData[] memory tokenDelegateData = + new LlamaAccountTokenDelegationScript.AccountTokenDelegateData[](2); + + for (uint256 i = 0; i < 2; i = i++) { + tokenDelegateData[i] = LlamaAccountTokenDelegationScript.AccountTokenDelegateData( + LlamaAccount(payable(address(i % 2 == 0 ? mpAccount1 : mpAccount2))), + IVotes(address(UNI)), + i % 2 == 0 ? delegatee1 : delegatee2 + ); + } + + executeDelegateTokensAction(tokenDelegateData); + + // After the action executes the accounts should still have 1,000 UNI tokens but the delegate is the delegatee + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(UNI.balanceOf(address(mpAccount2)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee1); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount2)), delegatee2); + } } From 9798a5ff91a433e10e4112d6eb733eba41378159 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 19:40:33 -0500 Subject: [PATCH 08/19] testing --- test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index 42dcc8288..ee86c289d 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -9,13 +9,13 @@ import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; +import {LlamaUtils} from "src/lib/LlamaUtils.sol"; import {Roles, LlamaTestSetup} from "test/utils/LlamaTestSetup.sol"; import {LlamaAccountTokenDelegationScript} from "src/llama-scripts/LlamaAccountTokenDelegationScript.sol"; import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { IERC20 public constant UNI = IERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); - address public constant UNI_WHALE = 0x878f0822A9e77c1dD7883E543747147Be8D63C3B; uint256 public constant UNI_AMOUNT = 1000e18; address delegationScriptAddress; @@ -152,7 +152,7 @@ contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { LlamaAccountTokenDelegationScript.AccountTokenDelegateData[] memory tokenDelegateData = new LlamaAccountTokenDelegationScript.AccountTokenDelegateData[](2); - for (uint256 i = 0; i < 2; i = i++) { + for (uint256 i = 0; i < 2; LlamaUtils.uncheckedIncrement(i)) { tokenDelegateData[i] = LlamaAccountTokenDelegationScript.AccountTokenDelegateData( LlamaAccount(payable(address(i % 2 == 0 ? mpAccount1 : mpAccount2))), IVotes(address(UNI)), From 7d9565b351bb8673cd2208d3758db28e29ebc6fc Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 19:49:45 -0500 Subject: [PATCH 09/19] testing --- script/DeployLlamaFactory.s.sol | 8 +++ .../accounts/LlamaAccountWithDelegation.t.sol | 46 ++++++++++++++ .../LlamaAccountTokenDelegationScript.t.sol | 60 ++++++++++--------- 3 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 test/accounts/LlamaAccountWithDelegation.t.sol diff --git a/script/DeployLlamaFactory.s.sol b/script/DeployLlamaFactory.s.sol index 18aec2988..8fd219960 100644 --- a/script/DeployLlamaFactory.s.sol +++ b/script/DeployLlamaFactory.s.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.19; import {Script, stdJson} from "forge-std/Script.sol"; import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; +import {LlamaAccountWithDelegation} from "src/accounts/LlamaAccountWithDelegation.sol"; import {LlamaCore} from "src/LlamaCore.sol"; import {LlamaFactory} from "src/LlamaFactory.sol"; import {LlamaLens} from "src/LlamaLens.sol"; @@ -28,6 +29,7 @@ contract DeployLlamaFactory is Script { LlamaAbsolutePeerReview absolutePeerReviewLogic; LlamaAbsoluteQuorum absoluteQuorumLogic; LlamaAccount accountLogic; + LlamaAccountWithDelegation accountWithDelegationLogic; LlamaPolicy policyLogic; LlamaPolicyMetadata policyMetadataLogic; @@ -125,5 +127,11 @@ contract DeployLlamaFactory is Script { vm.broadcast(); governanceScript = new LlamaGovernanceScript(); DeployUtils.print(string.concat(" LlamaGovernanceScript:", vm.toString(address(governanceScript)))); + + vm.broadcast(); + accountWithDelegationLogic = new LlamaAccountWithDelegation(); + DeployUtils.print( + string.concat(" LlamaAccountWithDelegationLogic:", vm.toString(address(accountWithDelegationLogic))) + ); } } diff --git a/test/accounts/LlamaAccountWithDelegation.t.sol b/test/accounts/LlamaAccountWithDelegation.t.sol new file mode 100644 index 000000000..09db1a487 --- /dev/null +++ b/test/accounts/LlamaAccountWithDelegation.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Test, console2} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; +import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; + +import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; +import {LlamaAccountWithDelegation} from "src/accounts/LlamaAccountWithDelegation.sol"; +import {LlamaUtils} from "src/lib/LlamaUtils.sol"; +import {Roles, LlamaTestSetup} from "test/utils/LlamaTestSetup.sol"; +import {LlamaAccountTokenDelegationScript} from "src/llama-scripts/LlamaAccountTokenDelegationScript.sol"; +import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; + +contract LlamaAccountWithDelegationTest is LlamaTestSetup { + IERC20 public constant UNI = IERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); + uint256 public constant UNI_AMOUNT = 1000e18; + + PermissionData public delegateTokenPermission; + PermissionData public batchDelegateTokenPermission; + + function setUp() public virtual override { + vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); + LlamaTestSetup.setUp(); + + deal(address(UNI), address(mpAccount1), UNI_AMOUNT); + + delegateTokenPermission = + PermissionData(address(mpAccount1), LlamaAccountWithDelegation.delegateToken.selector, mpStrategy1); + + batchDelegateTokenPermission = + PermissionData(address(mpAccount1), LlamaAccountWithDelegation.batchDelegateToken.selector, mpStrategy1); + + vm.startPrank(address(mpExecutor)); + mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokenPermission, true); + mpPolicy.setRolePermission(uint8(Roles.ActionCreator), batchDelegateTokenPermission, true); + vm.stopPrank(); + } +} + +contract DelegateToken is LlamaAccountWithDelegationTest {} + +contract BatchDelegateToken is LlamaAccountWithDelegationTest {} diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index ee86c289d..8f40ffbaf 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -50,7 +50,7 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { } } -contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { +contract DelegateTokenFromAccount is LlamaAccountTokenDelegationScriptTest { function executeDelegateTokenAction( LlamaAccountTokenDelegationScript.AccountTokenDelegateData memory tokenDelegateData ) internal { @@ -79,34 +79,6 @@ contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { mpCore.executeAction(actionInfo); } - function executeDelegateTokensAction( - LlamaAccountTokenDelegationScript.AccountTokenDelegateData[] memory tokenDelegateData - ) internal { - bytes memory data = abi.encodeCall(LlamaAccountTokenDelegationScript.delegateTokensFromAccount, (tokenDelegateData)); - - vm.prank(actionCreatorAaron); - uint256 actionId = - mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data, ""); - - ActionInfo memory actionInfo = ActionInfo( - actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data - ); - vm.warp(block.timestamp + 1); - - vm.prank(approverAdam); - mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); - vm.prank(approverAlicia); - mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); - - vm.warp(block.timestamp + 6 days); - - mpCore.queueAction(actionInfo); - - vm.warp(block.timestamp + 5 days); - - mpCore.executeAction(actionInfo); - } - function test_TokensDelegatedToExecutor() external { // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); @@ -141,6 +113,36 @@ contract DelegateToExecutor is LlamaAccountTokenDelegationScriptTest { assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee); } +} + +contract DelegateTokensFromAccount is LlamaAccountTokenDelegationScriptTest { + function executeDelegateTokensAction( + LlamaAccountTokenDelegationScript.AccountTokenDelegateData[] memory tokenDelegateData + ) internal { + bytes memory data = abi.encodeCall(LlamaAccountTokenDelegationScript.delegateTokensFromAccount, (tokenDelegateData)); + + vm.prank(actionCreatorAaron); + uint256 actionId = + mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data, ""); + + ActionInfo memory actionInfo = ActionInfo( + actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, delegationScriptAddress, 0, data + ); + vm.warp(block.timestamp + 1); + + vm.prank(approverAdam); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + vm.prank(approverAlicia); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + + vm.warp(block.timestamp + 6 days); + + mpCore.queueAction(actionInfo); + + vm.warp(block.timestamp + 5 days); + + mpCore.executeAction(actionInfo); + } function test_DelegateTokensFromAccounts(address delegatee1, address delegatee2) external { // Assert that the accounts have 1,000 UNI tokens and hasn't delegated them yet From b0351c51420560c5435468a571b1a08158afc484 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 20:52:09 -0500 Subject: [PATCH 10/19] account logic tests --- .../accounts/LlamaAccountWithDelegation.t.sol | 115 +++++++++++++++++- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/test/accounts/LlamaAccountWithDelegation.t.sol b/test/accounts/LlamaAccountWithDelegation.t.sol index 09db1a487..a5f9632d6 100644 --- a/test/accounts/LlamaAccountWithDelegation.t.sol +++ b/test/accounts/LlamaAccountWithDelegation.t.sol @@ -8,16 +8,16 @@ import {stdJson} from "forge-std/StdJson.sol"; import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; -import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; import {LlamaAccountWithDelegation} from "src/accounts/LlamaAccountWithDelegation.sol"; import {LlamaUtils} from "src/lib/LlamaUtils.sol"; import {Roles, LlamaTestSetup} from "test/utils/LlamaTestSetup.sol"; -import {LlamaAccountTokenDelegationScript} from "src/llama-scripts/LlamaAccountTokenDelegationScript.sol"; import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; contract LlamaAccountWithDelegationTest is LlamaTestSetup { IERC20 public constant UNI = IERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); + IERC20 public constant AAVE = IERC20(0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9); uint256 public constant UNI_AMOUNT = 1000e18; + uint256 public constant AAVE_AMOUNT = 28_600e18; PermissionData public delegateTokenPermission; PermissionData public batchDelegateTokenPermission; @@ -27,6 +27,7 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { LlamaTestSetup.setUp(); deal(address(UNI), address(mpAccount1), UNI_AMOUNT); + deal(address(AAVE), address(mpAccount1), AAVE_AMOUNT); delegateTokenPermission = PermissionData(address(mpAccount1), LlamaAccountWithDelegation.delegateToken.selector, mpStrategy1); @@ -41,6 +42,112 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { } } -contract DelegateToken is LlamaAccountWithDelegationTest {} +contract DelegateToken is LlamaAccountWithDelegationTest { + function executeDelegateTokenAction(LlamaAccountWithDelegation.TokenDelegateData memory tokenDelegateData) internal { + bytes memory data = abi.encodeCall(LlamaAccountWithDelegation.delegateToken, (tokenDelegateData)); -contract BatchDelegateToken is LlamaAccountWithDelegationTest {} + vm.prank(actionCreatorAaron); + uint256 actionId = mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data, ""); + + ActionInfo memory actionInfo = + ActionInfo(actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data); + vm.warp(block.timestamp + 1); + + vm.prank(approverAdam); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + vm.prank(approverAlicia); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + + vm.warp(block.timestamp + 6 days); + + mpCore.queueAction(actionInfo); + + vm.warp(block.timestamp + 5 days); + + mpCore.executeAction(actionInfo); + } + + function test_TokensDelegatedToExecutor() external { + // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + + LlamaAccountWithDelegation.TokenDelegateData memory tokenDelegateData = + LlamaAccountWithDelegation.TokenDelegateData(IVotes(address(UNI)), address(mpExecutor)); + + executeDelegateTokenAction(tokenDelegateData); + + // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); + } + + function test_TokensDelegatedToAnyAddress(address delegatee) external { + // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + + LlamaAccountWithDelegation.TokenDelegateData memory tokenDelegateData = + LlamaAccountWithDelegation.TokenDelegateData(IVotes(address(UNI)), delegatee); + + executeDelegateTokenAction(tokenDelegateData); + + // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee); + } +} + +contract BatchDelegateToken is LlamaAccountWithDelegationTest { + function executeBatchDelegateTokenAction(LlamaAccountWithDelegation.TokenDelegateData[] memory tokenDelegateData) + internal + { + bytes memory data = abi.encodeCall(LlamaAccountWithDelegation.batchDelegateToken, (tokenDelegateData)); + + vm.prank(actionCreatorAaron); + uint256 actionId = mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data, ""); + + ActionInfo memory actionInfo = + ActionInfo(actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data); + vm.warp(block.timestamp + 1); + + vm.prank(approverAdam); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + vm.prank(approverAlicia); + mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); + + vm.warp(block.timestamp + 6 days); + + mpCore.queueAction(actionInfo); + + vm.warp(block.timestamp + 5 days); + + mpCore.executeAction(actionInfo); + } + + function test_BatchDelegateTokensFromAccount(address delegatee1, address delegatee2) external { + // Assert that the account has UNI and AAVE tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(AAVE.balanceOf(address(mpAccount1)), 28_600e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + assertEq(IVotes(address(AAVE)).delegates(address(mpAccount1)), address(0)); + + LlamaAccountWithDelegation.TokenDelegateData[] memory tokenDelegateData = + new LlamaAccountWithDelegation.TokenDelegateData[](2); + + for (uint256 i = 0; i < 2; LlamaUtils.uncheckedIncrement(i)) { + tokenDelegateData[i] = LlamaAccountWithDelegation.TokenDelegateData( + i % 2 == 0 ? IVotes(address(UNI)) : IVotes(address(AAVE)), i % 2 == 0 ? delegatee1 : delegatee2 + ); + } + + executeBatchDelegateTokenAction(tokenDelegateData); + + // After the action executes the accounts should still have all UNI and AAVE tokens but the delegate is the + // delegatee + assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(AAVE.balanceOf(address(mpAccount1)), 28_600e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee1); + assertEq(IVotes(address(AAVE)).delegates(address(mpAccount1)), delegatee2); + } +} From d3f2a5e01d2966eebaaa048aa39d2729ba16d6bf Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 21:04:54 -0500 Subject: [PATCH 11/19] Update test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol Co-authored-by: dd0sxx --- test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index 8f40ffbaf..2bda27d47 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -22,7 +22,7 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { PermissionData public delegateTokenPermission; PermissionData public delegateTokensPermission; - + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); function setUp() public virtual override { vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); LlamaTestSetup.setUp(); From f5d8b5606b918034f6b820be2a7ae74a04914cf2 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 21:24:36 -0500 Subject: [PATCH 12/19] tests --- .../accounts/LlamaAccountWithDelegation.t.sol | 83 ++++++++++++------- .../LlamaAccountTokenDelegationScript.t.sol | 9 +- 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/test/accounts/LlamaAccountWithDelegation.t.sol b/test/accounts/LlamaAccountWithDelegation.t.sol index a5f9632d6..f02e197a2 100644 --- a/test/accounts/LlamaAccountWithDelegation.t.sol +++ b/test/accounts/LlamaAccountWithDelegation.t.sol @@ -8,6 +8,10 @@ import {stdJson} from "forge-std/StdJson.sol"; import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {IVotes} from "@openzeppelin/governance/utils/IVotes.sol"; +import {DeployUtils} from "script/DeployUtils.sol"; + +import {ILlamaAccount} from "src/interfaces/ILlamaAccount.sol"; +import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; import {LlamaAccountWithDelegation} from "src/accounts/LlamaAccountWithDelegation.sol"; import {LlamaUtils} from "src/lib/LlamaUtils.sol"; import {Roles, LlamaTestSetup} from "test/utils/LlamaTestSetup.sol"; @@ -15,9 +19,11 @@ import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; contract LlamaAccountWithDelegationTest is LlamaTestSetup { IERC20 public constant UNI = IERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); - IERC20 public constant AAVE = IERC20(0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9); + IERC20 public constant ENS = IERC20(0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72); uint256 public constant UNI_AMOUNT = 1000e18; - uint256 public constant AAVE_AMOUNT = 28_600e18; + uint256 public constant ENS_AMOUNT = 28_600e18; + + ILlamaAccount accountWithDelegation; PermissionData public delegateTokenPermission; PermissionData public batchDelegateTokenPermission; @@ -26,16 +32,25 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); LlamaTestSetup.setUp(); - deal(address(UNI), address(mpAccount1), UNI_AMOUNT); - deal(address(AAVE), address(mpAccount1), AAVE_AMOUNT); + LlamaAccountWithDelegation.Config[] memory newAccount = new LlamaAccountWithDelegation.Config[](3); + newAccount[0] = LlamaAccount.Config({name: "LlamaAccountWithDelegation"}); + + deal(address(UNI), address(accountWithDelegation), UNI_AMOUNT); + deal(address(ENS), address(accountWithDelegation), ENS_AMOUNT); delegateTokenPermission = - PermissionData(address(mpAccount1), LlamaAccountWithDelegation.delegateToken.selector, mpStrategy1); + PermissionData(address(accountWithDelegation), LlamaAccountWithDelegation.delegateToken.selector, mpStrategy1); + + batchDelegateTokenPermission = PermissionData( + address(accountWithDelegation), LlamaAccountWithDelegation.batchDelegateToken.selector, mpStrategy1 + ); - batchDelegateTokenPermission = - PermissionData(address(mpAccount1), LlamaAccountWithDelegation.batchDelegateToken.selector, mpStrategy1); + accountWithDelegation = + lens.computeLlamaAccountAddress(address(accountWithDelegationLogic), abi.encode(newAccount[0]), address(mpCore)); vm.startPrank(address(mpExecutor)); + mpCore.setAccountLogicAuthorization(accountWithDelegationLogic, true); + mpCore.createAccounts(accountWithDelegationLogic, DeployUtils.encodeAccountConfigs(newAccount)); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokenPermission, true); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), batchDelegateTokenPermission, true); vm.stopPrank(); @@ -47,10 +62,12 @@ contract DelegateToken is LlamaAccountWithDelegationTest { bytes memory data = abi.encodeCall(LlamaAccountWithDelegation.delegateToken, (tokenDelegateData)); vm.prank(actionCreatorAaron); - uint256 actionId = mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data, ""); + uint256 actionId = + mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(accountWithDelegation), 0, data, ""); - ActionInfo memory actionInfo = - ActionInfo(actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data); + ActionInfo memory actionInfo = ActionInfo( + actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(accountWithDelegation), 0, data + ); vm.warp(block.timestamp + 1); vm.prank(approverAdam); @@ -69,8 +86,8 @@ contract DelegateToken is LlamaAccountWithDelegationTest { function test_TokensDelegatedToExecutor() external { // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet - assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(0)); LlamaAccountWithDelegation.TokenDelegateData memory tokenDelegateData = LlamaAccountWithDelegation.TokenDelegateData(IVotes(address(UNI)), address(mpExecutor)); @@ -78,14 +95,14 @@ contract DelegateToken is LlamaAccountWithDelegationTest { executeDelegateTokenAction(tokenDelegateData); // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address - assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); + assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(mpExecutor)); } function test_TokensDelegatedToAnyAddress(address delegatee) external { // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet - assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); + assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(0)); LlamaAccountWithDelegation.TokenDelegateData memory tokenDelegateData = LlamaAccountWithDelegation.TokenDelegateData(IVotes(address(UNI)), delegatee); @@ -93,8 +110,8 @@ contract DelegateToken is LlamaAccountWithDelegationTest { executeDelegateTokenAction(tokenDelegateData); // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address - assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee); + assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), delegatee); } } @@ -105,10 +122,12 @@ contract BatchDelegateToken is LlamaAccountWithDelegationTest { bytes memory data = abi.encodeCall(LlamaAccountWithDelegation.batchDelegateToken, (tokenDelegateData)); vm.prank(actionCreatorAaron); - uint256 actionId = mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data, ""); + uint256 actionId = + mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(accountWithDelegation), 0, data, ""); - ActionInfo memory actionInfo = - ActionInfo(actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(mpAccount1), 0, data); + ActionInfo memory actionInfo = ActionInfo( + actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(accountWithDelegation), 0, data + ); vm.warp(block.timestamp + 1); vm.prank(approverAdam); @@ -126,28 +145,28 @@ contract BatchDelegateToken is LlamaAccountWithDelegationTest { } function test_BatchDelegateTokensFromAccount(address delegatee1, address delegatee2) external { - // Assert that the account has UNI and AAVE tokens and hasn't delegated them yet - assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(AAVE.balanceOf(address(mpAccount1)), 28_600e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); - assertEq(IVotes(address(AAVE)).delegates(address(mpAccount1)), address(0)); + // Assert that the account has UNI and ENS tokens and hasn't delegated them yet + assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); + assertEq(ENS.balanceOf(address(accountWithDelegation)), 28_600e18); + assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(0)); + assertEq(IVotes(address(ENS)).delegates(address(accountWithDelegation)), address(0)); LlamaAccountWithDelegation.TokenDelegateData[] memory tokenDelegateData = new LlamaAccountWithDelegation.TokenDelegateData[](2); for (uint256 i = 0; i < 2; LlamaUtils.uncheckedIncrement(i)) { tokenDelegateData[i] = LlamaAccountWithDelegation.TokenDelegateData( - i % 2 == 0 ? IVotes(address(UNI)) : IVotes(address(AAVE)), i % 2 == 0 ? delegatee1 : delegatee2 + i % 2 == 0 ? IVotes(address(UNI)) : IVotes(address(ENS)), i % 2 == 0 ? delegatee1 : delegatee2 ); } executeBatchDelegateTokenAction(tokenDelegateData); - // After the action executes the accounts should still have all UNI and AAVE tokens but the delegate is the + // After the action executes the accounts should still have all UNI and ENS tokens but the delegate is the // delegatee - assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(AAVE.balanceOf(address(mpAccount1)), 28_600e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), delegatee1); - assertEq(IVotes(address(AAVE)).delegates(address(mpAccount1)), delegatee2); + assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); + assertEq(ENS.balanceOf(address(accountWithDelegation)), 28_600e18); + assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), delegatee1); + assertEq(IVotes(address(ENS)).delegates(address(accountWithDelegation)), delegatee2); } } diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index 2bda27d47..efaf6f81f 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -15,6 +15,8 @@ import {LlamaAccountTokenDelegationScript} from "src/llama-scripts/LlamaAccountT import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + IERC20 public constant UNI = IERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); uint256 public constant UNI_AMOUNT = 1000e18; @@ -22,7 +24,7 @@ contract LlamaAccountTokenDelegationScriptTest is LlamaTestSetup { PermissionData public delegateTokenPermission; PermissionData public delegateTokensPermission; - event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + function setUp() public virtual override { vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); LlamaTestSetup.setUp(); @@ -89,11 +91,12 @@ contract DelegateTokenFromAccount is LlamaAccountTokenDelegationScriptTest { LlamaAccount(payable(address(mpAccount1))), IVotes(address(UNI)), address(mpExecutor) ); + vm.expectEmit(); + emit DelegateChanged(address(mpAccount1), address(0), address(mpExecutor)); executeDelegateTokenAction(tokenDelegateData); - // After the action executes the account should still have 1,000 UNI tokens but the delegate is the executor address + // After the action executes the account should still have 1,000 UNI tokens assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); - assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); } function test_TokensDelegatedToAnyAddress(address delegatee) external { From de3efb0cd0786af37ccb5b32a757504fa1663c0f Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 21:35:27 -0500 Subject: [PATCH 13/19] fix tests --- .../accounts/LlamaAccountWithDelegation.t.sol | 21 ++++++++++++------- .../LlamaAccountTokenDelegationScript.t.sol | 3 +-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/test/accounts/LlamaAccountWithDelegation.t.sol b/test/accounts/LlamaAccountWithDelegation.t.sol index f02e197a2..a58e6b4f9 100644 --- a/test/accounts/LlamaAccountWithDelegation.t.sol +++ b/test/accounts/LlamaAccountWithDelegation.t.sol @@ -32,9 +32,17 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); LlamaTestSetup.setUp(); - LlamaAccountWithDelegation.Config[] memory newAccount = new LlamaAccountWithDelegation.Config[](3); + LlamaAccount.Config[] memory newAccount = new LlamaAccount.Config[](1); newAccount[0] = LlamaAccount.Config({name: "LlamaAccountWithDelegation"}); + bytes[] memory encoded = new bytes[](1); + for (uint256 i = 0; i < newAccount.length; i++) { + encoded[i] = abi.encode(newAccount[i]); + } + + accountWithDelegation = + lens.computeLlamaAccountAddress(address(accountWithDelegationLogic), abi.encode(newAccount[0]), address(mpCore)); + deal(address(UNI), address(accountWithDelegation), UNI_AMOUNT); deal(address(ENS), address(accountWithDelegation), ENS_AMOUNT); @@ -45,12 +53,9 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { address(accountWithDelegation), LlamaAccountWithDelegation.batchDelegateToken.selector, mpStrategy1 ); - accountWithDelegation = - lens.computeLlamaAccountAddress(address(accountWithDelegationLogic), abi.encode(newAccount[0]), address(mpCore)); - vm.startPrank(address(mpExecutor)); mpCore.setAccountLogicAuthorization(accountWithDelegationLogic, true); - mpCore.createAccounts(accountWithDelegationLogic, DeployUtils.encodeAccountConfigs(newAccount)); + mpCore.createAccounts(accountWithDelegationLogic, encoded); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokenPermission, true); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), batchDelegateTokenPermission, true); vm.stopPrank(); @@ -84,7 +89,7 @@ contract DelegateToken is LlamaAccountWithDelegationTest { mpCore.executeAction(actionInfo); } - function test_TokensDelegatedToExecutor() external { + function test_DelegateToExecutor() external { // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(0)); @@ -99,7 +104,7 @@ contract DelegateToken is LlamaAccountWithDelegationTest { assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(mpExecutor)); } - function test_TokensDelegatedToAnyAddress(address delegatee) external { + function test_DelegateToAnyAddress(address delegatee) external { // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(0)); @@ -144,7 +149,7 @@ contract BatchDelegateToken is LlamaAccountWithDelegationTest { mpCore.executeAction(actionInfo); } - function test_BatchDelegateTokensFromAccount(address delegatee1, address delegatee2) external { + function test_BatchDelegateTokens(address delegatee1, address delegatee2) external { // Assert that the account has UNI and ENS tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); assertEq(ENS.balanceOf(address(accountWithDelegation)), 28_600e18); diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index efaf6f81f..273a90ca0 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -91,12 +91,11 @@ contract DelegateTokenFromAccount is LlamaAccountTokenDelegationScriptTest { LlamaAccount(payable(address(mpAccount1))), IVotes(address(UNI)), address(mpExecutor) ); - vm.expectEmit(); - emit DelegateChanged(address(mpAccount1), address(0), address(mpExecutor)); executeDelegateTokenAction(tokenDelegateData); // After the action executes the account should still have 1,000 UNI tokens assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); + assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); } function test_TokensDelegatedToAnyAddress(address delegatee) external { From 305b046ce3effbc9dfbc18a137a1cef82a2c0734 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 21:48:58 -0500 Subject: [PATCH 14/19] remove fuzz tests --- .../accounts/LlamaAccountWithDelegation.t.sol | 58 +++++-------------- 1 file changed, 13 insertions(+), 45 deletions(-) diff --git a/test/accounts/LlamaAccountWithDelegation.t.sol b/test/accounts/LlamaAccountWithDelegation.t.sol index a58e6b4f9..b3579fb6a 100644 --- a/test/accounts/LlamaAccountWithDelegation.t.sol +++ b/test/accounts/LlamaAccountWithDelegation.t.sol @@ -32,16 +32,11 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { vm.createSelectFork(vm.rpcUrl("mainnet"), 18_642_270); LlamaTestSetup.setUp(); - LlamaAccount.Config[] memory newAccount = new LlamaAccount.Config[](1); - newAccount[0] = LlamaAccount.Config({name: "LlamaAccountWithDelegation"}); - - bytes[] memory encoded = new bytes[](1); - for (uint256 i = 0; i < newAccount.length; i++) { - encoded[i] = abi.encode(newAccount[i]); - } + LlamaAccount.Config[] memory newAccounts = new LlamaAccount.Config[](1); + newAccounts[0] = LlamaAccount.Config({name: "LlamaAccountWithDelegation"}); accountWithDelegation = - lens.computeLlamaAccountAddress(address(accountWithDelegationLogic), abi.encode(newAccount[0]), address(mpCore)); + lens.computeLlamaAccountAddress(address(accountWithDelegationLogic), abi.encode(newAccounts[0]), address(mpCore)); deal(address(UNI), address(accountWithDelegation), UNI_AMOUNT); deal(address(ENS), address(accountWithDelegation), ENS_AMOUNT); @@ -55,7 +50,7 @@ contract LlamaAccountWithDelegationTest is LlamaTestSetup { vm.startPrank(address(mpExecutor)); mpCore.setAccountLogicAuthorization(accountWithDelegationLogic, true); - mpCore.createAccounts(accountWithDelegationLogic, encoded); + mpCore.createAccounts(accountWithDelegationLogic, DeployUtils.encodeAccountConfigs(newAccounts)); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), delegateTokenPermission, true); mpPolicy.setRolePermission(uint8(Roles.ActionCreator), batchDelegateTokenPermission, true); vm.stopPrank(); @@ -104,7 +99,8 @@ contract DelegateToken is LlamaAccountWithDelegationTest { assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(mpExecutor)); } - function test_DelegateToAnyAddress(address delegatee) external { + function test_DelegateToOtherAddress() external { + address delegatee = makeAddr("delegatee"); // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(accountWithDelegation)), address(0)); @@ -121,35 +117,9 @@ contract DelegateToken is LlamaAccountWithDelegationTest { } contract BatchDelegateToken is LlamaAccountWithDelegationTest { - function executeBatchDelegateTokenAction(LlamaAccountWithDelegation.TokenDelegateData[] memory tokenDelegateData) - internal - { - bytes memory data = abi.encodeCall(LlamaAccountWithDelegation.batchDelegateToken, (tokenDelegateData)); - - vm.prank(actionCreatorAaron); - uint256 actionId = - mpCore.createAction(uint8(Roles.ActionCreator), mpStrategy1, address(accountWithDelegation), 0, data, ""); - - ActionInfo memory actionInfo = ActionInfo( - actionId, actionCreatorAaron, uint8(Roles.ActionCreator), mpStrategy1, address(accountWithDelegation), 0, data - ); - vm.warp(block.timestamp + 1); - - vm.prank(approverAdam); - mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); - vm.prank(approverAlicia); - mpCore.castApproval(uint8(Roles.Approver), actionInfo, ""); - - vm.warp(block.timestamp + 6 days); - - mpCore.queueAction(actionInfo); - - vm.warp(block.timestamp + 5 days); - - mpCore.executeAction(actionInfo); - } - - function test_BatchDelegateTokens(address delegatee1, address delegatee2) external { + function test_BatchDelegateTokens() external { + address delegatee1 = address(mpExecutor); + address delegatee2 = makeAddr("delegatee2"); // Assert that the account has UNI and ENS tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(accountWithDelegation)), 1000e18); assertEq(ENS.balanceOf(address(accountWithDelegation)), 28_600e18); @@ -159,13 +129,11 @@ contract BatchDelegateToken is LlamaAccountWithDelegationTest { LlamaAccountWithDelegation.TokenDelegateData[] memory tokenDelegateData = new LlamaAccountWithDelegation.TokenDelegateData[](2); - for (uint256 i = 0; i < 2; LlamaUtils.uncheckedIncrement(i)) { - tokenDelegateData[i] = LlamaAccountWithDelegation.TokenDelegateData( - i % 2 == 0 ? IVotes(address(UNI)) : IVotes(address(ENS)), i % 2 == 0 ? delegatee1 : delegatee2 - ); - } + tokenDelegateData[0] = LlamaAccountWithDelegation.TokenDelegateData(IVotes(address(UNI)), delegatee1); + tokenDelegateData[1] = LlamaAccountWithDelegation.TokenDelegateData(IVotes(address(ENS)), delegatee2); - executeBatchDelegateTokenAction(tokenDelegateData); + vm.prank(address(mpExecutor)); + LlamaAccountWithDelegation(payable(address(accountWithDelegation))).batchDelegateToken(tokenDelegateData); // After the action executes the accounts should still have all UNI and ENS tokens but the delegate is the // delegatee From d64badbc5afa47482ed38eea85750cec6db8c17f Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 11 Dec 2023 21:54:01 -0500 Subject: [PATCH 15/19] remove fuzz tests --- .../accounts/LlamaAccountWithDelegation.t.sol | 1 - .../LlamaAccountTokenDelegationScript.t.sol | 20 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/accounts/LlamaAccountWithDelegation.t.sol b/test/accounts/LlamaAccountWithDelegation.t.sol index b3579fb6a..5d4543496 100644 --- a/test/accounts/LlamaAccountWithDelegation.t.sol +++ b/test/accounts/LlamaAccountWithDelegation.t.sol @@ -13,7 +13,6 @@ import {DeployUtils} from "script/DeployUtils.sol"; import {ILlamaAccount} from "src/interfaces/ILlamaAccount.sol"; import {LlamaAccount} from "src/accounts/LlamaAccount.sol"; import {LlamaAccountWithDelegation} from "src/accounts/LlamaAccountWithDelegation.sol"; -import {LlamaUtils} from "src/lib/LlamaUtils.sol"; import {Roles, LlamaTestSetup} from "test/utils/LlamaTestSetup.sol"; import {ActionInfo, PermissionData} from "src/lib/Structs.sol"; diff --git a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol index 273a90ca0..11f10bbc6 100644 --- a/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol +++ b/test/llama-scripts/LlamaAccountTokenDelegationScript.t.sol @@ -98,7 +98,8 @@ contract DelegateTokenFromAccount is LlamaAccountTokenDelegationScriptTest { assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(mpExecutor)); } - function test_TokensDelegatedToAnyAddress(address delegatee) external { + function test_TokensDelegatedToOtherAddress() external { + address delegatee = makeAddr("scriptDelegatee"); // Assert that the account has 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); assertEq(IVotes(address(UNI)).delegates(address(mpAccount1)), address(0)); @@ -146,7 +147,9 @@ contract DelegateTokensFromAccount is LlamaAccountTokenDelegationScriptTest { mpCore.executeAction(actionInfo); } - function test_DelegateTokensFromAccounts(address delegatee1, address delegatee2) external { + function test_DelegateTokensFromAccounts() external { + address delegatee1 = makeAddr("scriptDelegate1"); + address delegatee2 = makeAddr("scriptDelegate2"); // Assert that the accounts have 1,000 UNI tokens and hasn't delegated them yet assertEq(UNI.balanceOf(address(mpAccount1)), 1000e18); assertEq(UNI.balanceOf(address(mpAccount2)), 1000e18); @@ -156,13 +159,12 @@ contract DelegateTokensFromAccount is LlamaAccountTokenDelegationScriptTest { LlamaAccountTokenDelegationScript.AccountTokenDelegateData[] memory tokenDelegateData = new LlamaAccountTokenDelegationScript.AccountTokenDelegateData[](2); - for (uint256 i = 0; i < 2; LlamaUtils.uncheckedIncrement(i)) { - tokenDelegateData[i] = LlamaAccountTokenDelegationScript.AccountTokenDelegateData( - LlamaAccount(payable(address(i % 2 == 0 ? mpAccount1 : mpAccount2))), - IVotes(address(UNI)), - i % 2 == 0 ? delegatee1 : delegatee2 - ); - } + tokenDelegateData[0] = LlamaAccountTokenDelegationScript.AccountTokenDelegateData( + LlamaAccount(payable(address(mpAccount1))), IVotes(address(UNI)), delegatee1 + ); + tokenDelegateData[1] = LlamaAccountTokenDelegationScript.AccountTokenDelegateData( + LlamaAccount(payable(address(mpAccount2))), IVotes(address(UNI)), delegatee2 + ); executeDelegateTokensAction(tokenDelegateData); From 83a51acc12a4b9343e5230e2c93d54ee663d724e Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 18 Dec 2023 09:53:49 -0700 Subject: [PATCH 16/19] remove constructor --- src/accounts/LlamaAccountWithDelegation.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/accounts/LlamaAccountWithDelegation.sol b/src/accounts/LlamaAccountWithDelegation.sol index 2c7a39aaf..2d3f1af51 100644 --- a/src/accounts/LlamaAccountWithDelegation.sol +++ b/src/accounts/LlamaAccountWithDelegation.sol @@ -21,16 +21,6 @@ contract LlamaAccountWithDelegation is LlamaAccount { address delegatee; // The address being delegated to. } - // ====================================================== - // ======== Contract Creation and Initialization ======== - // ====================================================== - - /// @dev This contract is deployed as a minimal proxy from the core's `_deployAccounts` function. The - /// `_disableInitializers` locks the implementation (logic) contract, preventing any future initialization of it. - constructor() { - _disableInitializers(); - } - // =========================================== // ======== External and Public Logic ======== // =========================================== From be0043756a4f45930c16c299bc949cf6f52985d4 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 18 Dec 2023 09:55:49 -0700 Subject: [PATCH 17/19] fix selector --- src/llama-scripts/LlamaAccountTokenDelegationScript.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol index 171286b94..6bd21a286 100644 --- a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -34,7 +34,7 @@ contract LlamaAccountTokenDelegationScript is LlamaBaseScript { uint256 internal constant VALUE = 0; /// @notice The function selector of the `IVotes` `delegate(address)` function. - bytes4 internal constant DELEGATE_SELECTOR = 0x5c19a95c; + bytes4 internal constant DELEGATE_SELECTOR = IVotes.delegate.selector; // ======================================== // ======= Delegate token functions ======= From 30d24a9e1e1979ee30938655b1154aae08c195ce Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 18 Dec 2023 09:57:32 -0700 Subject: [PATCH 18/19] simplify script --- src/llama-scripts/LlamaAccountTokenDelegationScript.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol index 6bd21a286..5c6704c36 100644 --- a/src/llama-scripts/LlamaAccountTokenDelegationScript.sol +++ b/src/llama-scripts/LlamaAccountTokenDelegationScript.sol @@ -61,11 +61,7 @@ contract LlamaAccountTokenDelegationScript is LlamaBaseScript { { uint256 length = accountTokenDelegateData.length; for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { - delegateTokenFromAccount( - AccountTokenDelegateData( - accountTokenDelegateData[i].account, accountTokenDelegateData[i].token, accountTokenDelegateData[i].delegatee - ) - ); + delegateTokenFromAccount(accountTokenDelegateData[i]); } } } From fa08ff645706bc374753ab9362a434ccc7171444 Mon Sep 17 00:00:00 2001 From: Austin Green Date: Mon, 18 Dec 2023 10:06:46 -0700 Subject: [PATCH 19/19] add script to deployment --- script/DeployLlamaFactory.s.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/script/DeployLlamaFactory.s.sol b/script/DeployLlamaFactory.s.sol index 8fd219960..0c293e9ea 100644 --- a/script/DeployLlamaFactory.s.sol +++ b/script/DeployLlamaFactory.s.sol @@ -10,6 +10,7 @@ import {LlamaFactory} from "src/LlamaFactory.sol"; import {LlamaLens} from "src/LlamaLens.sol"; import {LlamaPolicy} from "src/LlamaPolicy.sol"; import {LlamaPolicyMetadata} from "src/LlamaPolicyMetadata.sol"; +import {LlamaAccountTokenDelegationScript} from "src/llama-scripts/LlamaAccountTokenDelegationScript.sol"; import {LlamaGovernanceScript} from "src/llama-scripts/LlamaGovernanceScript.sol"; import {LlamaAbsolutePeerReview} from "src/strategies/absolute/LlamaAbsolutePeerReview.sol"; import {LlamaAbsoluteQuorum} from "src/strategies/absolute/LlamaAbsoluteQuorum.sol"; @@ -39,6 +40,7 @@ contract DeployLlamaFactory is Script { // Llama scripts LlamaGovernanceScript governanceScript; + LlamaAccountTokenDelegationScript accountTokenDelegationScript; function run() public { DeployUtils.print(string.concat("Deploying Llama factory to chain:", vm.toString(block.chainid))); @@ -133,5 +135,11 @@ contract DeployLlamaFactory is Script { DeployUtils.print( string.concat(" LlamaAccountWithDelegationLogic:", vm.toString(address(accountWithDelegationLogic))) ); + + vm.broadcast(); + accountTokenDelegationScript = new LlamaAccountTokenDelegationScript(); + DeployUtils.print( + string.concat(" LlamaAccountTokenDelegationScript:", vm.toString(address(accountTokenDelegationScript))) + ); } }