Skip to content

Commit

Permalink
feat(budget): add quest budget contract and support (#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
Quazia authored Jun 13, 2024
1 parent 7d7f9c6 commit a30f202
Show file tree
Hide file tree
Showing 16 changed files with 2,714 additions and 2 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ sed -i '' "s/TEST_CLAIM_SIGNER_PRIVATE_KEY=/TEST_CLAIM_SIGNER_PRIVATE_KEY=0xac09
forge test
```

### Run a specific test Contract:

Test QuestBudgetTest contracts:
```bash
forge test --match-contract QuestBudgetTest
```

### Run test coverage report:

```bash
Expand All @@ -167,6 +174,8 @@ If you see something like this `expected error: 0xdd8133e6 != 0xce3f0005` in For
1. Deploy the ProtocolRewards
`forge script script/ProtocolRewards.s.sol:ProtocolRewardsDeploy --rpc-url sepolia --broadcast --verify -vvvv`
1. Set any storage variables manually if needed
1. Deploy the QuestBudget
`forge script script/QuestBudget.s.sol:QuestBudgetDeploy --rpc-url sepolia --broadcast --verify -vvvv`


### with mantel, add:
Expand Down
178 changes: 178 additions & 0 deletions broadcast/QuestBudget.s.sol/11155111/run-1717432982.json

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions broadcast/QuestBudget.s.sol/11155111/run-1717437866.json

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions broadcast/QuestBudget.s.sol/11155111/run-1717450347.json

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions broadcast/QuestBudget.s.sol/11155111/run-1717450764.json

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions broadcast/QuestBudget.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

396 changes: 396 additions & 0 deletions contracts/QuestBudget.sol

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion contracts/interfaces/IQuestFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ interface IQuestFactory {
string memory actionType,
string memory questName
) external pure returns (string memory);

function questFee() external view returns (uint16);

// Create
function create1155QuestAndQueue(
address rewardTokenAddress_,
Expand All @@ -206,8 +207,25 @@ interface IQuestFactory {
string memory
) external payable returns (address);

function createERC20Quest(
uint32 txHashChainId_,
address rewardTokenAddress_,
uint256 endTime_,
uint256 startTime_,
uint256 totalParticipants_,
uint256 rewardAmount_,
string calldata questId_,
string calldata actionType_,
string calldata questName_,
string calldata projectName_,
uint256 referralRewardFee_
) external returns (address);


function claimOptimized(bytes calldata signature_, bytes calldata data_) external payable;

function cancelQuest(string calldata questId_) external;

// Set
function setClaimSignerAddress(address claimSignerAddress_) external;
function setErc1155QuestAddress(address erc1155QuestAddress_) external;
Expand Down
34 changes: 34 additions & 0 deletions contracts/references/BoostError.sol
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();
}
142 changes: 142 additions & 0 deletions contracts/references/Budget.sol
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;
}
}
42 changes: 42 additions & 0 deletions contracts/references/Cloneable.sol
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);
}
}
74 changes: 74 additions & 0 deletions contracts/references/Mocks.sol
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);
}
}
2 changes: 1 addition & 1 deletion lib/solady
Submodule solady updated 119 files
26 changes: 26 additions & 0 deletions script/QuestBudget.s.sol
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();
}
}
Loading

0 comments on commit a30f202

Please sign in to comment.