-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Payment): add budget and quota controlling contract for mission …
…rewards payments (#63)
- Loading branch information
Showing
6 changed files
with
544 additions
and
122 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// SPDX-License-Identifier: BSD-3-Clause-Clear | ||
pragma solidity 0.8.23; | ||
|
||
import {QuotaControl} from "./QuotaControl.sol"; | ||
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; | ||
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
/** | ||
* @title Payment Contract | ||
* @dev A contract that enables secure payments and withdrawals using ERC20 tokens. | ||
* The contract is controlled by an Oracle role and incorporates quota control | ||
* to manage payments within a specified limit. | ||
* | ||
* Inherits from `QuotaControl` to manage payment quotas. | ||
*/ | ||
contract Payment is QuotaControl { | ||
using SafeERC20 for IERC20; | ||
|
||
/// @dev Role identifier for the Oracle. Accounts with this role can trigger payments. | ||
bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); | ||
|
||
/// @dev ERC20 token used for payments in this contract. | ||
IERC20 public immutable token; | ||
|
||
/** | ||
* @dev Error emitted when an attempt to pay fails due to insufficient contract balance. | ||
* @param balance The current token balance of the contract. | ||
* @param needed The minimum token amount required to execute the payment. | ||
*/ | ||
error InsufficientBalance(uint256 balance, uint256 needed); | ||
|
||
/** | ||
* @dev Constructor that initializes the contract with the provided parameters. | ||
* @param oracle The address of the account assigned the `ORACLE_ROLE` to manage payments. | ||
* @param _token The address of the ERC20 token contract used for payments. | ||
* @param initialQuota The initial quota limit for payments. | ||
* @param initialPeriod The initial time period for which the quota is valid. | ||
* @param admin The address assigned the `DEFAULT_ADMIN_ROLE` for administrative privileges. | ||
*/ | ||
constructor(address oracle, address _token, uint256 initialQuota, uint256 initialPeriod, address admin) | ||
QuotaControl(initialQuota, initialPeriod, admin) | ||
{ | ||
_grantRole(ORACLE_ROLE, oracle); // Grant ORACLE_ROLE to the specified oracle address. | ||
token = IERC20(_token); // Set the ERC20 token used for payments. | ||
} | ||
|
||
/** | ||
* @notice Pays a specified amount to a list of recipients. | ||
* @dev Can only be called by accounts with the `ORACLE_ROLE`. | ||
* The total required amount is calculated by multiplying the number of recipients by the specified amount. | ||
* If the contract's token balance is insufficient, the transaction reverts with `InsufficientBalance`. | ||
* | ||
* @param recipients An array of addresses to receive the payments. | ||
* @param amount The amount of tokens to be paid to each recipient. | ||
* | ||
* Emits a `Transfer` event from the token for each recipient. | ||
*/ | ||
function pay(address[] calldata recipients, uint256 amount) external onlyRole(ORACLE_ROLE) { | ||
uint256 needed = recipients.length * amount; // Calculate the total tokens required. | ||
uint256 balance = token.balanceOf(address(this)); // Get the current balance of the contract. | ||
|
||
if (balance < needed) { | ||
revert InsufficientBalance(balance, needed); | ||
} | ||
|
||
_checkedResetClaimed(); | ||
_checkedUpdateClaimed(needed); | ||
|
||
for (uint256 i = 0; i < recipients.length; i++) { | ||
token.safeTransfer(recipients[i], amount); | ||
} | ||
} | ||
|
||
/** | ||
* @notice Withdraws a specified amount of tokens to the provided recipient address. | ||
* @dev Can only be called by accounts with the `DEFAULT_ADMIN_ROLE`. | ||
* | ||
* @param recipient The address to receive the withdrawn tokens. | ||
* @param amount The amount of tokens to be transferred to the recipient. | ||
* | ||
* Emits a `Transfer` event from the token to the recipient. | ||
*/ | ||
function withdraw(address recipient, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
token.safeTransfer(recipient, 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,164 @@ | ||
// SPDX-License-Identifier: BSD-3-Clause-Clear | ||
pragma solidity 0.8.23; | ||
|
||
import {AccessControl} from "openzeppelin-contracts/contracts/access/AccessControl.sol"; | ||
import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; | ||
|
||
/** | ||
* @title QuotaControl | ||
* @dev A contract designed to manage and control periodic quotas, such as token distributions or other `uint256`-based allowances. | ||
* It ensures that the total claims or usage within a given period do not exceed the predefined quota, thus regulating the flow of resources. | ||
* | ||
* The contract employs a time-based system where the quota is automatically reset at the start of each period. | ||
* An administrator, holding the `DEFAULT_ADMIN_ROLE`, has the authority to set and modify both the quota and the renewal period. | ||
* | ||
* Key Features: | ||
* - Prevents claims that exceed the current quota. | ||
* - Allows the admin to update the quota and renewal period. | ||
* - Automatically resets the claimed amount when a new period begins. | ||
*/ | ||
contract QuotaControl is AccessControl { | ||
using Math for uint256; | ||
|
||
/** | ||
* @dev The maximum allowable period for reward quota renewal. This limit prevents potential overflows and reduces the need for safe math checks. | ||
*/ | ||
uint256 public constant MAX_PERIOD = 30 days; | ||
|
||
/** | ||
* @dev The current period for reward quota renewal, specified in seconds. | ||
*/ | ||
uint256 public period; | ||
|
||
/** | ||
* @dev The maximum amount of rewards that can be distributed in the current period. | ||
*/ | ||
uint256 public quota; | ||
|
||
/** | ||
* @dev The timestamp when the reward quota will be renewed next. | ||
*/ | ||
uint256 public quotaRenewalTimestamp; | ||
|
||
/** | ||
* @dev The total amount of rewards claimed within the current period. | ||
*/ | ||
uint256 public claimed; | ||
|
||
/** | ||
* @dev Error triggered when the claim amount exceeds the current reward quota. | ||
*/ | ||
error QuotaExceeded(); | ||
|
||
/** | ||
* @dev Error triggered when the period for reward renewal is set to zero. | ||
*/ | ||
error ZeroPeriod(); | ||
|
||
/** | ||
* @dev Error triggered when the set period exceeds the maximum allowable period. | ||
*/ | ||
error TooLongPeriod(); | ||
|
||
/** | ||
* @dev Emitted when the reward quota is updated. | ||
* @param quota The new reward quota. | ||
*/ | ||
event QuotaSet(uint256 quota); | ||
|
||
/** | ||
* @dev Emitted when the reward period is updated. | ||
* @param period The new reward period in seconds. | ||
*/ | ||
event PeriodSet(uint256 period); | ||
|
||
/** | ||
* @dev Initializes the contract with an initial reward quota, reward period, and admin. | ||
* @param initialQuota The initial maximum amount of rewards distributable in each period. | ||
* @param initialPeriod The initial duration of the reward period in seconds. | ||
* @param admin The address granted the `DEFAULT_ADMIN_ROLE`, responsible for updating contract settings. | ||
* | ||
* Requirements: | ||
* - `initialPeriod` must be within the acceptable range (greater than 0 and less than or equal to `MAX_PERIOD`). | ||
*/ | ||
constructor(uint256 initialQuota, uint256 initialPeriod, address admin) { | ||
_mustBeWithinPeriodRange(initialPeriod); | ||
_grantRole(DEFAULT_ADMIN_ROLE, admin); | ||
|
||
quota = initialQuota; | ||
period = initialPeriod; | ||
quotaRenewalTimestamp = block.timestamp + period; | ||
} | ||
|
||
/** | ||
* @dev Sets a new reward quota. Can only be called by an account with the `DEFAULT_ADMIN_ROLE`. | ||
* @param newQuota The new maximum amount of rewards distributable in each period. | ||
*/ | ||
function setQuota(uint256 newQuota) external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
quota = newQuota; | ||
emit QuotaSet(newQuota); | ||
} | ||
|
||
/** | ||
* @dev Sets a new reward period. Can only be called by an account with the `DEFAULT_ADMIN_ROLE`. | ||
* @param newPeriod The new duration of the reward period in seconds. | ||
* | ||
* Requirements: | ||
* - `newPeriod` must be greater than 0 and less than or equal to `MAX_PERIOD`. | ||
*/ | ||
function setPeriod(uint256 newPeriod) external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
_mustBeWithinPeriodRange(newPeriod); | ||
period = newPeriod; | ||
emit PeriodSet(newPeriod); | ||
} | ||
|
||
/** | ||
* @dev Internal function that resets the claimed rewards to 0 and updates the next quota renewal timestamp. | ||
* If the current timestamp is beyond the quota renewal timestamp, a new period begins. | ||
* | ||
* The reset calculation ensures that the renewal timestamp will always be aligned with the period's duration, even if time has passed beyond the expected renewal time. | ||
*/ | ||
function _checkedResetClaimed() internal { | ||
if (block.timestamp >= quotaRenewalTimestamp) { | ||
claimed = 0; | ||
|
||
// Align the quota renewal timestamp to the next period start | ||
uint256 timeAhead = block.timestamp - quotaRenewalTimestamp; | ||
quotaRenewalTimestamp = block.timestamp + period - (timeAhead % period); | ||
} | ||
} | ||
|
||
/** | ||
* @dev Internal function to update the claimed rewards by a specified amount. | ||
* | ||
* If the total claimed amount exceeds the quota, the transaction is reverted. | ||
* @param amount The amount of rewards being claimed. | ||
* | ||
* Requirements: | ||
* - The updated `claimed` amount must not exceed the current reward quota. | ||
*/ | ||
function _checkedUpdateClaimed(uint256 amount) internal { | ||
(bool success, uint256 newClaimed) = claimed.tryAdd(amount); | ||
if (!success || newClaimed > quota) { | ||
revert QuotaExceeded(); | ||
} | ||
claimed = newClaimed; | ||
} | ||
|
||
/** | ||
* @dev Internal function to validate that the provided reward period is within the allowed range. | ||
* @param newPeriod The period to validate. | ||
* | ||
* Requirements: | ||
* - The `newPeriod` must be greater than 0. | ||
* - The `newPeriod` must not exceed `MAX_PERIOD`. | ||
*/ | ||
function _mustBeWithinPeriodRange(uint256 newPeriod) internal { | ||
if (newPeriod == 0) { | ||
revert ZeroPeriod(); | ||
} | ||
if (newPeriod > MAX_PERIOD) { | ||
revert TooLongPeriod(); | ||
} | ||
} | ||
} |
Oops, something went wrong.