Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(budget): add quest budget contract and support #281

Merged
merged 13 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading