-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(budget): add quest budget contract and support (#281)
- Loading branch information
Showing
16 changed files
with
2,714 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
broadcast/QuestBudget.s.sol/11155111/run-1717432982.json
Large diffs are not rendered by default.
Oops, something went wrong.
178 changes: 178 additions & 0 deletions
178
broadcast/QuestBudget.s.sol/11155111/run-1717437866.json
Large diffs are not rendered by default.
Oops, something went wrong.
178 changes: 178 additions & 0 deletions
178
broadcast/QuestBudget.s.sol/11155111/run-1717450347.json
Large diffs are not rendered by default.
Oops, something went wrong.
178 changes: 178 additions & 0 deletions
178
broadcast/QuestBudget.s.sol/11155111/run-1717450764.json
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.19; | ||
|
||
/// @title BoostError | ||
/// @notice Standardized errors for the Boost protocol | ||
/// @dev Some of these errors are introduced by third-party libraries, rather than Boost contracts directly, and are copied here for clarity and ease of testing. | ||
library BoostError { | ||
/// @notice Thrown when a claim attempt fails | ||
error ClaimFailed(address caller, bytes data); | ||
|
||
/// @notice Thrown when there are insufficient funds for an operation | ||
error InsufficientFunds(address asset, uint256 available, uint256 required); | ||
|
||
/// @notice Thrown when a non-conforming instance for a given type is encountered | ||
error InvalidInstance(bytes4 expectedInterface, address instance); | ||
|
||
/// @notice Thrown when an invalid initialization is attempted | ||
error InvalidInitialization(); | ||
|
||
/// @notice Thrown when the length of two arrays are not equal | ||
error LengthMismatch(); | ||
|
||
/// @notice Thrown when a method is not implemented | ||
error NotImplemented(); | ||
|
||
/// @notice Thrown when a previously used signature is replayed | ||
error Replayed(address signer, bytes32 hash, bytes signature); | ||
|
||
/// @notice Thrown when a transfer fails for an unknown reason | ||
error TransferFailed(address asset, address to, uint256 amount); | ||
|
||
/// @notice Thrown when the requested action is unauthorized | ||
error Unauthorized(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.19; | ||
|
||
import {Ownable} from "solady/auth/Ownable.sol"; | ||
import {Receiver} from "solady/accounts/Receiver.sol"; | ||
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; | ||
|
||
import {BoostError} from "contracts/references/BoostError.sol"; | ||
import {Cloneable} from "contracts/references/Cloneable.sol"; | ||
|
||
/// @title Boost Budget | ||
/// @notice Abstract contract for a generic Budget within the Boost protocol | ||
/// @dev Budget classes are expected to implement the allocation, reclamation, and disbursement of assets. | ||
/// @dev WARNING: Budgets currently support only ETH, ERC20, and ERC1155 assets. Other asset types may be added in the future. | ||
abstract contract Budget is Ownable, Cloneable, Receiver { | ||
using SafeTransferLib for address; | ||
|
||
enum AssetType { | ||
ETH, | ||
ERC20, | ||
ERC1155 | ||
} | ||
|
||
/// @notice A struct representing the inputs for an allocation | ||
/// @param assetType The type of asset to allocate | ||
/// @param asset The address of the asset to allocate | ||
/// @param target The address of the payee or payer (from or to, depending on the operation) | ||
/// @param data The implementation-specific data for the allocation (amount, token ID, etc.) | ||
struct Transfer { | ||
AssetType assetType; | ||
address asset; | ||
address target; | ||
bytes data; | ||
} | ||
|
||
/// @notice The payload for an ETH or ERC20 transfer | ||
/// @param amount The amount of the asset to transfer | ||
struct FungiblePayload { | ||
uint256 amount; | ||
} | ||
|
||
/// @notice The payload for an ERC1155 transfer | ||
/// @param tokenId The ID of the token to transfer | ||
/// @param amount The amount of the token to transfer | ||
/// @param data Any additional data to forward to the ERC1155 contract | ||
struct ERC1155Payload { | ||
uint256 tokenId; | ||
uint256 amount; | ||
bytes data; | ||
} | ||
|
||
/// @notice Emitted when an address's authorization status changes | ||
event Authorized(address indexed account, bool isAuthorized); | ||
|
||
/// @notice Emitted when assets are distributed from the budget | ||
event Distributed(address indexed asset, address to, uint256 amount); | ||
|
||
/// @notice Thrown when the allocation is invalid | ||
error InvalidAllocation(address asset, uint256 amount); | ||
|
||
/// @notice Thrown when there are insufficient funds for an operation | ||
error InsufficientFunds(address asset, uint256 available, uint256 required); | ||
|
||
/// @notice Thrown when the length of two arrays are not equal | ||
error LengthMismatch(); | ||
|
||
/// @notice Thrown when a transfer fails for an unknown reason | ||
error TransferFailed(address asset, address to, uint256 amount); | ||
|
||
/// @notice Initialize the budget and set the owner | ||
/// @dev The owner is set to the contract deployer | ||
constructor() { | ||
_initializeOwner(msg.sender); | ||
} | ||
|
||
/// @notice Allocate assets to the budget | ||
/// @param data_ The compressed data for the allocation (amount, token address, token ID, etc.) | ||
/// @return True if the allocation was successful | ||
function allocate(bytes calldata data_) external payable virtual returns (bool); | ||
|
||
/// @notice Reclaim assets from the budget | ||
/// @param data_ The compressed data for the reclamation (amount, token address, token ID, etc.) | ||
/// @return True if the reclamation was successful | ||
function reclaim(bytes calldata data_) external virtual returns (bool); | ||
|
||
/// @notice Disburse assets from the budget to a single recipient | ||
/// @param data_ The compressed {Transfer} request | ||
/// @return True if the disbursement was successful | ||
function disburse(bytes calldata data_) external virtual returns (bool); | ||
|
||
/// @notice Disburse assets from the budget to multiple recipients | ||
/// @param data_ The array of compressed {Transfer} requests | ||
/// @return True if all disbursements were successful | ||
function disburseBatch(bytes[] calldata data_) external virtual returns (bool); | ||
|
||
/// @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 | ||
function total(address asset_) external view virtual returns (uint256); | ||
|
||
/// @notice Get the amount of assets available for distribution from the budget | ||
/// @param asset_ The address of the asset | ||
/// @return The amount of assets available | ||
function available(address asset_) external view virtual returns (uint256); | ||
|
||
/// @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 returns (uint256); | ||
|
||
/// @notice Reconcile the budget to ensure the known state matches the actual state | ||
/// @param data_ The compressed data for the reconciliation (amount, token address, token ID, etc.) | ||
/// @return The amount of assets reconciled | ||
function reconcile(bytes calldata data_) external virtual returns (uint256); | ||
|
||
/// @inheritdoc Cloneable | ||
function supportsInterface(bytes4 interfaceId) public view virtual override(Cloneable) returns (bool) { | ||
return interfaceId == type(Budget).interfaceId || super.supportsInterface(interfaceId); | ||
} | ||
|
||
/// @notice Set the authorized status of the given accounts | ||
/// @param accounts_ The accounts to authorize or deauthorize | ||
/// @param isAuthorized_ The authorization status for the given accounts | ||
/// @dev The mechanism for managing authorization is left to the implementing contract | ||
function setAuthorized(address[] calldata accounts_, bool[] calldata isAuthorized_) external virtual; | ||
|
||
/// @notice Check if the given account is authorized to use the budget | ||
/// @param account_ The account to check | ||
/// @return True if the account is authorized | ||
/// @dev The mechanism for checking authorization is left to the implementing contract | ||
function isAuthorized(address account_) external view virtual returns (bool); | ||
|
||
/// @inheritdoc Receiver | ||
receive() external payable virtual override { | ||
return; | ||
} | ||
|
||
/// @inheritdoc Receiver | ||
fallback() external payable virtual override { | ||
return; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.19; | ||
|
||
import {Initializable} from "solady/utils/Initializable.sol"; | ||
import {ERC165} from "openzeppelin-contracts/utils/introspection/ERC165.sol"; | ||
|
||
/// @title Cloneable | ||
/// @notice A contract that can be cloned and initialized only once | ||
abstract contract Cloneable is Initializable, ERC165 { | ||
/// @notice Thrown when an inheriting contract does not implement the initializer function | ||
error InitializerNotImplemented(); | ||
|
||
/// @notice Thrown when the provided initialization data is invalid | ||
/// @dev This error indicates that the given data is not valid for the implementation (i.e. does not decode to the expected types) | ||
error InvalidInitializationData(); | ||
|
||
/// @notice Thrown when the contract has already been initialized | ||
error CloneAlreadyInitialized(); | ||
|
||
/// @notice A modifier to restrict a function to only be called before initialization | ||
/// @dev This is intended to enforce that a function can only be called before the contract has been initialized | ||
modifier onlyBeforeInitialization() { | ||
if (_getInitializedVersion() != 0) revert CloneAlreadyInitialized(); | ||
_; | ||
} | ||
|
||
/// @notice Initialize the clone with the given arbitrary data | ||
/// @param - The compressed initialization data (if required) | ||
/// @dev The data is expected to be ABI encoded bytes compressed using {LibZip-cdCompress} | ||
/// @dev All implementations must override this function to initialize the contract | ||
function initialize(bytes calldata) public virtual initializer { | ||
revert InitializerNotImplemented(); | ||
} | ||
|
||
/// @inheritdoc ERC165 | ||
/// @notice Check if the contract supports the given interface | ||
/// @param interfaceId The interface identifier | ||
/// @return True if the contract supports the interface | ||
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { | ||
return interfaceId == type(Cloneable).interfaceId || super.supportsInterface(interfaceId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.19; | ||
|
||
import {LibString} from "solady/utils/LibString.sol"; | ||
import {ERC20} from "solady/tokens/ERC20.sol"; | ||
import {ERC721} from "solady/tokens/ERC721.sol"; | ||
import {ERC1155} from "openzeppelin-contracts/token/ERC1155/ERC1155.sol"; | ||
|
||
/** | ||
* 🚨 WARNING: The mocks in this file are for testing purposes only. DO NOT use | ||
* ANY of this code in production, ever, or you will lose all of your money, | ||
* friends, and credibility. Also, your cat might run away for fear of being | ||
* associated with someone who makes such poor life choices. | ||
*/ | ||
|
||
/// @title MockERC721 | ||
/// @notice A mock ERC721 token (FOR TESTING PURPOSES ONLY) | ||
contract MockERC721 is ERC721 { | ||
uint256 public totalSupply; | ||
uint256 public mintPrice = 0.1 ether; | ||
|
||
function name() public pure override returns (string memory) { | ||
return "Mock ERC721"; | ||
} | ||
|
||
function symbol() public pure override returns (string memory) { | ||
return "MOCK"; | ||
} | ||
|
||
function mint(address to) public payable { | ||
require(msg.value >= mintPrice, "MockERC721: gimme more money!"); | ||
// pre-increment so IDs start at 1 | ||
_mint(to, ++totalSupply); | ||
} | ||
|
||
function tokenURI(uint256 id) public view virtual override returns (string memory) { | ||
return string(abi.encodePacked("https://example.com/token/", LibString.toString(id))); | ||
} | ||
} | ||
|
||
/// @title MockERC20 | ||
/// @notice A mock ERC20 token (FOR TESTING PURPOSES ONLY) | ||
contract MockERC20 is ERC20 { | ||
function name() public pure override returns (string memory) { | ||
return "Mock ERC20"; | ||
} | ||
|
||
function symbol() public pure override returns (string memory) { | ||
return "MOCK"; | ||
} | ||
|
||
function mint(address to, uint256 amount) public { | ||
_mint(to, amount); | ||
} | ||
|
||
function mintPayable(address to, uint256 amount) public payable { | ||
require(msg.value >= amount / 100, "MockERC20: gimme more money!"); | ||
_mint(to, amount); | ||
} | ||
} | ||
|
||
/// @title MockERC1155 | ||
/// @notice A mock ERC1155 token (FOR TESTING PURPOSES ONLY) | ||
contract MockERC1155 is ERC1155 { | ||
constructor() ERC1155("https://example.com/token/{id}") {} | ||
|
||
function mint(address to, uint256 id, uint256 amount) public { | ||
_mint(to, id, amount, ""); | ||
} | ||
|
||
function burn(address from, uint256 id, uint256 amount) public { | ||
_burn(from, id, amount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.19; | ||
|
||
import {Script} from "forge-std/Script.sol"; | ||
import {QuestBudget} from "../contracts/QuestBudget.sol"; | ||
import {QuestContractConstants as C} from "../contracts/libraries/QuestContractConstants.sol"; | ||
import {LibClone} from "solady/utils/LibClone.sol"; | ||
|
||
contract QuestBudgetDeploy is Script { | ||
function run() external { | ||
uint256 deployerPrivateKey = vm.envUint("TEST_CLAIM_SIGNER_PRIVATE_KEY"); | ||
address operator = vm.envAddress("MAINNET_QUEST_BUDGET_OPERATOR"); | ||
address owner = vm.envAddress("MAINNET_QUEST_BUDGET_OWNER"); | ||
address[] memory authorized = new address[](1); | ||
authorized[0] = operator; // Add more authorized addresses if needed | ||
|
||
vm.startBroadcast(deployerPrivateKey); | ||
|
||
// Deploy QuestBudget | ||
QuestBudget questBudget = QuestBudget(payable(LibClone.clone(address(new QuestBudget())))); | ||
questBudget.initialize( | ||
abi.encode(QuestBudget.InitPayload({owner: owner, questFactory: C.QUEST_FACTORY_ADDRESS, authorized: authorized})) | ||
); | ||
vm.stopBroadcast(); | ||
} | ||
} |
Oops, something went wrong.