-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BOOST-4438] ManagedBudget Contract (#32)
- Loading branch information
Showing
4 changed files
with
1,573 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,355 @@ | ||
// 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 {ReentrancyGuard} from "@solady/utils/ReentrancyGuard.sol"; | ||
import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; | ||
|
||
import {BoostError} from "contracts/shared/BoostError.sol"; | ||
import {Budget} from "contracts/budgets/Budget.sol"; | ||
import {Cloneable} from "contracts/shared/Cloneable.sol"; | ||
|
||
/// @title Abstract Managed Budget | ||
/// @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 AManagedBudget is Budget, OwnableRoles, IERC1155Receiver, ReentrancyGuard { | ||
using SafeTransferLib for address; | ||
|
||
/// @notice The role for depositing funds. | ||
uint256 public constant MANAGER_ROLE = _ROLE_0; | ||
/// @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 Cloneable | ||
/// @param data_ The packed init data for the budget `(address owner, address[] authorized)` | ||
function initialize(bytes calldata data_) public virtual override { | ||
revert NotInitializing(); | ||
} | ||
|
||
/// @inheritdoc Budget | ||
/// @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 Budget | ||
/// @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 reclaim(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 Budget | ||
/// @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 Budget | ||
/// @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 Budget | ||
/// @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 Budget | ||
/// @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]); | ||
} | ||
} | ||
|
||
/// @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 Budget | ||
/// @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 Budget | ||
/// @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 Budget | ||
/// @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 Budget | ||
/// @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 Cloneable | ||
function supportsInterface(bytes4 interfaceId) public view virtual override(Budget, IERC165) returns (bool) { | ||
return interfaceId == type(AManagedBudget).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId | ||
|| interfaceId == type(IERC165).interfaceId || Budget.supportsInterface(interfaceId); | ||
} | ||
|
||
/// @inheritdoc Cloneable | ||
function getComponentInterface() public pure virtual override returns (bytes4) { | ||
return type(AManagedBudget).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,32 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.24; | ||
|
||
import {AManagedBudget} from "contracts/budgets/AManagedBudget.sol"; | ||
|
||
/// @title Managed Budget | ||
/// @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 { | ||
/// @notice The payload for initializing a ManagedBudget | ||
struct InitPayload { | ||
address owner; | ||
address[] authorized; | ||
uint256[] roles; | ||
} | ||
|
||
/// @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() { | ||
_disableInitializers(); | ||
} | ||
|
||
/// @inheritdoc AManagedBudget | ||
/// @param data_ The packed init data for the budget `(address owner, address[] authorized, uint256[] roles)` | ||
function initialize(bytes calldata data_) public virtual override initializer { | ||
InitPayload memory init_ = abi.decode(data_, (InitPayload)); | ||
_initializeOwner(init_.owner); | ||
for (uint256 i = 0; i < init_.authorized.length; i++) { | ||
_setRoles(init_.authorized[i], init_.roles[i]); | ||
} | ||
} | ||
} |
Oops, something went wrong.