From 8a010654c9bc9f46a19d96e999d6892cf44ff0d7 Mon Sep 17 00:00:00 2001 From: Kevin Siegler <17910833+topocount@users.noreply.github.com> Date: Fri, 6 Sep 2024 09:57:37 -0500 Subject: [PATCH] chore(evm): migrate unshared business logic to impl contracts Any unshared logic is moved to impls, while preserving public interfaces to ensure interfaces are available to the sdk This includes migrating ACL logic to impl contracts out of abstracts because they have no EIP-165 interfaces --- .../evm/contracts/actions/AContractAction.sol | 11 + .../contracts/actions/AERC721MintAction.sol | 54 +-- .../evm/contracts/actions/AEventAction.sol | 30 +- .../evm/contracts/actions/ContractAction.sol | 11 - .../contracts/actions/ERC721MintAction.sol | 57 ++- .../evm/contracts/actions/EventAction.sol | 26 ++ .../evm/contracts/allowlists/AAllowList.sol | 9 +- .../contracts/allowlists/ASimpleAllowList.sol | 24 +- .../contracts/allowlists/ASimpleDenyList.sol | 22 +- .../contracts/allowlists/SimpleAllowList.sol | 30 +- .../contracts/allowlists/SimpleDenyList.sol | 29 +- .../evm/contracts/budgets/AManagedBudget.sol | 294 +--------------- .../evm/contracts/budgets/ASimpleBudget.sol | 276 --------------- .../evm/contracts/budgets/AVestingBudget.sol | 196 +---------- .../evm/contracts/budgets/ManagedBudget.sol | 324 ++++++++++++++++++ .../evm/contracts/budgets/SimpleBudget.sol | 287 +++++++++++++++- .../evm/contracts/budgets/VestingBudget.sol | 199 +++++++++++ .../incentives/AAllowListIncentive.sol | 42 --- .../contracts/incentives/ACGDAIncentive.sol | 63 +--- .../incentives/AERC1155Incentive.sol | 79 ----- .../contracts/incentives/AERC20Incentive.sol | 73 +--- .../contracts/incentives/APointsIncentive.sol | 53 +-- .../incentives/AllowListIncentive.sol | 41 +++ .../contracts/incentives/CGDAIncentive.sol | 55 +++ .../contracts/incentives/ERC1155Incentive.sol | 83 +++++ .../contracts/incentives/ERC20Incentive.sol | 75 ++++ .../contracts/incentives/PointsIncentive.sol | 52 +++ packages/evm/test/BoostCore.t.sol | 4 +- .../evm/test/actions/ContractAction.t.sol | 8 +- packages/evm/test/budgets/ManagedBudget.t.sol | 2 +- packages/evm/test/budgets/SimpleBudget.t.sol | 33 +- packages/evm/test/e2e/EndToEndBasic.t.sol | 4 +- .../test/e2e/EndToEndSignerValidator.t.sol | 4 +- 33 files changed, 1306 insertions(+), 1244 deletions(-) diff --git a/packages/evm/contracts/actions/AContractAction.sol b/packages/evm/contracts/actions/AContractAction.sol index 0e0cee22f..f286924a8 100644 --- a/packages/evm/contracts/actions/AContractAction.sol +++ b/packages/evm/contracts/actions/AContractAction.sol @@ -8,6 +8,17 @@ import {ACloneable} from "contracts/shared/ACloneable.sol"; import {AAction} from "contracts/actions/AAction.sol"; abstract contract AContractAction is AAction { + /// @notice The payload for initializing a ContractAction + /// @param target The target contract address + /// @param selector The selector for the function to be called + /// @param value The native token value to send with the function call + struct InitPayload { + uint256 chainId; + address target; + bytes4 selector; + uint256 value; + } + /// @notice Thrown when execution on a given chain is not supported error TargetChainUnsupported(uint256 targetChainId); diff --git a/packages/evm/contracts/actions/AERC721MintAction.sol b/packages/evm/contracts/actions/AERC721MintAction.sol index 07a3c894e..111e50a26 100644 --- a/packages/evm/contracts/actions/AERC721MintAction.sol +++ b/packages/evm/contracts/actions/AERC721MintAction.sol @@ -1,14 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -import {Ownable as AOwnable} from "@solady/auth/Ownable.sol"; -import {ERC721} from "@solady/tokens/ERC721.sol"; - -import {BoostError} from "contracts/shared/BoostError.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; -import {AAction} from "contracts/actions/AAction.sol"; -import {ContractAction} from "contracts/actions/ContractAction.sol"; import {AContractAction} from "contracts/actions/AContractAction.sol"; import {AValidator} from "contracts/validators/AValidator.sol"; @@ -17,57 +11,11 @@ import {AValidator} from "contracts/validators/AValidator.sol"; /// @dev The action is expected to be prepared with the data payload for the minting of the token /// @dev This a minimal generic implementation that should be extended if additional functionality or customizations are required /// @dev It is expected that the target contract has an externally accessible mint function whose selector -abstract contract AERC721MintAction is ContractAction, AValidator, AOwnable { +abstract contract AERC721MintAction is AContractAction, AValidator { /// @notice The set of validated tokens /// @dev This is intended to prevent multiple validations against the same token ID mapping(uint256 => bool) public validated; - /// @inheritdoc ContractAction - /// @notice Initialize the contract with the owner and the required data - function initialize(bytes calldata data_) public virtual override(ContractAction, ACloneable) initializer { - ContractAction.initialize(data_); - } - - /// @notice Execute the action (not yet implemented) - /// @param data_ The data payload for the call (not used in this implementation) - /// @return success The success status of the call - /// @return returnData The return data from the call - function execute(bytes calldata data_) external payable override returns (bool success, bytes memory returnData) { - (data_, success, returnData); - revert BoostError.NotImplemented(); - } - - /// @notice Prepare the action for execution and return the expected payload - /// @param data_ The ABI-encoded payload for the target contract call - /// @return The encoded payload to be sent to the target contract - /// @dev Note that the mint value is NOT included in the prepared payload but must be sent with the call - function prepare(bytes calldata data_) public view override returns (bytes memory) { - return super.prepare(data_); - } - - /// @inheritdoc AValidator - /// @notice Validate that the action has been completed successfully - /// @param data_ The data payload for the action `(address holder, (uint256 tokenId))` - /// @return success True if the action has been validated for the user - /// @dev The first 20 bytes of the payload must be the holder address and the remaining bytes must be an encoded token ID (uint256) - /// @dev Example: `abi.encode(address(holder), abi.encode(uint256(tokenId)))` - function validate(uint256, /*unused*/ uint256, /* unused */ address, /*unused*/ bytes calldata data_) - external - virtual - override - returns (bool success) - { - (address holder, bytes memory payload) = abi.decode(data_, (address, bytes)); - uint256 tokenId = uint256(bytes32(payload)); - - if (ERC721(target).ownerOf(tokenId) == holder && !validated[tokenId]) { - validated[tokenId] = true; - return true; - } else { - return false; - } - } - /// @inheritdoc AContractAction function getComponentInterface() public pure virtual override(AContractAction, AValidator) returns (bytes4) { return type(AERC721MintAction).interfaceId; diff --git a/packages/evm/contracts/actions/AEventAction.sol b/packages/evm/contracts/actions/AEventAction.sol index 779142374..c66cd9d22 100644 --- a/packages/evm/contracts/actions/AEventAction.sol +++ b/packages/evm/contracts/actions/AEventAction.sol @@ -47,39 +47,17 @@ abstract contract AEventAction is AAction { Criteria actionParameter; } - /// @notice Prepare the action for execution and return the expected payload - /// @param data_ The ABI-encoded payload for the target contract call - /// @return bytes_ The encoded payload to be sent to the target contract - /// @dev Note that the mint value is NOT included in the prepared payload but must be sent with the call - function prepare(bytes calldata data_) public view virtual override returns (bytes memory bytes_) { - // Since this action is marshalled off-chain we don't need to prepare the payload - revert BoostError.NotImplemented(); - //return data_; - } + function getActionEventsCount() public view virtual returns (uint256); - function execute(bytes calldata data_) external payable virtual override returns (bool, bytes memory) { - // Since this action is marshalled off-chain we don't need to execute the payload - revert BoostError.NotImplemented(); - //return (true, data_); - } + function getActionEvent(uint256 index) public view virtual returns (ActionEvent memory); + + function getActionEvents() public view virtual returns (ActionEvent[] memory); /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override returns (bytes4) { return type(AEventAction).interfaceId; } - function getActionEventsCount() public view virtual returns (uint256) { - return actionEvents.length; - } - - function getActionEvent(uint256 index) public view virtual returns (ActionEvent memory) { - return actionEvents[index]; - } - - function getActionEvents() public view virtual returns (ActionEvent[] memory) { - return actionEvents; - } - /// @inheritdoc AAction function supportsInterface(bytes4 interfaceId) public view virtual override(AAction) returns (bool) { return interfaceId == type(AEventAction).interfaceId || super.supportsInterface(interfaceId); diff --git a/packages/evm/contracts/actions/ContractAction.sol b/packages/evm/contracts/actions/ContractAction.sol index 688fc4421..0dfcc718a 100644 --- a/packages/evm/contracts/actions/ContractAction.sol +++ b/packages/evm/contracts/actions/ContractAction.sol @@ -8,17 +8,6 @@ import {ACloneable} from "contracts/shared/ACloneable.sol"; import {AContractAction} from "contracts/actions/AContractAction.sol"; contract ContractAction is AContractAction { - /// @notice The payload for initializing a ContractAction - /// @param target The target contract address - /// @param selector The selector for the function to be called - /// @param value The native token value to send with the function call - struct InitPayload { - uint256 chainId; - address target; - bytes4 selector; - uint256 value; - } - constructor() { _disableInitializers(); } diff --git a/packages/evm/contracts/actions/ERC721MintAction.sol b/packages/evm/contracts/actions/ERC721MintAction.sol index 4d98bb180..4a8cbfb56 100644 --- a/packages/evm/contracts/actions/ERC721MintAction.sol +++ b/packages/evm/contracts/actions/ERC721MintAction.sol @@ -2,36 +2,77 @@ pragma solidity ^0.8.24; import {ERC721} from "@solady/tokens/ERC721.sol"; +import {Ownable as AOwnable} from "@solady/auth/Ownable.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; +import {AValidator} from "contracts/validators/AValidator.sol"; import {AERC721MintAction} from "contracts/actions/AERC721MintAction.sol"; -import {ContractAction} from "contracts/actions/ContractAction.sol"; +import {BoostError} from "contracts/shared/BoostError.sol"; /// @title ERC721 Mint AAction /// @notice A primitive action to mint and/or validate that an ERC721 token has been minted /// @dev The action is expected to be prepared with the data payload for the minting of the token /// @dev This a minimal generic implementation that should be extended if additional functionality or customizations are required /// @dev It is expected that the target contract has an externally accessible mint function whose selector -contract ERC721MintAction is AERC721MintAction { +contract ERC721MintAction is AOwnable, AERC721MintAction { /// @notice Construct the ERC721 Mint AAction /// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function. constructor() { _disableInitializers(); } - /// @inheritdoc ACloneable - /// @notice Initialize the contract with the owner and the required mint data - /// @param data_ The data payload for the mint action `(address target, bytes4 selector, uint256 value)` - function initialize(bytes calldata data_) public virtual override initializer { + /// @inheritdoc ACloneable + /// @notice Initialize the contract with the owner and the required data + function initialize(bytes calldata data_) public virtual override(ACloneable) initializer { _initialize(abi.decode(data_, (InitPayload))); } - function _initialize(InitPayload memory init_) internal override onlyInitializing { + /// @notice Execute the action (not yet implemented) + /// @param data_ The data payload for the call (not used in this implementation) + /// @return success The success status of the call + /// @return returnData The return data from the call + function execute(bytes calldata data_) external payable override returns (bool success, bytes memory returnData) { + (data_, success, returnData); + revert BoostError.NotImplemented(); + } + + /// @notice Prepare the action for execution and return the expected payload + /// @param data_ The ABI-encoded payload for the target contract call + /// @return The encoded payload to be sent to the target contract + /// @dev Note that the mint value is NOT included in the prepared payload but must be sent with the call + function prepare(bytes calldata data_) public view override returns (bytes memory) { + return super.prepare(data_); + } + + /// @inheritdoc AValidator + /// @notice Validate that the action has been completed successfully + /// @param data_ The data payload for the action `(address holder, (uint256 tokenId))` + /// @return success True if the action has been validated for the user + /// @dev The first 20 bytes of the payload must be the holder address and the remaining bytes must be an encoded token ID (uint256) + /// @dev Example: `abi.encode(address(holder), abi.encode(uint256(tokenId)))` + function validate(uint256, /*unused*/ uint256, /* unused */ address, /*unused*/ bytes calldata data_) + external + virtual + override + returns (bool success) + { + (address holder, bytes memory payload) = abi.decode(data_, (address, bytes)); + uint256 tokenId = uint256(bytes32(payload)); + + if (ERC721(target).ownerOf(tokenId) == holder && !validated[tokenId]) { + validated[tokenId] = true; + return true; + } else { + return false; + } + } + + function _initialize(InitPayload memory init_) internal virtual onlyInitializing { + _initializeOwner(msg.sender); chainId = init_.chainId; target = init_.target; selector = init_.selector; value = init_.value; - _initializeOwner(msg.sender); } } diff --git a/packages/evm/contracts/actions/EventAction.sol b/packages/evm/contracts/actions/EventAction.sol index 735ed713d..90e21e3f8 100644 --- a/packages/evm/contracts/actions/EventAction.sol +++ b/packages/evm/contracts/actions/EventAction.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {ERC721} from "@solady/tokens/ERC721.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; +import {BoostError} from "contracts/shared/BoostError.sol"; import {AEventAction} from "contracts/actions/AEventAction.sol"; @@ -35,4 +36,29 @@ contract EventAction is AEventAction { actionEvents.push(init_.actionEventThree); actionEvents.push(init_.actionEventFour); } + + /// @notice Prepare the action for execution and return the expected payload + /// @return bytes_ The encoded payload to be sent to the target contract + /// @dev Note that the mint value is NOT included in the prepared payload but must be sent with the call + function prepare(bytes calldata) public view virtual override returns (bytes memory) { + // Since this action is marshalled off-chain we don't need to prepare the payload + revert BoostError.NotImplemented(); + } + + function execute(bytes calldata) external payable virtual override returns (bool, bytes memory) { + // Since this action is marshalled off-chain we don't need to execute the payload + revert BoostError.NotImplemented(); + } + + function getActionEventsCount() public view virtual override returns (uint256) { + return actionEvents.length; + } + + function getActionEvent(uint256 index) public view virtual override returns (ActionEvent memory) { + return actionEvents[index]; + } + + function getActionEvents() public view virtual override returns (ActionEvent[] memory) { + return actionEvents; + } } diff --git a/packages/evm/contracts/allowlists/AAllowList.sol b/packages/evm/contracts/allowlists/AAllowList.sol index 365e627ec..b173dee75 100644 --- a/packages/evm/contracts/allowlists/AAllowList.sol +++ b/packages/evm/contracts/allowlists/AAllowList.sol @@ -1,19 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -import {Ownable} from "@solady/auth/Ownable.sol"; - import {ACloneable} from "contracts/shared/ACloneable.sol"; /// @title Boost AllowList /// @notice Abstract contract for a generic Allow List within the Boost protocol /// @dev Allow List classes are expected to implement the authorization of users based on implementation-specific criteria, which may involve validation of a data payload. If no data is required, calldata should be empty. -abstract contract AAllowList is Ownable, ACloneable { - /// @notice Constructor to initialize the owner - constructor() { - _initializeOwner(msg.sender); - } - +abstract contract AAllowList is ACloneable { /// @notice Check if a user is authorized /// @param user_ The address of the user /// @param data_ The data payload for the authorization check, if applicable diff --git a/packages/evm/contracts/allowlists/ASimpleAllowList.sol b/packages/evm/contracts/allowlists/ASimpleAllowList.sol index 1bfffc6a1..a0e57b511 100644 --- a/packages/evm/contracts/allowlists/ASimpleAllowList.sol +++ b/packages/evm/contracts/allowlists/ASimpleAllowList.sol @@ -1,42 +1,22 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; - import {ACloneable} from "contracts/shared/ACloneable.sol"; -import {BoostError} from "contracts/shared/BoostError.sol"; import {AAllowList} from "contracts/allowlists/AAllowList.sol"; /// @title Simple AllowList /// @notice A simple implementation of an AllowList that checks if a user is authorized based on a list of allowed addresses -abstract contract ASimpleAllowList is AAllowList, OwnableRoles { +abstract contract ASimpleAllowList is AAllowList { /// @notice The role for managing the allow list uint256 public constant LIST_MANAGER_ROLE = 1 << 1; - /// @dev An internal mapping of allowed statuses - mapping(address => bool) internal _allowed; - - /// @notice Check if a user is authorized - /// @param user_ The address of the user - /// @param - The data payload for the authorization check, not used in this implementation - /// @return True if the user is authorized - function isAllowed(address user_, bytes calldata /* data_ - unused */ ) external view override returns (bool) { - return _allowed[user_]; - } - /// @notice Set the allowed status of a user /// @param users_ The list of users to update /// @param allowed_ The allowed status of each user /// @dev The length of the `users_` and `allowed_` arrays must be the same /// @dev This function can only be called by the owner - function setAllowed(address[] calldata users_, bool[] calldata allowed_) external onlyRoles(LIST_MANAGER_ROLE) { - if (users_.length != allowed_.length) revert BoostError.LengthMismatch(); - - for (uint256 i = 0; i < users_.length; i++) { - _allowed[users_[i]] = allowed_[i]; - } - } + function setAllowed(address[] calldata users_, bool[] calldata allowed_) external virtual; /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { diff --git a/packages/evm/contracts/allowlists/ASimpleDenyList.sol b/packages/evm/contracts/allowlists/ASimpleDenyList.sol index 6fa89bb52..41a424932 100644 --- a/packages/evm/contracts/allowlists/ASimpleDenyList.sol +++ b/packages/evm/contracts/allowlists/ASimpleDenyList.sol @@ -1,37 +1,19 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -import {BoostError} from "contracts/shared/BoostError.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; import {AAllowList} from "contracts/allowlists/AAllowList.sol"; -/// @title SimpleDenyList +/// @title ASimpleDenyList /// @notice A simple implementation of an AllowList that implicitly allows all addresses except those explicitly added to the deny list abstract contract ASimpleDenyList is AAllowList { - /// @dev An internal mapping of denied statuses - mapping(address => bool) internal _denied; - - /// @notice Check if a user is authorized (i.e. not denied) - /// @param user_ The address of the user - /// @param - The data payload for the authorization check, not used in this implementation - /// @return True if the user is authorized - function isAllowed(address user_, bytes calldata /* data_ - unused */ ) external view override returns (bool) { - return !_denied[user_]; - } - /// @notice Set the denied status of a user /// @param users_ The list of users to update /// @param denied_ The denied status of each user /// @dev The length of the `users_` and `denied_` arrays must be the same /// @dev This function can only be called by the owner - function setDenied(address[] calldata users_, bool[] calldata denied_) external onlyOwner { - if (users_.length != denied_.length) revert BoostError.LengthMismatch(); - - for (uint256 i = 0; i < users_.length; i++) { - _denied[users_[i]] = denied_[i]; - } - } + function setDenied(address[] calldata users_, bool[] calldata denied_) external virtual; /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { diff --git a/packages/evm/contracts/allowlists/SimpleAllowList.sol b/packages/evm/contracts/allowlists/SimpleAllowList.sol index 22b1b0dd1..4f8501d14 100644 --- a/packages/evm/contracts/allowlists/SimpleAllowList.sol +++ b/packages/evm/contracts/allowlists/SimpleAllowList.sol @@ -10,7 +10,10 @@ import {ASimpleAllowList} from "contracts/allowlists/ASimpleAllowList.sol"; /// @title Simple AllowList /// @notice A simple implementation of an AllowList that checks if a user is authorized based on a list of allowed addresses -contract SimpleAllowList is ASimpleAllowList { +contract SimpleAllowList is ASimpleAllowList, OwnableRoles { + /// @dev An internal mapping of allowed statuses + mapping(address => bool) internal _allowed; + /// @notice Construct a new SimpleAllowList /// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function. constructor() { @@ -27,4 +30,29 @@ contract SimpleAllowList is ASimpleAllowList { _allowed[allowList_[i]] = true; } } + + /// @notice Check if a user is authorized + /// @param user_ The address of the user + /// @param - The data payload for the authorization check, not used in this implementation + /// @return True if the user is authorized + function isAllowed(address user_, bytes calldata /* data_ - unused */ ) external view override returns (bool) { + return _allowed[user_]; + } + + /// @notice Set the allowed status of a user + /// @param users_ The list of users to update + /// @param allowed_ The allowed status of each user + /// @dev The length of the `users_` and `allowed_` arrays must be the same + /// @dev This function can only be called by the owner + function setAllowed(address[] calldata users_, bool[] calldata allowed_) + external + override + onlyRoles(LIST_MANAGER_ROLE) + { + if (users_.length != allowed_.length) revert BoostError.LengthMismatch(); + + for (uint256 i = 0; i < users_.length; i++) { + _allowed[users_[i]] = allowed_[i]; + } + } } diff --git a/packages/evm/contracts/allowlists/SimpleDenyList.sol b/packages/evm/contracts/allowlists/SimpleDenyList.sol index 953671c4c..1a00e60b1 100644 --- a/packages/evm/contracts/allowlists/SimpleDenyList.sol +++ b/packages/evm/contracts/allowlists/SimpleDenyList.sol @@ -1,11 +1,17 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; +import {Ownable as AOwnable} from "@solady/auth/Ownable.sol"; + import {ASimpleDenyList} from "contracts/allowlists/ASimpleDenyList.sol"; +import {BoostError} from "contracts/shared/BoostError.sol"; /// @title SimpleDenyList /// @notice A simple implementation of an AllowList that implicitly allows all addresses except those explicitly added to the deny list -contract SimpleDenyList is ASimpleDenyList { +contract SimpleDenyList is AOwnable, ASimpleDenyList { + /// @dev An internal mapping of denied statuses + mapping(address => bool) internal _denied; + /// @notice Construct a new SimpleDenyList /// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function. constructor() { @@ -22,4 +28,25 @@ contract SimpleDenyList is ASimpleDenyList { _denied[denyList_[i]] = true; } } + + /// @notice Check if a user is authorized (i.e. not denied) + /// @param user_ The address of the user + /// @param - The data payload for the authorization check, not used in this implementation + /// @return True if the user is authorized + function isAllowed(address user_, bytes calldata /* data_ - unused */ ) external view override returns (bool) { + return !_denied[user_]; + } + + /// @notice Set the denied status of a user + /// @param users_ The list of users to update + /// @param denied_ The denied status of each user + /// @dev The length of the `users_` and `denied_` arrays must be the same + /// @dev This function can only be called by the owner + function setDenied(address[] calldata users_, bool[] calldata denied_) external override onlyOwner { + if (users_.length != denied_.length) revert BoostError.LengthMismatch(); + + for (uint256 i = 0; i < users_.length; i++) { + _denied[users_[i]] = denied_[i]; + } + } } diff --git a/packages/evm/contracts/budgets/AManagedBudget.sol b/packages/evm/contracts/budgets/AManagedBudget.sol index d425de7f3..481e796d5 100644 --- a/packages/evm/contracts/budgets/AManagedBudget.sol +++ b/packages/evm/contracts/budgets/AManagedBudget.sol @@ -24,317 +24,33 @@ abstract contract AManagedBudget is ABudget, OwnableRoles, IERC1155Receiver, Ree /// @notice The role for depositing, withdrawal, and manager management uint256 public constant ADMIN_ROLE = _ROLE_1; - /// @dev The total amount of each fungible asset distributed from the budget - mapping(address => uint256) private _distributedFungible; - - /// @dev The total amount of each ERC1155 asset and token ID distributed from the budget - mapping(address => mapping(uint256 => uint256)) private _distributedERC1155; - - /// @inheritdoc ABudget - /// @notice Allocates assets to the budget - /// @param data_ The packed data for the {Transfer} request - /// @return True if the allocation was successful - /// @dev The caller must have already approved the contract to transfer the asset - /// @dev If the asset transfer fails, the allocation will revert - function allocate(bytes calldata data_) external payable virtual override returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ETH) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - // Ensure the value received is equal to the `payload.amount` - if (msg.value != payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else if (request.assetType == AssetType.ERC20) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - // Transfer `payload.amount` of the token to this contract - request.asset.safeTransferFrom(request.target, address(this), payload.amount); - if (request.asset.balanceOf(address(this)) < payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else if (request.assetType == AssetType.ERC1155) { - ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); - - // Transfer `payload.amount` of `payload.tokenId` to this contract - IERC1155(request.asset).safeTransferFrom( - request.target, address(this), payload.tokenId, payload.amount, payload.data - ); - if (IERC1155(request.asset).balanceOf(address(this), payload.tokenId) < payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else { - // Unsupported asset type - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Reclaims assets from the budget if sender is owner or admin - /// @param data_ The packed {Transfer} request - /// @return True if the request was successful - /// @dev Only admins can directly reclaim assets from the budget - /// @dev If the amount is zero, the entire balance of the asset will be transferred to the receiver - /// @dev If the asset transfer fails, the reclamation will revert - function clawback(bytes calldata data_) external virtual override onlyOwnerOrRoles(ADMIN_ROLE) returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - _transferFungible( - request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount - ); - } else if (request.assetType == AssetType.ERC1155) { - ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); - _transferERC1155( - request.asset, - request.target, - payload.tokenId, - payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount, - payload.data - ); - } else { - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Disburses assets from the budget to a single recipient if sender is owner, admin, or manager - /// @param data_ The packed {Transfer} request - /// @return True if the disbursement was successful - /// @dev If the asset transfer fails, the disbursement will revert - function disburse(bytes calldata data_) - public - virtual - override - onlyOwnerOrRoles(ADMIN_ROLE | MANAGER_ROLE) - returns (bool) - { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - uint256 avail = available(request.asset); - if (payload.amount > avail) { - revert InsufficientFunds(request.asset, avail, payload.amount); - } - - _transferFungible(request.asset, request.target, payload.amount); - } else if (request.assetType == AssetType.ERC1155) { - ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); - - uint256 avail = IERC1155(request.asset).balanceOf(address(this), payload.tokenId); - if (payload.amount > avail) { - revert InsufficientFunds(request.asset, avail, payload.amount); - } - - _transferERC1155(request.asset, request.target, payload.tokenId, payload.amount, payload.data); - } else { - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Disburses assets from the budget to multiple recipients - /// @param data_ The packed array of {Transfer} requests - /// @return True if all disbursements were successful - function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) { - for (uint256 i = 0; i < data_.length; i++) { - if (!disburse(data_[i])) return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @dev Checks if account has any level of authorization - function isAuthorized(address account_) public view virtual override returns (bool) { - return owner() == account_ || hasAnyRole(account_, MANAGER_ROLE | ADMIN_ROLE); - } - - /// @inheritdoc ABudget - /// @dev If authorization is true, grant manager role, otherwise revoke manager role. - function setAuthorized(address[] calldata accounts_, bool[] calldata authorized_) - external - virtual - override - onlyOwnerOrRoles(ADMIN_ROLE) - { - if (accounts_.length != authorized_.length) { - revert BoostError.LengthMismatch(); - } - for (uint256 i = 0; i < accounts_.length; i++) { - bool authorization = authorized_[i]; - if (authorization == true) { - _grantRoles(accounts_[i], MANAGER_ROLE); - } else { - _removeRoles(accounts_[i], MANAGER_ROLE); - } - } - } - /// @notice Set roles for accounts authoried to use the budget /// @param accounts_ The accounts to assign the corresponding role by index /// @param roles_ The roles to assign - function grantRoles(address[] calldata accounts_, uint256[] calldata roles_) - external - virtual - onlyOwnerOrRoles(ADMIN_ROLE) - { - if (accounts_.length != roles_.length) { - revert BoostError.LengthMismatch(); - } - for (uint256 i = 0; i < accounts_.length; i++) { - _grantRoles(accounts_[i], roles_[i]); - } - } + function grantRoles(address[] calldata accounts_, uint256[] calldata roles_) external virtual; /// @notice Revoke roles for accounts authoried to use the budget /// @param accounts_ The accounts to assign the corresponding role by index /// @param roles_ The roles to remove - function revokeRoles(address[] calldata accounts_, uint256[] calldata roles_) - external - virtual - onlyOwnerOrRoles(ADMIN_ROLE) - { - if (accounts_.length != roles_.length) { - revert BoostError.LengthMismatch(); - } - for (uint256 i = 0; i < accounts_.length; i++) { - _removeRoles(accounts_[i], roles_[i]); - } - } - - /// @inheritdoc ABudget - /// @notice Get the total amount of assets allocated to the budget, including any that have been distributed - /// @param asset_ The address of the asset - /// @return The total amount of assets - /// @dev This is simply the sum of the current balance and the distributed amount - function total(address asset_) external view virtual override returns (uint256) { - return available(asset_) + _distributedFungible[asset_]; - } + function revokeRoles(address[] calldata accounts_, uint256[] calldata roles_) external virtual; /// @notice Get the total amount of ERC1155 assets allocated to the budget, including any that have been distributed /// @param asset_ The address of the asset /// @param tokenId_ The ID of the token /// @return The total amount of assets - function total(address asset_, uint256 tokenId_) external view virtual returns (uint256) { - return IERC1155(asset_).balanceOf(address(this), tokenId_) + _distributedERC1155[asset_][tokenId_]; - } - - /// @inheritdoc ABudget - /// @notice Get the amount of assets available for distribution from the budget - /// @param asset_ The address of the asset (or the zero address for native assets) - /// @return The amount of assets available - /// @dev This is simply the current balance held by the budget - /// @dev If the zero address is passed, this function will return the native balance - function available(address asset_) public view virtual override returns (uint256) { - return asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); - } + function total(address asset_, uint256 tokenId_) external view virtual returns (uint256); /// @notice Get the amount of ERC1155 assets available for distribution from the budget /// @param asset_ The address of the asset /// @param tokenId_ The ID of the token /// @return The amount of assets available - function available(address asset_, uint256 tokenId_) public view virtual returns (uint256) { - return IERC1155(asset_).balanceOf(address(this), tokenId_); - } - - /// @inheritdoc ABudget - /// @notice Get the amount of assets that have been distributed from the budget - /// @param asset_ The address of the asset - /// @return The amount of assets distributed - function distributed(address asset_) external view virtual override returns (uint256) { - return _distributedFungible[asset_]; - } + function available(address asset_, uint256 tokenId_) public view virtual returns (uint256); /// @notice Get the amount of ERC1155 assets that have been distributed from the budget /// @param asset_ The address of the asset /// @param tokenId_ The ID of the token /// @return The amount of assets distributed - function distributed(address asset_, uint256 tokenId_) external view virtual returns (uint256) { - return _distributedERC1155[asset_][tokenId_]; - } - - /// @inheritdoc ABudget - /// @dev This is a no-op as there is no local balance to reconcile - function reconcile(bytes calldata) external virtual override returns (uint256) { - return 0; - } - - /// @notice Transfer assets to the recipient - /// @param asset_ The address of the asset - /// @param to_ The address of the recipient - /// @param amount_ The amount of the asset to transfer - /// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract) - /// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert - function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant { - // Increment the total amount of the asset distributed from the budget - if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); - if (amount_ > available(asset_)) { - revert InsufficientFunds(asset_, available(asset_), amount_); - } - - _distributedFungible[asset_] += amount_; - - // Transfer the asset to the recipient - if (asset_ == address(0)) { - SafeTransferLib.safeTransferETH(to_, amount_); - } else { - asset_.safeTransfer(to_, amount_); - } - - emit Distributed(asset_, to_, amount_); - } - - function _transferERC1155(address asset_, address to_, uint256 tokenId_, uint256 amount_, bytes memory data_) - internal - virtual - nonReentrant - { - // Increment the total amount of the asset distributed from the budget - if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); - if (amount_ > available(asset_, tokenId_)) { - revert InsufficientFunds(asset_, available(asset_, tokenId_), amount_); - } - - _distributedERC1155[asset_][tokenId_] += amount_; - - // Transfer the asset to the recipient - // wake-disable-next-line reentrancy (`nonReentrant` modifier is applied to the function) - IERC1155(asset_).safeTransferFrom(address(this), to_, tokenId_, amount_, data_); - - emit Distributed(asset_, to_, amount_); - } - - /// @inheritdoc IERC1155Receiver - /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - // We don't need to do anything here - return IERC1155Receiver.onERC1155Received.selector; - } - - /// @inheritdoc IERC1155Receiver - /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - // We don't need to do anything here - return IERC1155Receiver.onERC1155BatchReceived.selector; - } + function distributed(address asset_, uint256 tokenId_) external view virtual returns (uint256); /// @inheritdoc ACloneable function supportsInterface(bytes4 interfaceId) public view virtual override(ABudget, IERC165) returns (bool) { diff --git a/packages/evm/contracts/budgets/ASimpleBudget.sol b/packages/evm/contracts/budgets/ASimpleBudget.sol index 45c870cb8..33e061406 100644 --- a/packages/evm/contracts/budgets/ASimpleBudget.sol +++ b/packages/evm/contracts/budgets/ASimpleBudget.sol @@ -16,282 +16,6 @@ import {ACloneable} from "contracts/shared/ACloneable.sol"; /// @notice A minimal budget implementation that simply holds and distributes tokens (ERC20-like and native) /// @dev This type of budget supports ETH, ERC20, and ERC1155 assets only abstract contract ASimpleBudget is ABudget, IERC1155Receiver, ReentrancyGuard { - using SafeTransferLib for address; - - /// @dev The total amount of each fungible asset distributed from the budget - mapping(address => uint256) private _distributedFungible; - - /// @dev The total amount of each ERC1155 asset and token ID distributed from the budget - mapping(address => mapping(uint256 => uint256)) private _distributedERC1155; - - /// @dev The mapping of authorized addresses - mapping(address => bool) internal _isAuthorized; - - /// @notice A modifier that allows only authorized addresses to call the function - modifier onlyAuthorized() { - if (!isAuthorized(msg.sender)) revert Unauthorized(); - _; - } - - /// @inheritdoc ABudget - /// @notice Allocates assets to the budget - /// @param data_ The packed data for the {Transfer} request - /// @return True if the allocation was successful - /// @dev The caller must have already approved the contract to transfer the asset - /// @dev If the asset transfer fails, the allocation will revert - function allocate(bytes calldata data_) external payable virtual override returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ETH) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - // Ensure the value received is equal to the `payload.amount` - if (msg.value != payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else if (request.assetType == AssetType.ERC20) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - // Transfer `payload.amount` of the token to this contract - request.asset.safeTransferFrom(request.target, address(this), payload.amount); - if (request.asset.balanceOf(address(this)) < payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else if (request.assetType == AssetType.ERC1155) { - ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); - - // Transfer `payload.amount` of `payload.tokenId` to this contract - IERC1155(request.asset).safeTransferFrom( - request.target, address(this), payload.tokenId, payload.amount, payload.data - ); - if (IERC1155(request.asset).balanceOf(address(this), payload.tokenId) < payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else { - // Unsupported asset type - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Reclaims assets from the budget - /// @param data_ The packed {Transfer} request - /// @return True if the request was successful - /// @dev Only the owner can directly reclaim assets from the budget - /// @dev If the amount is zero, the entire balance of the asset will be transferred to the receiver - /// @dev If the asset transfer fails, the reclamation will revert - function clawback(bytes calldata data_) external virtual override onlyOwner returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - _transferFungible( - request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount - ); - } else if (request.assetType == AssetType.ERC1155) { - ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); - _transferERC1155( - request.asset, - request.target, - payload.tokenId, - payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount, - payload.data - ); - } else { - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Disburses assets from the budget to a single recipient - /// @param data_ The packed {Transfer} request - /// @return True if the disbursement was successful - /// @dev If the asset transfer fails, the disbursement will revert - function disburse(bytes calldata data_) public virtual override onlyAuthorized returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - uint256 avail = available(request.asset); - if (payload.amount > avail) { - revert InsufficientFunds(request.asset, avail, payload.amount); - } - - _transferFungible(request.asset, request.target, payload.amount); - } else if (request.assetType == AssetType.ERC1155) { - ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); - - uint256 avail = IERC1155(request.asset).balanceOf(address(this), payload.tokenId); - if (payload.amount > avail) { - revert InsufficientFunds(request.asset, avail, payload.amount); - } - - _transferERC1155(request.asset, request.target, payload.tokenId, payload.amount, payload.data); - } else { - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Disburses assets from the budget to multiple recipients - /// @param data_ The packed array of {Transfer} requests - /// @return True if all disbursements were successful - function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) { - for (uint256 i = 0; i < data_.length; i++) { - if (!disburse(data_[i])) return false; - } - - return true; - } - - /// @inheritdoc ABudget - function setAuthorized(address[] calldata account_, bool[] calldata authorized_) - external - virtual - override - onlyOwner - { - if (account_.length != authorized_.length) revert BoostError.LengthMismatch(); - for (uint256 i = 0; i < account_.length; i++) { - _isAuthorized[account_[i]] = authorized_[i]; - } - } - - /// @inheritdoc ABudget - function isAuthorized(address account_) public view virtual override returns (bool) { - return _isAuthorized[account_] || account_ == owner(); - } - - /// @inheritdoc ABudget - /// @notice Get the total amount of assets allocated to the budget, including any that have been distributed - /// @param asset_ The address of the asset - /// @return The total amount of assets - /// @dev This is simply the sum of the current balance and the distributed amount - function total(address asset_) external view virtual override returns (uint256) { - return available(asset_) + _distributedFungible[asset_]; - } - - /// @notice Get the total amount of ERC1155 assets allocated to the budget, including any that have been distributed - /// @param asset_ The address of the asset - /// @param tokenId_ The ID of the token - /// @return The total amount of assets - function total(address asset_, uint256 tokenId_) external view virtual returns (uint256) { - return IERC1155(asset_).balanceOf(address(this), tokenId_) + _distributedERC1155[asset_][tokenId_]; - } - - /// @inheritdoc ABudget - /// @notice Get the amount of assets available for distribution from the budget - /// @param asset_ The address of the asset (or the zero address for native assets) - /// @return The amount of assets available - /// @dev This is simply the current balance held by the budget - /// @dev If the zero address is passed, this function will return the native balance - function available(address asset_) public view virtual override returns (uint256) { - return asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); - } - - /// @notice Get the amount of ERC1155 assets available for distribution from the budget - /// @param asset_ The address of the asset - /// @param tokenId_ The ID of the token - /// @return The amount of assets available - function available(address asset_, uint256 tokenId_) public view virtual returns (uint256) { - return IERC1155(asset_).balanceOf(address(this), tokenId_); - } - - /// @inheritdoc ABudget - /// @notice Get the amount of assets that have been distributed from the budget - /// @param asset_ The address of the asset - /// @return The amount of assets distributed - function distributed(address asset_) external view virtual override returns (uint256) { - return _distributedFungible[asset_]; - } - - /// @notice Get the amount of ERC1155 assets that have been distributed from the budget - /// @param asset_ The address of the asset - /// @param tokenId_ The ID of the token - /// @return The amount of assets distributed - function distributed(address asset_, uint256 tokenId_) external view virtual returns (uint256) { - return _distributedERC1155[asset_][tokenId_]; - } - - /// @inheritdoc ABudget - /// @dev This is a no-op as there is no local balance to reconcile - function reconcile(bytes calldata) external virtual override returns (uint256) { - return 0; - } - - /// @notice Transfer assets to the recipient - /// @param asset_ The address of the asset - /// @param to_ The address of the recipient - /// @param amount_ The amount of the asset to transfer - /// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract) - /// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert - function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant { - // Increment the total amount of the asset distributed from the budget - if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); - if (amount_ > available(asset_)) { - revert InsufficientFunds(asset_, available(asset_), amount_); - } - - _distributedFungible[asset_] += amount_; - - // Transfer the asset to the recipient - if (asset_ == address(0)) { - SafeTransferLib.safeTransferETH(to_, amount_); - } else { - asset_.safeTransfer(to_, amount_); - } - - emit Distributed(asset_, to_, amount_); - } - - function _transferERC1155(address asset_, address to_, uint256 tokenId_, uint256 amount_, bytes memory data_) - internal - virtual - nonReentrant - { - // Increment the total amount of the asset distributed from the budget - if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); - if (amount_ > available(asset_, tokenId_)) { - revert InsufficientFunds(asset_, available(asset_, tokenId_), amount_); - } - - _distributedERC1155[asset_][tokenId_] += amount_; - - // Transfer the asset to the recipient - // wake-disable-next-line reentrancy (`nonReentrant` modifier is applied to the function) - IERC1155(asset_).safeTransferFrom(address(this), to_, tokenId_, amount_, data_); - - emit Distributed(asset_, to_, amount_); - } - - /// @inheritdoc IERC1155Receiver - /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - // We don't need to do anything here - return IERC1155Receiver.onERC1155Received.selector; - } - - /// @inheritdoc IERC1155Receiver - /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - // We don't need to do anything here - return IERC1155Receiver.onERC1155BatchReceived.selector; - } - /// @inheritdoc ACloneable function supportsInterface(bytes4 interfaceId) public view virtual override(ABudget, IERC165) returns (bool) { return interfaceId == type(ASimpleBudget).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId diff --git a/packages/evm/contracts/budgets/AVestingBudget.sol b/packages/evm/contracts/budgets/AVestingBudget.sol index 77ea0fa55..9f95e465f 100644 --- a/packages/evm/contracts/budgets/AVestingBudget.sol +++ b/packages/evm/contracts/budgets/AVestingBudget.sol @@ -22,12 +22,6 @@ import {ACloneable} from "contracts/shared/ACloneable.sol"; abstract contract AVestingBudget is ABudget, ReentrancyGuard { using SafeTransferLib for address; - /// @dev The total amount of each fungible asset distributed from the budget - mapping(address => uint256) private _distributedFungible; - - /// @dev The mapping of authorized addresses - mapping(address => bool) internal _isAuthorized; - /// @notice The timestamp at which the vesting schedule begins uint64 public start; @@ -37,197 +31,9 @@ abstract contract AVestingBudget is ABudget, ReentrancyGuard { /// @notice The duration of the cliff period (in seconds) uint64 public cliff; - /// @notice A modifier that allows only authorized addresses to call the function - modifier onlyAuthorized() { - if (!isAuthorized(msg.sender)) revert Unauthorized(); - _; - } - - /// @inheritdoc ABudget - /// @notice Allocates assets to the budget - /// @param data_ The packed data for the {Transfer} request - /// @return True if the allocation was successful - /// @dev The caller must have already approved the contract to transfer the asset - /// @dev If the asset transfer fails, the allocation will revert - function allocate(bytes calldata data_) external payable virtual override returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ETH) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - // Ensure the value received is equal to the `payload.amount` - if (msg.value != payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else if (request.assetType == AssetType.ERC20) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - - // Transfer `payload.amount` of the token to this contract - request.asset.safeTransferFrom(request.target, address(this), payload.amount); - if (request.asset.balanceOf(address(this)) < payload.amount) { - revert InvalidAllocation(request.asset, payload.amount); - } - } else { - // Unsupported asset type - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Reclaims assets from the budget - /// @param data_ The packed {Transfer} request - /// @return True if the request was successful - /// @dev Only the owner can directly reclaim assets from the budget, and this action is not subject to the vesting schedule - /// @dev If the amount is zero, the entire available balance of the asset will be transferred to the receiver - /// @dev If the asset transfer fails for any reason, the function will revert - function clawback(bytes calldata data_) external virtual override onlyOwner returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - _transferFungible( - request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount - ); - } else { - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Disburses assets from the budget to a single recipient - /// @param data_ The packed {Transfer} request - /// @return True if the disbursement was successful - /// @dev The maximum amount that can be disbursed is the {available} amount - function disburse(bytes calldata data_) public virtual override onlyAuthorized returns (bool) { - Transfer memory request = abi.decode(data_, (Transfer)); - if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) { - FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); - _transferFungible(request.asset, request.target, payload.amount); - } else { - return false; - } - - return true; - } - - /// @inheritdoc ABudget - /// @notice Disburses assets from the budget to multiple recipients - /// @param data_ The packed array of {Transfer} requests - /// @return True if all disbursements were successful - function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) { - for (uint256 i = 0; i < data_.length; i++) { - if (!disburse(data_[i])) return false; - } - - return true; - } - - /// @inheritdoc ABudget - function setAuthorized(address[] calldata account_, bool[] calldata authorized_) - external - virtual - override - onlyOwner - { - if (account_.length != authorized_.length) revert BoostError.LengthMismatch(); - for (uint256 i = 0; i < account_.length; i++) { - _isAuthorized[account_[i]] = authorized_[i]; - } - } - - /// @inheritdoc ABudget - function isAuthorized(address account_) public view virtual override returns (bool) { - return _isAuthorized[account_] || account_ == owner(); - } - /// @notice Get the end time of the vesting schedule /// @return The end time of the vesting schedule - function end() external view virtual returns (uint256) { - return start + duration; - } - - /// @inheritdoc ABudget - /// @notice Get the total amount of assets allocated to the budget, including any that have been distributed - /// @param asset_ The address of the asset - /// @return The total amount of assets - /// @dev This is equal to the sum of the total current balance and the total distributed amount - function total(address asset_) external view virtual override returns (uint256) { - uint256 balance = asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); - return _distributedFungible[asset_] + balance; - } - - /// @inheritdoc ABudget - /// @notice Get the amount of assets available for distribution from the budget as of the current block timestamp - /// @param asset_ The address of the asset (or the zero address for native assets) - /// @return The amount of assets currently available for distribution - /// @dev This is equal to the total vested amount minus any already distributed - function available(address asset_) public view virtual override returns (uint256) { - return _vestedAllocation(asset_, uint64(block.timestamp)) - _distributedFungible[asset_]; - } - - /// @inheritdoc ABudget - /// @notice Get the amount of assets that have been distributed from the budget - /// @param asset_ The address of the asset - /// @return The amount of assets distributed - function distributed(address asset_) external view virtual override returns (uint256) { - return _distributedFungible[asset_]; - } - - /// @inheritdoc ABudget - /// @dev This is a no-op as there is no local balance to reconcile - function reconcile(bytes calldata) external virtual override returns (uint256) { - return 0; - } - - /// @notice Transfer assets to the recipient - /// @param asset_ The address of the asset - /// @param to_ The address of the recipient - /// @param amount_ The amount of the asset to transfer - /// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract) - /// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert - function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant { - // Increment the total amount of the asset distributed from the budget - if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); - if (amount_ > available(asset_)) { - revert InsufficientFunds(asset_, available(asset_), amount_); - } - - _distributedFungible[asset_] += amount_; - - // Transfer the asset to the recipient - if (asset_ == address(0)) { - SafeTransferLib.safeTransferETH(to_, amount_); - } else { - asset_.safeTransfer(to_, amount_); - } - - emit Distributed(asset_, to_, amount_); - } - - /// @notice Calculate the portion of allocated assets vested at a given timestamp - /// @param asset_ The address of the asset - /// @param timestamp_ The timestamp used to calculate the vested amount - /// @return The amount of assets vested at that point in time - function _vestedAllocation(address asset_, uint64 timestamp_) internal view virtual returns (uint256) { - uint256 balance = asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); - return _linearVestedAmount(balance + _distributedFungible[asset_], timestamp_); - } - - /// @notice Calculate the amount of assets vested at a given timestamp using a linear vesting schedule - /// @param totalAllocation The total amount of the asset allocated to the budget (including prior distributions) - /// @param timestamp The timestamp used to calculate the vested amount - /// @return The amount of assets vested at that point in time - function _linearVestedAmount(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) { - if (timestamp < start + cliff) { - return 0; - } else if (timestamp >= start + duration) { - return totalAllocation; - } else { - return totalAllocation * (timestamp - start) / duration; - } - } + function end() external virtual returns (uint256); /// @inheritdoc ACloneable function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { diff --git a/packages/evm/contracts/budgets/ManagedBudget.sol b/packages/evm/contracts/budgets/ManagedBudget.sol index 92178559b..e1ef6c7e1 100644 --- a/packages/evm/contracts/budgets/ManagedBudget.sol +++ b/packages/evm/contracts/budgets/ManagedBudget.sol @@ -1,13 +1,23 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol"; + +import {BoostError} from "contracts/shared/BoostError.sol"; import {AManagedBudget} from "contracts/budgets/AManagedBudget.sol"; +import {ABudget} from "contracts/budgets/ABudget.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; /// @title Managed ABudget /// @notice A minimal budget implementation with RBAC that simply holds and distributes tokens (ERC20-like and native) /// @dev This type of budget supports ETH, ERC20, and ERC1155 assets only contract ManagedBudget is AManagedBudget { + using SafeTransferLib for address; + /// @notice The payload for initializing a ManagedBudget struct InitPayload { address owner; @@ -15,6 +25,12 @@ contract ManagedBudget is AManagedBudget { uint256[] roles; } + /// @dev The total amount of each fungible asset distributed from the budget + mapping(address => uint256) private _distributedFungible; + + /// @dev The total amount of each ERC1155 asset and token ID distributed from the budget + mapping(address => mapping(uint256 => uint256)) private _distributedERC1155; + /// @notice Construct a new ManagedBudget /// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function. constructor() { @@ -30,4 +46,312 @@ contract ManagedBudget is AManagedBudget { _setRoles(init_.authorized[i], init_.roles[i]); } } + + /// @inheritdoc ABudget + /// @notice Allocates assets to the budget + /// @param data_ The packed data for the {Transfer} request + /// @return True if the allocation was successful + /// @dev The caller must have already approved the contract to transfer the asset + /// @dev If the asset transfer fails, the allocation will revert + function allocate(bytes calldata data_) external payable virtual override returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ETH) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + // Ensure the value received is equal to the `payload.amount` + if (msg.value != payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else if (request.assetType == AssetType.ERC20) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + // Transfer `payload.amount` of the token to this contract + request.asset.safeTransferFrom(request.target, address(this), payload.amount); + if (request.asset.balanceOf(address(this)) < payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else if (request.assetType == AssetType.ERC1155) { + ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); + + // Transfer `payload.amount` of `payload.tokenId` to this contract + IERC1155(request.asset).safeTransferFrom( + request.target, address(this), payload.tokenId, payload.amount, payload.data + ); + if (IERC1155(request.asset).balanceOf(address(this), payload.tokenId) < payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else { + // Unsupported asset type + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Reclaims assets from the budget if sender is owner or admin + /// @param data_ The packed {Transfer} request + /// @return True if the request was successful + /// @dev Only admins can directly reclaim assets from the budget + /// @dev If the amount is zero, the entire balance of the asset will be transferred to the receiver + /// @dev If the asset transfer fails, the reclamation will revert + function clawback(bytes calldata data_) external virtual override onlyOwnerOrRoles(ADMIN_ROLE) returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + _transferFungible( + request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount + ); + } else if (request.assetType == AssetType.ERC1155) { + ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); + _transferERC1155( + request.asset, + request.target, + payload.tokenId, + payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount, + payload.data + ); + } else { + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Disburses assets from the budget to a single recipient if sender is owner, admin, or manager + /// @param data_ The packed {Transfer} request + /// @return True if the disbursement was successful + /// @dev If the asset transfer fails, the disbursement will revert + function disburse(bytes calldata data_) + public + virtual + override + onlyOwnerOrRoles(ADMIN_ROLE | MANAGER_ROLE) + returns (bool) + { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + uint256 avail = available(request.asset); + if (payload.amount > avail) { + revert InsufficientFunds(request.asset, avail, payload.amount); + } + + _transferFungible(request.asset, request.target, payload.amount); + } else if (request.assetType == AssetType.ERC1155) { + ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); + + uint256 avail = IERC1155(request.asset).balanceOf(address(this), payload.tokenId); + if (payload.amount > avail) { + revert InsufficientFunds(request.asset, avail, payload.amount); + } + + _transferERC1155(request.asset, request.target, payload.tokenId, payload.amount, payload.data); + } else { + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Disburses assets from the budget to multiple recipients + /// @param data_ The packed array of {Transfer} requests + /// @return True if all disbursements were successful + function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) { + for (uint256 i = 0; i < data_.length; i++) { + if (!disburse(data_[i])) return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @dev Checks if account has any level of authorization + function isAuthorized(address account_) public view virtual override returns (bool) { + return owner() == account_ || hasAnyRole(account_, MANAGER_ROLE | ADMIN_ROLE); + } + + /// @inheritdoc ABudget + /// @dev If authorization is true, grant manager role, otherwise revoke manager role. + function setAuthorized(address[] calldata accounts_, bool[] calldata authorized_) + external + virtual + override + onlyOwnerOrRoles(ADMIN_ROLE) + { + if (accounts_.length != authorized_.length) { + revert BoostError.LengthMismatch(); + } + for (uint256 i = 0; i < accounts_.length; i++) { + bool authorization = authorized_[i]; + if (authorization == true) { + _grantRoles(accounts_[i], MANAGER_ROLE); + } else { + _removeRoles(accounts_[i], MANAGER_ROLE); + } + } + } + + /// @notice Set roles for accounts authoried to use the budget + /// @param accounts_ The accounts to assign the corresponding role by index + /// @param roles_ The roles to assign + function grantRoles(address[] calldata accounts_, uint256[] calldata roles_) + external + virtual + override + onlyOwnerOrRoles(ADMIN_ROLE) + { + if (accounts_.length != roles_.length) { + revert BoostError.LengthMismatch(); + } + for (uint256 i = 0; i < accounts_.length; i++) { + _grantRoles(accounts_[i], roles_[i]); + } + } + + /// @notice Revoke roles for accounts authoried to use the budget + /// @param accounts_ The accounts to assign the corresponding role by index + /// @param roles_ The roles to remove + function revokeRoles(address[] calldata accounts_, uint256[] calldata roles_) + external + virtual + override + onlyOwnerOrRoles(ADMIN_ROLE) + { + if (accounts_.length != roles_.length) { + revert BoostError.LengthMismatch(); + } + for (uint256 i = 0; i < accounts_.length; i++) { + _removeRoles(accounts_[i], roles_[i]); + } + } + + /// @inheritdoc ABudget + /// @notice Get the total amount of assets allocated to the budget, including any that have been distributed + /// @param asset_ The address of the asset + /// @return The total amount of assets + /// @dev This is simply the sum of the current balance and the distributed amount + function total(address asset_) external view virtual override returns (uint256) { + return available(asset_) + _distributedFungible[asset_]; + } + + /// @notice Get the total amount of ERC1155 assets allocated to the budget, including any that have been distributed + /// @param asset_ The address of the asset + /// @param tokenId_ The ID of the token + /// @return The total amount of assets + function total(address asset_, uint256 tokenId_) external view virtual override returns (uint256) { + return IERC1155(asset_).balanceOf(address(this), tokenId_) + _distributedERC1155[asset_][tokenId_]; + } + + /// @inheritdoc ABudget + /// @notice Get the amount of assets available for distribution from the budget + /// @param asset_ The address of the asset (or the zero address for native assets) + /// @return The amount of assets available + /// @dev This is simply the current balance held by the budget + /// @dev If the zero address is passed, this function will return the native balance + function available(address asset_) public view virtual override returns (uint256) { + return asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); + } + + /// @notice Get the amount of ERC1155 assets available for distribution from the budget + /// @param asset_ The address of the asset + /// @param tokenId_ The ID of the token + /// @return The amount of assets available + function available(address asset_, uint256 tokenId_) public view virtual override returns (uint256) { + return IERC1155(asset_).balanceOf(address(this), tokenId_); + } + + /// @inheritdoc ABudget + /// @notice Get the amount of assets that have been distributed from the budget + /// @param asset_ The address of the asset + /// @return The amount of assets distributed + function distributed(address asset_) external view virtual override returns (uint256) { + return _distributedFungible[asset_]; + } + + /// @notice Get the amount of ERC1155 assets that have been distributed from the budget + /// @param asset_ The address of the asset + /// @param tokenId_ The ID of the token + /// @return The amount of assets distributed + function distributed(address asset_, uint256 tokenId_) external view virtual override returns (uint256) { + return _distributedERC1155[asset_][tokenId_]; + } + + /// @inheritdoc ABudget + /// @dev This is a no-op as there is no local balance to reconcile + function reconcile(bytes calldata) external virtual override returns (uint256) { + return 0; + } + + /// @notice Transfer assets to the recipient + /// @param asset_ The address of the asset + /// @param to_ The address of the recipient + /// @param amount_ The amount of the asset to transfer + /// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract) + /// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert + function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant { + // Increment the total amount of the asset distributed from the budget + if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); + if (amount_ > available(asset_)) { + revert InsufficientFunds(asset_, available(asset_), amount_); + } + + _distributedFungible[asset_] += amount_; + + // Transfer the asset to the recipient + if (asset_ == address(0)) { + SafeTransferLib.safeTransferETH(to_, amount_); + } else { + asset_.safeTransfer(to_, amount_); + } + + emit Distributed(asset_, to_, amount_); + } + + function _transferERC1155(address asset_, address to_, uint256 tokenId_, uint256 amount_, bytes memory data_) + internal + virtual + nonReentrant + { + // Increment the total amount of the asset distributed from the budget + if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); + if (amount_ > available(asset_, tokenId_)) { + revert InsufficientFunds(asset_, available(asset_, tokenId_), amount_); + } + + _distributedERC1155[asset_][tokenId_] += amount_; + + // Transfer the asset to the recipient + // wake-disable-next-line reentrancy (`nonReentrant` modifier is applied to the function) + IERC1155(asset_).safeTransferFrom(address(this), to_, tokenId_, amount_, data_); + + emit Distributed(asset_, to_, amount_); + } + + /// @inheritdoc IERC1155Receiver + /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + // We don't need to do anything here + return IERC1155Receiver.onERC1155Received.selector; + } + + /// @inheritdoc IERC1155Receiver + /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + // We don't need to do anything here + return IERC1155Receiver.onERC1155BatchReceived.selector; + } } diff --git a/packages/evm/contracts/budgets/SimpleBudget.sol b/packages/evm/contracts/budgets/SimpleBudget.sol index 371eb968b..2522b7f0b 100644 --- a/packages/evm/contracts/budgets/SimpleBudget.sol +++ b/packages/evm/contracts/budgets/SimpleBudget.sol @@ -1,13 +1,39 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -import {ASimpleBudget} from "contracts/budgets/ASimpleBudget.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol"; +import {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol"; + +import {BoostError} from "contracts/shared/BoostError.sol"; +import {ABudget} from "contracts/budgets/ABudget.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; +import {ASimpleBudget} from "contracts/budgets/ASimpleBudget.sol"; /// @title Simple ABudget /// @notice A minimal budget implementation that simply holds and distributes tokens (ERC20-like and native) /// @dev This type of budget supports ETH, ERC20, and ERC1155 assets only contract SimpleBudget is ASimpleBudget { + using SafeTransferLib for address; + + /// @dev The total amount of each fungible asset distributed from the budget + mapping(address => uint256) private _distributedFungible; + + /// @dev The total amount of each ERC1155 asset and token ID distributed from the budget + mapping(address => mapping(uint256 => uint256)) private _distributedERC1155; + + /// @dev The mapping of authorized addresses + mapping(address => bool) internal _isAuthorized; + + /// @notice A modifier that allows only authorized addresses to call the function + modifier onlyAuthorized() { + if (!isAuthorized(msg.sender)) revert Unauthorized(); + _; + } + /// @notice The payload for initializing a SimpleBudget struct InitPayload { address owner; @@ -29,4 +55,263 @@ contract SimpleBudget is ASimpleBudget { _isAuthorized[init_.authorized[i]] = true; } } + + /// @inheritdoc ABudget + /// @notice Allocates assets to the budget + /// @param data_ The packed data for the {Transfer} request + /// @return True if the allocation was successful + /// @dev The caller must have already approved the contract to transfer the asset + /// @dev If the asset transfer fails, the allocation will revert + function allocate(bytes calldata data_) external payable virtual override returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ETH) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + // Ensure the value received is equal to the `payload.amount` + if (msg.value != payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else if (request.assetType == AssetType.ERC20) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + // Transfer `payload.amount` of the token to this contract + request.asset.safeTransferFrom(request.target, address(this), payload.amount); + if (request.asset.balanceOf(address(this)) < payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else if (request.assetType == AssetType.ERC1155) { + ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); + + // Transfer `payload.amount` of `payload.tokenId` to this contract + IERC1155(request.asset).safeTransferFrom( + request.target, address(this), payload.tokenId, payload.amount, payload.data + ); + if (IERC1155(request.asset).balanceOf(address(this), payload.tokenId) < payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else { + // Unsupported asset type + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Reclaims assets from the budget + /// @param data_ The packed {Transfer} request + /// @return True if the request was successful + /// @dev Only the owner can directly reclaim assets from the budget + /// @dev If the amount is zero, the entire balance of the asset will be transferred to the receiver + /// @dev If the asset transfer fails, the reclamation will revert + function clawback(bytes calldata data_) external virtual override onlyOwner returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + _transferFungible( + request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount + ); + } else if (request.assetType == AssetType.ERC1155) { + ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); + _transferERC1155( + request.asset, + request.target, + payload.tokenId, + payload.amount == 0 ? IERC1155(request.asset).balanceOf(address(this), payload.tokenId) : payload.amount, + payload.data + ); + } else { + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Disburses assets from the budget to a single recipient + /// @param data_ The packed {Transfer} request + /// @return True if the disbursement was successful + /// @dev If the asset transfer fails, the disbursement will revert + function disburse(bytes calldata data_) public virtual override onlyAuthorized returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + uint256 avail = available(request.asset); + if (payload.amount > avail) { + revert InsufficientFunds(request.asset, avail, payload.amount); + } + + _transferFungible(request.asset, request.target, payload.amount); + } else if (request.assetType == AssetType.ERC1155) { + ERC1155Payload memory payload = abi.decode(request.data, (ERC1155Payload)); + + uint256 avail = IERC1155(request.asset).balanceOf(address(this), payload.tokenId); + if (payload.amount > avail) { + revert InsufficientFunds(request.asset, avail, payload.amount); + } + + _transferERC1155(request.asset, request.target, payload.tokenId, payload.amount, payload.data); + } else { + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Disburses assets from the budget to multiple recipients + /// @param data_ The packed array of {Transfer} requests + /// @return True if all disbursements were successful + function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) { + for (uint256 i = 0; i < data_.length; i++) { + if (!disburse(data_[i])) return false; + } + + return true; + } + + /// @inheritdoc ABudget + function setAuthorized(address[] calldata account_, bool[] calldata authorized_) + external + virtual + override + onlyOwner + { + if (account_.length != authorized_.length) revert BoostError.LengthMismatch(); + for (uint256 i = 0; i < account_.length; i++) { + _isAuthorized[account_[i]] = authorized_[i]; + } + } + + /// @inheritdoc ABudget + function isAuthorized(address account_) public view virtual override returns (bool) { + return _isAuthorized[account_] || account_ == owner(); + } + + /// @inheritdoc ABudget + /// @notice Get the total amount of assets allocated to the budget, including any that have been distributed + /// @param asset_ The address of the asset + /// @return The total amount of assets + /// @dev This is simply the sum of the current balance and the distributed amount + function total(address asset_) external view virtual override returns (uint256) { + return available(asset_) + _distributedFungible[asset_]; + } + + /// @notice Get the total amount of ERC1155 assets allocated to the budget, including any that have been distributed + /// @param asset_ The address of the asset + /// @param tokenId_ The ID of the token + /// @return The total amount of assets + function total(address asset_, uint256 tokenId_) external view virtual returns (uint256) { + return IERC1155(asset_).balanceOf(address(this), tokenId_) + _distributedERC1155[asset_][tokenId_]; + } + + /// @inheritdoc ABudget + /// @notice Get the amount of assets available for distribution from the budget + /// @param asset_ The address of the asset (or the zero address for native assets) + /// @return The amount of assets available + /// @dev This is simply the current balance held by the budget + /// @dev If the zero address is passed, this function will return the native balance + function available(address asset_) public view virtual override returns (uint256) { + return asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); + } + + /// @notice Get the amount of ERC1155 assets available for distribution from the budget + /// @param asset_ The address of the asset + /// @param tokenId_ The ID of the token + /// @return The amount of assets available + function available(address asset_, uint256 tokenId_) public view virtual returns (uint256) { + return IERC1155(asset_).balanceOf(address(this), tokenId_); + } + + /// @inheritdoc ABudget + /// @notice Get the amount of assets that have been distributed from the budget + /// @param asset_ The address of the asset + /// @return The amount of assets distributed + function distributed(address asset_) external view virtual override returns (uint256) { + return _distributedFungible[asset_]; + } + + /// @notice Get the amount of ERC1155 assets that have been distributed from the budget + /// @param asset_ The address of the asset + /// @param tokenId_ The ID of the token + /// @return The amount of assets distributed + function distributed(address asset_, uint256 tokenId_) external view virtual returns (uint256) { + return _distributedERC1155[asset_][tokenId_]; + } + + /// @inheritdoc ABudget + /// @dev This is a no-op as there is no local balance to reconcile + function reconcile(bytes calldata) external virtual override returns (uint256) { + return 0; + } + + /// @notice Transfer assets to the recipient + /// @param asset_ The address of the asset + /// @param to_ The address of the recipient + /// @param amount_ The amount of the asset to transfer + /// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract) + /// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert + function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant { + // Increment the total amount of the asset distributed from the budget + if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); + if (amount_ > available(asset_)) { + revert InsufficientFunds(asset_, available(asset_), amount_); + } + + _distributedFungible[asset_] += amount_; + + // Transfer the asset to the recipient + if (asset_ == address(0)) { + SafeTransferLib.safeTransferETH(to_, amount_); + } else { + asset_.safeTransfer(to_, amount_); + } + + emit Distributed(asset_, to_, amount_); + } + + function _transferERC1155(address asset_, address to_, uint256 tokenId_, uint256 amount_, bytes memory data_) + internal + virtual + nonReentrant + { + // Increment the total amount of the asset distributed from the budget + if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); + if (amount_ > available(asset_, tokenId_)) { + revert InsufficientFunds(asset_, available(asset_, tokenId_), amount_); + } + + _distributedERC1155[asset_][tokenId_] += amount_; + + // Transfer the asset to the recipient + // wake-disable-next-line reentrancy (`nonReentrant` modifier is applied to the function) + IERC1155(asset_).safeTransferFrom(address(this), to_, tokenId_, amount_, data_); + + emit Distributed(asset_, to_, amount_); + } + + /// @inheritdoc IERC1155Receiver + /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + // We don't need to do anything here + return IERC1155Receiver.onERC1155Received.selector; + } + + /// @inheritdoc IERC1155Receiver + /// @dev This contract does not care about the specifics of the inbound token, so we simply return the magic value (i.e. the selector for `onERC1155Received`) + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + // We don't need to do anything here + return IERC1155Receiver.onERC1155BatchReceived.selector; + } } diff --git a/packages/evm/contracts/budgets/VestingBudget.sol b/packages/evm/contracts/budgets/VestingBudget.sol index 86d3d25b6..a977d5ceb 100644 --- a/packages/evm/contracts/budgets/VestingBudget.sol +++ b/packages/evm/contracts/budgets/VestingBudget.sol @@ -21,6 +21,8 @@ import {AVestingBudget} from "contracts/budgets/AVestingBudget.sol"; /// - A vesting budget can also act as a time-lock, unlocking all assets at a specified point in time. To release assets at a specific time rather than vesting them over time, set the `start` to the desired time and the `duration` to zero. /// - This contract is {Ownable} to enable the owner to allocate to the budget, reclaim and disburse assets from the budget, and to set authorized addresses. Additionally, the owner can transfer ownership of the budget to another address. Doing so has no effect on the vesting schedule. contract VestingBudget is AVestingBudget { + using SafeTransferLib for address; + /// @notice The payload for initializing a VestingBudget struct InitPayload { address owner; @@ -30,6 +32,18 @@ contract VestingBudget is AVestingBudget { uint64 cliff; } + /// @dev The total amount of each fungible asset distributed from the budget + mapping(address => uint256) private _distributedFungible; + + /// @dev The mapping of authorized addresses + mapping(address => bool) internal _isAuthorized; + + /// @notice A modifier that allows only authorized addresses to call the function + modifier onlyAuthorized() { + if (!isAuthorized(msg.sender)) revert Unauthorized(); + _; + } + /// @notice Construct a new SimpleBudget /// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function. constructor() { @@ -51,4 +65,189 @@ contract VestingBudget is AVestingBudget { } } + /// @inheritdoc ABudget + /// @notice Allocates assets to the budget + /// @param data_ The packed data for the {Transfer} request + /// @return True if the allocation was successful + /// @dev The caller must have already approved the contract to transfer the asset + /// @dev If the asset transfer fails, the allocation will revert + function allocate(bytes calldata data_) external payable virtual override returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ETH) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + // Ensure the value received is equal to the `payload.amount` + if (msg.value != payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else if (request.assetType == AssetType.ERC20) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + + // Transfer `payload.amount` of the token to this contract + request.asset.safeTransferFrom(request.target, address(this), payload.amount); + if (request.asset.balanceOf(address(this)) < payload.amount) { + revert InvalidAllocation(request.asset, payload.amount); + } + } else { + // Unsupported asset type + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Reclaims assets from the budget + /// @param data_ The packed {Transfer} request + /// @return True if the request was successful + /// @dev Only the owner can directly reclaim assets from the budget, and this action is not subject to the vesting schedule + /// @dev If the amount is zero, the entire available balance of the asset will be transferred to the receiver + /// @dev If the asset transfer fails for any reason, the function will revert + function clawback(bytes calldata data_) external virtual override onlyOwner returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + _transferFungible( + request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount + ); + } else { + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Disburses assets from the budget to a single recipient + /// @param data_ The packed {Transfer} request + /// @return True if the disbursement was successful + /// @dev The maximum amount that can be disbursed is the {available} amount + function disburse(bytes calldata data_) public virtual override onlyAuthorized returns (bool) { + Transfer memory request = abi.decode(data_, (Transfer)); + if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) { + FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload)); + _transferFungible(request.asset, request.target, payload.amount); + } else { + return false; + } + + return true; + } + + /// @inheritdoc ABudget + /// @notice Disburses assets from the budget to multiple recipients + /// @param data_ The packed array of {Transfer} requests + /// @return True if all disbursements were successful + function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) { + for (uint256 i = 0; i < data_.length; i++) { + if (!disburse(data_[i])) return false; + } + + return true; + } + + /// @inheritdoc ABudget + function setAuthorized(address[] calldata account_, bool[] calldata authorized_) + external + virtual + override + onlyOwner + { + if (account_.length != authorized_.length) revert BoostError.LengthMismatch(); + for (uint256 i = 0; i < account_.length; i++) { + _isAuthorized[account_[i]] = authorized_[i]; + } + } + + /// @inheritdoc ABudget + function isAuthorized(address account_) public view virtual override returns (bool) { + return _isAuthorized[account_] || account_ == owner(); + } + + /// @notice Get the end time of the vesting schedule + /// @return The end time of the vesting schedule + function end() external view virtual override returns (uint256) { + return start + duration; + } + + /// @inheritdoc ABudget + /// @notice Get the total amount of assets allocated to the budget, including any that have been distributed + /// @param asset_ The address of the asset + /// @return The total amount of assets + /// @dev This is equal to the sum of the total current balance and the total distributed amount + function total(address asset_) external view virtual override returns (uint256) { + uint256 balance = asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); + return _distributedFungible[asset_] + balance; + } + + /// @inheritdoc ABudget + /// @notice Get the amount of assets available for distribution from the budget as of the current block timestamp + /// @param asset_ The address of the asset (or the zero address for native assets) + /// @return The amount of assets currently available for distribution + /// @dev This is equal to the total vested amount minus any already distributed + function available(address asset_) public view virtual override returns (uint256) { + return _vestedAllocation(asset_, uint64(block.timestamp)) - _distributedFungible[asset_]; + } + + /// @inheritdoc ABudget + /// @notice Get the amount of assets that have been distributed from the budget + /// @param asset_ The address of the asset + /// @return The amount of assets distributed + function distributed(address asset_) external view virtual override returns (uint256) { + return _distributedFungible[asset_]; + } + + /// @inheritdoc ABudget + /// @dev This is a no-op as there is no local balance to reconcile + function reconcile(bytes calldata) external virtual override returns (uint256) { + return 0; + } + + /// @notice Transfer assets to the recipient + /// @param asset_ The address of the asset + /// @param to_ The address of the recipient + /// @param amount_ The amount of the asset to transfer + /// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract) + /// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert + function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant { + // Increment the total amount of the asset distributed from the budget + if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_); + if (amount_ > available(asset_)) { + revert InsufficientFunds(asset_, available(asset_), amount_); + } + + _distributedFungible[asset_] += amount_; + + // Transfer the asset to the recipient + if (asset_ == address(0)) { + SafeTransferLib.safeTransferETH(to_, amount_); + } else { + asset_.safeTransfer(to_, amount_); + } + + emit Distributed(asset_, to_, amount_); + } + + /// @notice Calculate the portion of allocated assets vested at a given timestamp + /// @param asset_ The address of the asset + /// @param timestamp_ The timestamp used to calculate the vested amount + /// @return The amount of assets vested at that point in time + function _vestedAllocation(address asset_, uint64 timestamp_) internal view virtual returns (uint256) { + uint256 balance = asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); + return _linearVestedAmount(balance + _distributedFungible[asset_], timestamp_); + } + + /// @notice Calculate the amount of assets vested at a given timestamp using a linear vesting schedule + /// @param totalAllocation The total amount of the asset allocated to the budget (including prior distributions) + /// @param timestamp The timestamp used to calculate the vested amount + /// @return The amount of assets vested at that point in time + function _linearVestedAmount(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) { + if (timestamp < start + cliff) { + return 0; + } else if (timestamp >= start + duration) { + return totalAllocation; + } else { + return totalAllocation * (timestamp - start) / duration; + } + } } diff --git a/packages/evm/contracts/incentives/AAllowListIncentive.sol b/packages/evm/contracts/incentives/AAllowListIncentive.sol index 50a2bc5df..9f37afeca 100644 --- a/packages/evm/contracts/incentives/AAllowListIncentive.sol +++ b/packages/evm/contracts/incentives/AAllowListIncentive.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.24; -import {BoostError} from "contracts/shared/BoostError.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; import {SimpleAllowList} from "contracts/allowlists/SimpleAllowList.sol"; @@ -20,47 +19,6 @@ abstract contract AAllowListIncentive is AIncentive { /// @notice The maximum number of claims that can be made (one per address) uint256 public limit; - /// @inheritdoc AIncentive - /// @notice Claim a slot on the {SimpleAllowList} - /// @param claimTarget the entity receiving the payout - function claim(address claimTarget, bytes calldata) external virtual override onlyOwner returns (bool) { - if (claims++ >= limit || claimed[claimTarget]) revert NotClaimable(); - claimed[claimTarget] = true; - - (address[] memory users, bool[] memory allowed) = _makeAllowListPayload(claimTarget); - - allowList.setAllowed(users, allowed); - return true; - } - - /// @inheritdoc AIncentive - /// @dev Not a valid operation for this type of incentive - function clawback(bytes calldata) external pure override returns (bool) { - revert BoostError.NotImplemented(); - } - - /// @inheritdoc AIncentive - function isClaimable(address claimTarget, bytes calldata) external view virtual override returns (bool) { - return claims < limit && !claimed[claimTarget] && !allowList.isAllowed(claimTarget, ""); - } - - /// @inheritdoc AIncentive - /// @dev No preflight approval is required for this incentive (no tokens are handled) - function preflight(bytes calldata) external pure override returns (bytes memory) { - return new bytes(0); - } - - /// @notice Create the payload for the SimpleAllowList - /// @param target_ The target address to add to the allow list - /// @return A tuple of users and allowed statuses - function _makeAllowListPayload(address target_) internal pure returns (address[] memory, bool[] memory) { - address[] memory users = new address[](1); - bool[] memory allowed = new bool[](1); - users[0] = target_; - allowed[0] = true; - return (users, allowed); - } - /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { return type(AAllowListIncentive).interfaceId; diff --git a/packages/evm/contracts/incentives/ACGDAIncentive.sol b/packages/evm/contracts/incentives/ACGDAIncentive.sol index e73b62d4d..ec1b6936b 100644 --- a/packages/evm/contracts/incentives/ACGDAIncentive.sol +++ b/packages/evm/contracts/incentives/ACGDAIncentive.sol @@ -13,11 +13,6 @@ import {AIncentive} from "./AIncentive.sol"; /// @title Continuous Gradual Dutch Auction AIncentive /// @notice An ERC20 incentive implementation with reward amounts adjusting dynamically based on claim volume. abstract contract ACGDAIncentive is AIncentive { - using SafeTransferLib for address; - - /// @notice The ERC20-like token used for the incentive - address public asset; - /// @notice The configuration parameters for the CGDAIncentive /// @param rewardDecay The amount to subtract from the current reward after each claim /// @param rewardBoost The amount by which the reward increases for each hour without a claim (continuous linear increase) @@ -30,64 +25,12 @@ abstract contract ACGDAIncentive is AIncentive { uint256 currentReward; } + /// @notice The ERC20-like token used for the incentive + address public asset; + CGDAParameters public cgdaParams; uint256 public totalBudget; - /// @inheritdoc AIncentive - /// @notice Claim the incentive - function claim(address claimTarget, bytes calldata) external virtual override onlyOwner returns (bool) { - if (!_isClaimable(claimTarget)) revert NotClaimable(); - claims++; - - // Calculate the current reward and update the state - uint256 reward = currentReward(); - cgdaParams.lastClaimTime = block.timestamp; - cgdaParams.currentReward = - reward > cgdaParams.rewardDecay ? reward - cgdaParams.rewardDecay : cgdaParams.rewardDecay; - - // Transfer the reward to the recipient - asset.safeTransfer(claimTarget, reward); - - emit Claimed(claimTarget, abi.encodePacked(asset, claimTarget, reward)); - return true; - } - - /// @inheritdoc AIncentive - function clawback(bytes calldata data_) external virtual override onlyOwner returns (bool) { - ClawbackPayload memory claim_ = abi.decode(data_, (ClawbackPayload)); - (uint256 amount) = abi.decode(claim_.data, (uint256)); - - // Transfer the tokens back to the intended recipient - asset.safeTransfer(claim_.target, amount); - emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, amount)); - - return true; - } - - /// @inheritdoc AIncentive - function isClaimable(address claimTarget, bytes calldata) external view virtual override returns (bool) { - return _isClaimable(claimTarget); - } - - /// @notice Calculates the current reward based on the time since the last claim. - /// @return The current reward - /// @dev The reward is calculated based on the time since the last claim, the available budget, and the reward parameters. It increases linearly over time in the absence of claims, with each hour adding `rewardBoost` to the current reward, up to the available budget. - /// @dev For example, if there is one claim in the first hour, then no claims for three hours, the claimable reward would be `initialReward - rewardDecay + (rewardBoost * 3)` - function currentReward() public view override returns (uint256) { - uint256 timeSinceLastClaim = block.timestamp - cgdaParams.lastClaimTime; - uint256 available = asset.balanceOf(address(this)); - - // Calculate the current reward based on the time elapsed since the last claim - // on a linear scale, with `1 * rewardBoost` added for each hour without a claim - uint256 projectedReward = cgdaParams.currentReward + (timeSinceLastClaim * cgdaParams.rewardBoost) / 3600; - return projectedReward > available ? available : projectedReward; - } - - function _isClaimable(address recipient_) internal view returns (bool) { - uint256 reward = currentReward(); - return reward > 0 && asset.balanceOf(address(this)) >= reward && !claimed[recipient_]; - } - /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { return type(ACGDAIncentive).interfaceId; diff --git a/packages/evm/contracts/incentives/AERC1155Incentive.sol b/packages/evm/contracts/incentives/AERC1155Incentive.sol index 3f43a2a23..2433515d6 100644 --- a/packages/evm/contracts/incentives/AERC1155Incentive.sol +++ b/packages/evm/contracts/incentives/AERC1155Incentive.sol @@ -6,9 +6,7 @@ import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; -import {BoostError} from "contracts/shared/BoostError.sol"; -import {ABudget} from "contracts/budgets/ABudget.sol"; import {AIncentive} from "contracts/incentives/AIncentive.sol"; /// @title ERC1155Incentive @@ -38,83 +36,6 @@ abstract contract AERC1155Incentive is AIncentive, IERC1155Receiver { /// @notice Extra data to be passed to the ERC1155 contract bytes public extraData; - /// @notice Claim the incentive - /// @param claimTarget the recipient of the payout - /// @param data_ The data payload for the incentive claim `(address recipient, bytes data)` - /// @return True if the incentive was successfully claimed - function claim(address claimTarget, bytes calldata data_) external override onlyOwner returns (bool) { - // Disburse the incentive based on the strategy (POOL only for now) - if (strategy == Strategy.POOL) { - if (!_isClaimable(claimTarget)) revert NotClaimable(); - - claims++; - claimed[claimTarget] = true; - - // wake-disable-next-line reentrancy (not a risk here) - asset.safeTransferFrom(address(this), claimTarget, tokenId, 1, data_); - emit Claimed(claimTarget, abi.encodePacked(asset, claimTarget, tokenId, uint256(1), data_)); - - return true; - } - - return false; - } - - /// @inheritdoc AIncentive - function clawback(bytes calldata data_) external override onlyOwner returns (bool) { - ClawbackPayload memory claim_ = abi.decode(data_, (ClawbackPayload)); - (uint256 amount) = abi.decode(claim_.data, (uint256)); - - // Ensure the amount is valid and reduce the max claims accordingly - if (amount > limit) revert BoostError.ClaimFailed(msg.sender, abi.encode(claim_)); - limit -= amount; - - // Reclaim the incentive to the intended recipient - // wake-disable-next-line reentrancy (not a risk here) - asset.safeTransferFrom(address(this), claim_.target, tokenId, amount, claim_.data); - emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, tokenId, amount, claim_.data)); - - return true; - } - - /// @notice Check if an incentive is claimable - /// @param claimTarget the potential recipient of the payout - /// @return True if the incentive is claimable based on the data payload - /// @dev For the POOL strategy, the `bytes data` portion of the payload ignored - /// @dev The recipient must not have already claimed the incentive - function isClaimable(address claimTarget, bytes calldata) public view override returns (bool) { - return _isClaimable(claimTarget); - } - - /// @notice Check if an incentive is claimable for a specific recipient - /// @param recipient_ The address of the recipient - /// @return True if the incentive is claimable for the recipient - function _isClaimable(address recipient_) internal view returns (bool) { - return !claimed[recipient_] && claims < limit; - } - - /// @inheritdoc IERC1155Receiver - /// @dev This contract does not check the token ID and will accept all tokens - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - return this.onERC1155Received.selector; - } - - /// @inheritdoc IERC1155Receiver - /// @dev This contract does not check the token ID and will accept all batches - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return this.onERC1155BatchReceived.selector; - } - /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { return type(AERC1155Incentive).interfaceId; diff --git a/packages/evm/contracts/incentives/AERC20Incentive.sol b/packages/evm/contracts/incentives/AERC20Incentive.sol index ff12a382c..0c563059a 100644 --- a/packages/evm/contracts/incentives/AERC20Incentive.sol +++ b/packages/evm/contracts/incentives/AERC20Incentive.sol @@ -39,81 +39,10 @@ abstract contract AERC20Incentive is AIncentive { /// @notice The set of addresses that have claimed a slot in the incentive raffle address[] public entries; - - /// @notice Claim the incentive - /// @param claimTarget the address receiving the claim - /// @return True if the incentive was successfully claimed - function claim(address claimTarget, bytes calldata) external override onlyOwner returns (bool) { - if (!_isClaimable(claimTarget)) revert NotClaimable(); - - if (strategy == Strategy.POOL) { - claims++; - claimed[claimTarget] = true; - - asset.safeTransfer(claimTarget, reward); - - emit Claimed(claimTarget, abi.encodePacked(asset, claimTarget, reward)); - return true; - } else { - claims++; - claimed[claimTarget] = true; - entries.push(claimTarget); - - emit Entry(claimTarget); - return true; - } - } - - /// @inheritdoc AIncentive - function clawback(bytes calldata data_) external override onlyOwner returns (bool) { - ClawbackPayload memory claim_ = abi.decode(data_, (ClawbackPayload)); - (uint256 amount) = abi.decode(claim_.data, (uint256)); - - if (strategy == Strategy.RAFFLE) { - // Ensure the amount is the full reward and there are no raffle entries, then reset the limit - if (amount != reward || claims > 0) revert BoostError.ClaimFailed(msg.sender, abi.encode(claim_)); - limit = 0; - } else { - // Ensure the amount is a multiple of the reward and reduce the max claims accordingly - if (amount % reward != 0) revert BoostError.ClaimFailed(msg.sender, abi.encode(claim_)); - limit -= amount / reward; - } - - // Transfer the tokens back to the intended recipient - asset.safeTransfer(claim_.target, amount); - emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, amount)); - - return true; - } - - /// @notice Check if an incentive is claimable - /// @param claimTarget the address that could receive the claim - /// @return True if the incentive is claimable based on the data payload - /// @dev For the POOL strategy, the `bytes data` portion of the payload ignored - /// @dev The recipient must not have already claimed the incentive - function isClaimable(address claimTarget, bytes calldata) public view override returns (bool) { - return _isClaimable(claimTarget); - } - - /// @notice Check if an incentive is claimable for a specific recipient - /// @param recipient_ The address of the recipient - /// @return True if the incentive is claimable for the recipient - function _isClaimable(address recipient_) internal view returns (bool) { - return !claimed[recipient_] && claims < limit; - } - /// @notice Draw a winner from the raffle /// @dev Only valid when the strategy is set to `Strategy.RAFFLE` - function drawRaffle() external onlyOwner { - if (strategy != Strategy.RAFFLE) revert BoostError.Unauthorized(); - - LibPRNG.PRNG memory _prng = LibPRNG.PRNG({state: block.prevrandao + block.timestamp}); - address winnerAddress = entries[_prng.next() % entries.length]; - - asset.safeTransfer(winnerAddress, reward); - emit Claimed(winnerAddress, abi.encodePacked(asset, winnerAddress, reward)); - } + function drawRaffle() external virtual; /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { diff --git a/packages/evm/contracts/incentives/APointsIncentive.sol b/packages/evm/contracts/incentives/APointsIncentive.sol index 38071f280..54c36e019 100644 --- a/packages/evm/contracts/incentives/APointsIncentive.sol +++ b/packages/evm/contracts/incentives/APointsIncentive.sol @@ -5,8 +5,7 @@ import {BoostError} from "contracts/shared/BoostError.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; import {ABudget} from "contracts/budgets/ABudget.sol"; -import {AIncentive} from "./AIncentive.sol"; -import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; +import {AIncentive} from "contracts/incentives/AIncentive.sol"; /// @title Points AIncentive /// @notice A simple on-chain points incentive implementation that allows claiming of soulbound tokens @@ -24,56 +23,6 @@ abstract contract APointsIncentive is AIncentive { /// @notice The selector for the issuance function on the points contract bytes4 public selector; - /// @notice Claim the incentive - /// @param claimTarget the address receiving the claim funds - /// @return True if the incentive was successfully claimed - function claim(address claimTarget, bytes calldata) external override onlyOwner returns (bool) { - // check ownership - OwnableRoles points = OwnableRoles(venue); - if (points.owner() != address(this) && points.hasAnyRole(address(this), 1 << 1) != true) { - revert BoostError.Unauthorized(); - } - - if (!_isClaimable(claimTarget)) revert NotClaimable(); - - claims++; - claimed[claimTarget] = true; - - (bool success,) = venue.call(abi.encodeWithSelector(selector, claimTarget, reward)); - if (!success) revert ClaimFailed(); - - emit Claimed(claimTarget, abi.encodePacked(venue, claimTarget, reward)); - return true; - } - - /// @inheritdoc AIncentive - /// @dev Not a valid operation for this type of incentive - function clawback(bytes calldata) external pure override returns (bool) { - revert BoostError.NotImplemented(); - } - - /// @inheritdoc AIncentive - /// @notice No token approvals are required for this incentive - function preflight(bytes calldata) external pure override returns (bytes memory budgetData) { - return new bytes(0); - } - - /// @notice Check if an incentive is claimable - /// @param claimTarget The address receiving the claim - /// @return True if the incentive is claimable based on the data payload - /// @dev For the POOL strategy, the `bytes data` portion of the payload ignored - /// @dev The recipient must not have already claimed the incentive - function isClaimable(address claimTarget, bytes calldata) public view override returns (bool) { - return _isClaimable(claimTarget); - } - - /// @notice Check if an incentive is claimable for a specific recipient - /// @param recipient_ The address of the recipient - /// @return True if the incentive is claimable for the recipient - function _isClaimable(address recipient_) internal view returns (bool) { - return !claimed[recipient_] && claims < limit; - } - /// @inheritdoc ACloneable function getComponentInterface() public pure virtual override(ACloneable) returns (bytes4) { return type(APointsIncentive).interfaceId; diff --git a/packages/evm/contracts/incentives/AllowListIncentive.sol b/packages/evm/contracts/incentives/AllowListIncentive.sol index 8b3a751b2..e98991164 100644 --- a/packages/evm/contracts/incentives/AllowListIncentive.sol +++ b/packages/evm/contracts/incentives/AllowListIncentive.sol @@ -35,4 +35,45 @@ contract AllowListIncentive is AAllowListIncentive { allowList = init_.allowList; limit = init_.limit; } + + /// @inheritdoc AIncentive + /// @notice Claim a slot on the {SimpleAllowList} + /// @param claimTarget the entity receiving the payout + function claim(address claimTarget, bytes calldata) external virtual override onlyOwner returns (bool) { + if (claims++ >= limit || claimed[claimTarget]) revert NotClaimable(); + claimed[claimTarget] = true; + + (address[] memory users, bool[] memory allowed) = _makeAllowListPayload(claimTarget); + + allowList.setAllowed(users, allowed); + return true; + } + + /// @inheritdoc AIncentive + /// @dev Not a valid operation for this type of incentive + function clawback(bytes calldata) external pure override returns (bool) { + revert BoostError.NotImplemented(); + } + + /// @inheritdoc AIncentive + function isClaimable(address claimTarget, bytes calldata) external view virtual override returns (bool) { + return claims < limit && !claimed[claimTarget] && !allowList.isAllowed(claimTarget, ""); + } + + /// @inheritdoc AIncentive + /// @dev No preflight approval is required for this incentive (no tokens are handled) + function preflight(bytes calldata) external pure override returns (bytes memory) { + return new bytes(0); + } + + /// @notice Create the payload for the SimpleAllowList + /// @param target_ The target address to add to the allow list + /// @return A tuple of users and allowed statuses + function _makeAllowListPayload(address target_) internal pure returns (address[] memory, bool[] memory) { + address[] memory users = new address[](1); + bool[] memory allowed = new bool[](1); + users[0] = target_; + allowed[0] = true; + return (users, allowed); + } } diff --git a/packages/evm/contracts/incentives/CGDAIncentive.sol b/packages/evm/contracts/incentives/CGDAIncentive.sol index 6eb2a4325..476836f23 100644 --- a/packages/evm/contracts/incentives/CGDAIncentive.sol +++ b/packages/evm/contracts/incentives/CGDAIncentive.sol @@ -78,4 +78,59 @@ contract CGDAIncentive is ACGDAIncentive { }) ); } + + /// @inheritdoc AIncentive + /// @notice Claim the incentive + function claim(address claimTarget, bytes calldata) external virtual override onlyOwner returns (bool) { + if (!_isClaimable(claimTarget)) revert NotClaimable(); + claims++; + + // Calculate the current reward and update the state + uint256 reward = currentReward(); + cgdaParams.lastClaimTime = block.timestamp; + cgdaParams.currentReward = + reward > cgdaParams.rewardDecay ? reward - cgdaParams.rewardDecay : cgdaParams.rewardDecay; + + // Transfer the reward to the recipient + asset.safeTransfer(claimTarget, reward); + + emit Claimed(claimTarget, abi.encodePacked(asset, claimTarget, reward)); + return true; + } + + /// @inheritdoc AIncentive + function clawback(bytes calldata data_) external virtual override onlyOwner returns (bool) { + ClawbackPayload memory claim_ = abi.decode(data_, (ClawbackPayload)); + (uint256 amount) = abi.decode(claim_.data, (uint256)); + + // Transfer the tokens back to the intended recipient + asset.safeTransfer(claim_.target, amount); + emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, amount)); + + return true; + } + + /// @inheritdoc AIncentive + function isClaimable(address claimTarget, bytes calldata) external view virtual override returns (bool) { + return _isClaimable(claimTarget); + } + + /// @notice Calculates the current reward based on the time since the last claim. + /// @return The current reward + /// @dev The reward is calculated based on the time since the last claim, the available budget, and the reward parameters. It increases linearly over time in the absence of claims, with each hour adding `rewardBoost` to the current reward, up to the available budget. + /// @dev For example, if there is one claim in the first hour, then no claims for three hours, the claimable reward would be `initialReward - rewardDecay + (rewardBoost * 3)` + function currentReward() public view override returns (uint256) { + uint256 timeSinceLastClaim = block.timestamp - cgdaParams.lastClaimTime; + uint256 available = asset.balanceOf(address(this)); + + // Calculate the current reward based on the time elapsed since the last claim + // on a linear scale, with `1 * rewardBoost` added for each hour without a claim + uint256 projectedReward = cgdaParams.currentReward + (timeSinceLastClaim * cgdaParams.rewardBoost) / 3600; + return projectedReward > available ? available : projectedReward; + } + + function _isClaimable(address recipient_) internal view returns (bool) { + uint256 reward = currentReward(); + return reward > 0 && asset.balanceOf(address(this)) >= reward && !claimed[recipient_]; + } } diff --git a/packages/evm/contracts/incentives/ERC1155Incentive.sol b/packages/evm/contracts/incentives/ERC1155Incentive.sol index 00091a0e9..d1ec312c4 100644 --- a/packages/evm/contracts/incentives/ERC1155Incentive.sol +++ b/packages/evm/contracts/incentives/ERC1155Incentive.sol @@ -9,6 +9,12 @@ import {ABudget} from "contracts/budgets/ABudget.sol"; import {AERC1155Incentive} from "contracts/incentives/AERC1155Incentive.sol"; import {AIncentive} from "contracts/incentives/AIncentive.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {ACloneable} from "contracts/shared/ACloneable.sol"; + /// @title ERC1155Incentive /// @notice A simple ERC1155 incentive implementation that allows claiming of tokens contract ERC1155Incentive is AERC1155Incentive { @@ -69,4 +75,81 @@ contract ERC1155Incentive is AERC1155Incentive { }) ); } + + /// @notice Claim the incentive + /// @param claimTarget the recipient of the payout + /// @param data_ The data payload for the incentive claim `(address recipient, bytes data)` + /// @return True if the incentive was successfully claimed + function claim(address claimTarget, bytes calldata data_) external override onlyOwner returns (bool) { + // Disburse the incentive based on the strategy (POOL only for now) + if (strategy == Strategy.POOL) { + if (!_isClaimable(claimTarget)) revert NotClaimable(); + + claims++; + claimed[claimTarget] = true; + + // wake-disable-next-line reentrancy (not a risk here) + asset.safeTransferFrom(address(this), claimTarget, tokenId, 1, data_); + emit Claimed(claimTarget, abi.encodePacked(asset, claimTarget, tokenId, uint256(1), data_)); + + return true; + } + + return false; + } + + /// @inheritdoc AIncentive + function clawback(bytes calldata data_) external override onlyOwner returns (bool) { + ClawbackPayload memory claim_ = abi.decode(data_, (ClawbackPayload)); + (uint256 amount) = abi.decode(claim_.data, (uint256)); + + // Ensure the amount is valid and reduce the max claims accordingly + if (amount > limit) revert BoostError.ClaimFailed(msg.sender, abi.encode(claim_)); + limit -= amount; + + // Reclaim the incentive to the intended recipient + // wake-disable-next-line reentrancy (not a risk here) + asset.safeTransferFrom(address(this), claim_.target, tokenId, amount, claim_.data); + emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, tokenId, amount, claim_.data)); + + return true; + } + + /// @notice Check if an incentive is claimable + /// @param claimTarget the potential recipient of the payout + /// @return True if the incentive is claimable based on the data payload + /// @dev For the POOL strategy, the `bytes data` portion of the payload ignored + /// @dev The recipient must not have already claimed the incentive + function isClaimable(address claimTarget, bytes calldata) public view override returns (bool) { + return _isClaimable(claimTarget); + } + + /// @notice Check if an incentive is claimable for a specific recipient + /// @param recipient_ The address of the recipient + /// @return True if the incentive is claimable for the recipient + function _isClaimable(address recipient_) internal view returns (bool) { + return !claimed[recipient_] && claims < limit; + } + + /// @inheritdoc IERC1155Receiver + /// @dev This contract does not check the token ID and will accept all tokens + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + /// @inheritdoc IERC1155Receiver + /// @dev This contract does not check the token ID and will accept all batches + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + return this.onERC1155BatchReceived.selector; + } } diff --git a/packages/evm/contracts/incentives/ERC20Incentive.sol b/packages/evm/contracts/incentives/ERC20Incentive.sol index a4fb3d770..cdb3d9aa5 100644 --- a/packages/evm/contracts/incentives/ERC20Incentive.sol +++ b/packages/evm/contracts/incentives/ERC20Incentive.sol @@ -68,4 +68,79 @@ contract ERC20Incentive is AERC20Incentive { }) ); } + + /// @notice Claim the incentive + /// @param claimTarget the address receiving the claim + /// @return True if the incentive was successfully claimed + function claim(address claimTarget, bytes calldata) external override onlyOwner returns (bool) { + if (!_isClaimable(claimTarget)) revert NotClaimable(); + + if (strategy == Strategy.POOL) { + claims++; + claimed[claimTarget] = true; + + asset.safeTransfer(claimTarget, reward); + + emit Claimed(claimTarget, abi.encodePacked(asset, claimTarget, reward)); + return true; + } else { + claims++; + claimed[claimTarget] = true; + entries.push(claimTarget); + + emit Entry(claimTarget); + return true; + } + } + + /// @inheritdoc AIncentive + function clawback(bytes calldata data_) external override onlyOwner returns (bool) { + ClawbackPayload memory claim_ = abi.decode(data_, (ClawbackPayload)); + (uint256 amount) = abi.decode(claim_.data, (uint256)); + + if (strategy == Strategy.RAFFLE) { + // Ensure the amount is the full reward and there are no raffle entries, then reset the limit + if (amount != reward || claims > 0) revert BoostError.ClaimFailed(msg.sender, abi.encode(claim_)); + limit = 0; + } else { + // Ensure the amount is a multiple of the reward and reduce the max claims accordingly + if (amount % reward != 0) revert BoostError.ClaimFailed(msg.sender, abi.encode(claim_)); + limit -= amount / reward; + } + + // Transfer the tokens back to the intended recipient + asset.safeTransfer(claim_.target, amount); + emit Claimed(claim_.target, abi.encodePacked(asset, claim_.target, amount)); + + return true; + } + + /// @notice Check if an incentive is claimable + /// @param claimTarget the address that could receive the claim + /// @return True if the incentive is claimable based on the data payload + /// @dev For the POOL strategy, the `bytes data` portion of the payload ignored + /// @dev The recipient must not have already claimed the incentive + function isClaimable(address claimTarget, bytes calldata) public view override returns (bool) { + return _isClaimable(claimTarget); + } + + /// @notice Check if an incentive is claimable for a specific recipient + /// @param recipient_ The address of the recipient + /// @return True if the incentive is claimable for the recipient + function _isClaimable(address recipient_) internal view returns (bool) { + return !claimed[recipient_] && claims < limit; + } + + /// @notice Draw a winner from the raffle + /// @dev Only valid when the strategy is set to `Strategy.RAFFLE` + function drawRaffle() external override onlyOwner { + if (strategy != Strategy.RAFFLE) revert BoostError.Unauthorized(); + + LibPRNG.PRNG memory _prng = LibPRNG.PRNG({state: block.prevrandao + block.timestamp}); + + address winnerAddress = entries[_prng.next() % entries.length]; + + asset.safeTransfer(winnerAddress, reward); + emit Claimed(winnerAddress, abi.encodePacked(asset, winnerAddress, reward)); + } } diff --git a/packages/evm/contracts/incentives/PointsIncentive.sol b/packages/evm/contracts/incentives/PointsIncentive.sol index 8ba0fcfb7..8ceb7e4db 100644 --- a/packages/evm/contracts/incentives/PointsIncentive.sol +++ b/packages/evm/contracts/incentives/PointsIncentive.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.24; import {BoostError} from "contracts/shared/BoostError.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; +import {AIncentive} from "contracts/incentives/AIncentive.sol"; import {APointsIncentive} from "contracts/incentives/APointsIncentive.sol"; +import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; /// @title Points AIncentive /// @notice A simple on-chain points incentive implementation that allows claiming of soulbound tokens @@ -39,4 +41,54 @@ contract PointsIncentive is APointsIncentive { limit = init_.limit; _initializeOwner(msg.sender); } + + /// @notice Claim the incentive + /// @param claimTarget the address receiving the claim funds + /// @return True if the incentive was successfully claimed + function claim(address claimTarget, bytes calldata) external override onlyOwner returns (bool) { + // check ownership + OwnableRoles points = OwnableRoles(venue); + if (points.owner() != address(this) && points.hasAnyRole(address(this), 1 << 1) != true) { + revert BoostError.Unauthorized(); + } + + if (!_isClaimable(claimTarget)) revert NotClaimable(); + + claims++; + claimed[claimTarget] = true; + + (bool success,) = venue.call(abi.encodeWithSelector(selector, claimTarget, reward)); + if (!success) revert ClaimFailed(); + + emit Claimed(claimTarget, abi.encodePacked(venue, claimTarget, reward)); + return true; + } + + /// @inheritdoc AIncentive + /// @dev Not a valid operation for this type of incentive + function clawback(bytes calldata) external pure override returns (bool) { + revert BoostError.NotImplemented(); + } + + /// @inheritdoc AIncentive + /// @notice No token approvals are required for this incentive + function preflight(bytes calldata) external pure override returns (bytes memory budgetData) { + return new bytes(0); + } + + /// @notice Check if an incentive is claimable + /// @param claimTarget The address receiving the claim + /// @return True if the incentive is claimable based on the data payload + /// @dev For the POOL strategy, the `bytes data` portion of the payload ignored + /// @dev The recipient must not have already claimed the incentive + function isClaimable(address claimTarget, bytes calldata) public view override returns (bool) { + return _isClaimable(claimTarget); + } + + /// @notice Check if an incentive is claimable for a specific recipient + /// @param recipient_ The address of the recipient + /// @return True if the incentive is claimable for the recipient + function _isClaimable(address recipient_) internal view returns (bool) { + return !claimed[recipient_] && claims < limit; + } } diff --git a/packages/evm/test/BoostCore.t.sol b/packages/evm/test/BoostCore.t.sol index 7d170094d..96b20beb4 100644 --- a/packages/evm/test/BoostCore.t.sol +++ b/packages/evm/test/BoostCore.t.sol @@ -9,7 +9,7 @@ import {LibZip} from "@solady/utils/LibZip.sol"; // Actions import {AAction} from "contracts/actions/AAction.sol"; -import {ContractAction} from "contracts/actions/ContractAction.sol"; +import {AContractAction, ContractAction} from "contracts/actions/ContractAction.sol"; import {ERC721MintAction} from "contracts/actions/ERC721MintAction.sol"; // Allowlists @@ -291,7 +291,7 @@ contract BoostCoreTest is Test { isBase: true, instance: address(new ERC721MintAction()), parameters: abi.encode( - ContractAction.InitPayload({chainId: block.chainid, target: target, selector: selector, value: value}) + AContractAction.InitPayload({chainId: block.chainid, target: target, selector: selector, value: value}) ) }); } diff --git a/packages/evm/test/actions/ContractAction.t.sol b/packages/evm/test/actions/ContractAction.t.sol index c0f197d55..b89712751 100644 --- a/packages/evm/test/actions/ContractAction.t.sol +++ b/packages/evm/test/actions/ContractAction.t.sol @@ -20,7 +20,7 @@ contract ContractActionTest is Test { function setUp() public { action = ContractAction(LibClone.clone(address(baseAction))); - ContractAction.InitPayload memory payload = ContractAction.InitPayload({ + ContractAction.InitPayload memory payload = AContractAction.InitPayload({ chainId: block.chainid, target: address(target), selector: target.mintPayable.selector, @@ -29,7 +29,7 @@ contract ContractActionTest is Test { action.initialize(abi.encode(payload)); nonPayableTargetAction = ContractAction(LibClone.clone(address(baseAction))); - ContractAction.InitPayload memory nonPayablePayload = ContractAction.InitPayload({ + ContractAction.InitPayload memory nonPayablePayload = AContractAction.InitPayload({ chainId: block.chainid, target: address(target), selector: target.mint.selector, @@ -38,7 +38,7 @@ contract ContractActionTest is Test { nonPayableTargetAction.initialize(abi.encode(nonPayablePayload)); otherChainAction = ContractAction(LibClone.clone(address(baseAction))); - ContractAction.InitPayload memory otherChainPayload = ContractAction.InitPayload({ + ContractAction.InitPayload memory otherChainPayload = AContractAction.InitPayload({ chainId: block.chainid + 1, target: address(target), selector: target.mintPayable.selector, @@ -61,7 +61,7 @@ contract ContractActionTest is Test { vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); baseAction.initialize( abi.encode( - ContractAction.InitPayload({ + AContractAction.InitPayload({ chainId: block.chainid, target: address(target), selector: target.mintPayable.selector, diff --git a/packages/evm/test/budgets/ManagedBudget.t.sol b/packages/evm/test/budgets/ManagedBudget.t.sol index fafbf233f..73d12d150 100644 --- a/packages/evm/test/budgets/ManagedBudget.t.sol +++ b/packages/evm/test/budgets/ManagedBudget.t.sol @@ -1081,7 +1081,7 @@ contract ManagedBudgetTest is Test, IERC1155Receiver { // Allocate 100 tokens to the budget bytes memory data = abi.encodeWithSelector( - AManagedBudget.allocate.selector, + ManagedBudget.allocate.selector, _makeFungibleTransfer(ABudget.AssetType.ERC20, address(mockERC20), address(this), 100 ether) ); diff --git a/packages/evm/test/budgets/SimpleBudget.t.sol b/packages/evm/test/budgets/SimpleBudget.t.sol index 1770652ea..a3df20e81 100644 --- a/packages/evm/test/budgets/SimpleBudget.t.sol +++ b/packages/evm/test/budgets/SimpleBudget.t.sol @@ -14,7 +14,6 @@ import {BoostError} from "contracts/shared/BoostError.sol"; import {ABudget} from "contracts/budgets/ABudget.sol"; import {ACloneable} from "contracts/shared/ACloneable.sol"; import {SimpleBudget} from "contracts/budgets/SimpleBudget.sol"; -import {ASimpleBudget} from "contracts/budgets/ASimpleBudget.sol"; contract SimpleBudgetTest is Test, IERC1155Receiver { MockERC20 mockERC20; @@ -42,42 +41,42 @@ contract SimpleBudgetTest is Test, IERC1155Receiver { // SimpleBudget initial state // //////////////////////////////// - function test_InitialOwner() public { + function test_InitialOwner() public view { // Ensure the budget has the correct owner assertEq(simpleBudget.owner(), address(this)); } - function test_InitialDistributed() public { + function test_InitialDistributed() public view { // Ensure the budget has 0 tokens distributed assertEq(simpleBudget.total(address(mockERC20)), 0); } - function test_InitialDistributed1155() public { + function test_InitialDistributed1155() public view { // Ensure the budget has 0 of our 1155 tokens distributed assertEq(simpleBudget.total(address(mockERC1155), 42), 0); } - function test_InitialTotal() public { + function test_InitialTotal() public view { // Ensure the budget has 0 tokens allocated assertEq(simpleBudget.total(address(mockERC20)), 0); } - function test_InitialTotal1155() public { + function test_InitialTotal1155() public view { // Ensure the budget has 0 of our 1155 tokens allocated assertEq(simpleBudget.total(address(mockERC1155), 42), 0); } - function test_InitialAvailable() public { + function test_InitialAvailable() public view { // Ensure the budget has 0 tokens available assertEq(simpleBudget.available(address(mockERC20)), 0); } - function test_InitialAvailable1155() public { + function test_InitialAvailable1155() public view { // Ensure the budget has 0 of our 1155 tokens available assertEq(simpleBudget.available(address(mockERC1155), 42), 0); } - function test_InitializerDisabled() public { + function test_InitializerDisabled() public view { // Because the slot is private, we use `vm.load` to access it then parse out the bits: // - [0] is the `initializing` flag (which should be 0 == false) // - [1..64] hold the `initializedVersion` (which should be 1) @@ -656,7 +655,7 @@ contract SimpleBudgetTest is Test, IERC1155Receiver { assertEq(simpleBudget.available(address(0)), 100 ether); } - function testAvailable_NeverAllocated() public { + function testAvailable_NeverAllocated() public view { // Ensure the budget has 0 tokens available assertEq(simpleBudget.available(address(otherMockERC20)), 0); } @@ -752,7 +751,7 @@ contract SimpleBudgetTest is Test, IERC1155Receiver { assertFalse(simpleBudget.isAuthorized(address(0xdeadbeef))); } - function testIsAuthorized_Owner() public { + function testIsAuthorized_Owner() public view { assertTrue(simpleBudget.isAuthorized(address(this))); } @@ -760,7 +759,7 @@ contract SimpleBudgetTest is Test, IERC1155Receiver { // SimpleBudget.getComponentInterface // //////////////////////////////////// - function testGetComponentInterface() public { + function testGetComponentInterface() public view { // Ensure the contract supports the ABudget interface console.logBytes4(simpleBudget.getComponentInterface()); } @@ -769,22 +768,22 @@ contract SimpleBudgetTest is Test, IERC1155Receiver { // SimpleBudget.supportsInterface // //////////////////////////////////// - function testSupportsBudgetInterface() public { + function testSupportsBudgetInterface() public view { // Ensure the contract supports the ABudget interface assertTrue(simpleBudget.supportsInterface(type(ABudget).interfaceId)); } - function testSupportsERC1155Receiver() public { + function testSupportsERC1155Receiver() public view { // Ensure the contract supports the ABudget interface assertTrue(simpleBudget.supportsInterface(type(IERC1155Receiver).interfaceId)); } - function testSupportsERC165() public { + function testSupportsERC165() public view { // Ensure the contract supports the ABudget interface assertTrue(simpleBudget.supportsInterface(type(IERC165).interfaceId)); } - function testSupportsInterface_NotSupported() public { + function testSupportsInterface_NotSupported() public view { // Ensure the contract does not support an unsupported interface assertFalse(simpleBudget.supportsInterface(type(Test).interfaceId)); } @@ -805,7 +804,7 @@ contract SimpleBudgetTest is Test, IERC1155Receiver { // Allocate 100 tokens to the budget bytes memory data = abi.encodeWithSelector( - ASimpleBudget.allocate.selector, + SimpleBudget.allocate.selector, _makeFungibleTransfer(ABudget.AssetType.ERC20, address(mockERC20), address(this), 100 ether) ); diff --git a/packages/evm/test/e2e/EndToEndBasic.t.sol b/packages/evm/test/e2e/EndToEndBasic.t.sol index 7bc526a0d..f96dfb188 100644 --- a/packages/evm/test/e2e/EndToEndBasic.t.sol +++ b/packages/evm/test/e2e/EndToEndBasic.t.sol @@ -22,7 +22,7 @@ import {ABudget} from "contracts/budgets/ABudget.sol"; import {SimpleBudget} from "contracts/budgets/SimpleBudget.sol"; import {AAction} from "contracts/actions/AAction.sol"; -import {ContractAction} from "contracts/actions/ContractAction.sol"; +import {AContractAction, ContractAction} from "contracts/actions/ContractAction.sol"; import {ERC721MintAction} from "contracts/actions/ERC721MintAction.sol"; import {AIncentive} from "contracts/incentives/AIncentive.sol"; @@ -288,7 +288,7 @@ contract EndToEndBasic is Test { ) ), parameters: abi.encode( - ContractAction.InitPayload({ + AContractAction.InitPayload({ chainId: block.chainid, target: address(erc721), selector: MockERC721.mint.selector, diff --git a/packages/evm/test/e2e/EndToEndSignerValidator.t.sol b/packages/evm/test/e2e/EndToEndSignerValidator.t.sol index 8a86a0931..d6c5441a1 100644 --- a/packages/evm/test/e2e/EndToEndSignerValidator.t.sol +++ b/packages/evm/test/e2e/EndToEndSignerValidator.t.sol @@ -25,7 +25,7 @@ import {ABudget} from "contracts/budgets/ABudget.sol"; import {ManagedBudget} from "contracts/budgets/ManagedBudget.sol"; import {AAction} from "contracts/actions/AAction.sol"; -import {ContractAction} from "contracts/actions/ContractAction.sol"; +import {AContractAction} from "contracts/actions/AContractAction.sol"; import {ERC721MintAction} from "contracts/actions/ERC721MintAction.sol"; import {AIncentive} from "contracts/incentives/AIncentive.sol"; @@ -285,7 +285,7 @@ contract EndToEndSigner is Test, OwnableRoles { ) ), parameters: abi.encode( - ContractAction.InitPayload({ + AContractAction.InitPayload({ chainId: block.chainid, target: address(erc721), selector: MockERC721.mint.selector,