190 lines
6.2 KiB
Solidity
190 lines
6.2 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @title CWRedemptionQueue
|
|
* @notice Tiered redemption for hub c* collateral released from cW burn flows.
|
|
* @dev Instant <= instantTierMax, standard <= standardTierMax (24h delay), above => extended queue.
|
|
*/
|
|
contract CWRedemptionQueue is AccessControl, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE_ROLE");
|
|
bytes32 public constant PROCESSOR_ROLE = keccak256("PROCESSOR_ROLE");
|
|
|
|
enum Tier {
|
|
Instant,
|
|
Standard,
|
|
Extended
|
|
}
|
|
|
|
enum Status {
|
|
Pending,
|
|
Ready,
|
|
Claimed,
|
|
Cancelled
|
|
}
|
|
|
|
struct Request {
|
|
address beneficiary;
|
|
address token;
|
|
uint256 amount;
|
|
Tier tier;
|
|
Status status;
|
|
uint64 readyAt;
|
|
}
|
|
|
|
uint256 public instantTierMax = 5_000 * 1e6;
|
|
uint256 public standardTierMax = 50_000 * 1e6;
|
|
uint256 public standardDelay = 24 hours;
|
|
uint256 public extendedDelay = 72 hours;
|
|
|
|
uint256 public nextRequestId = 1;
|
|
mapping(uint256 => Request) public requests;
|
|
|
|
event TierLimitsUpdated(uint256 instantMax, uint256 standardMax, uint256 standardDelaySec, uint256 extendedDelaySec);
|
|
event RedemptionEnqueued(uint256 indexed requestId, address indexed beneficiary, address indexed token, uint256 amount, Tier tier);
|
|
event RedemptionReady(uint256 indexed requestId);
|
|
event RedemptionClaimed(uint256 indexed requestId, address indexed beneficiary, uint256 amount);
|
|
|
|
error ZeroAddress();
|
|
error InvalidAmount();
|
|
error NotReady();
|
|
error InvalidStatus();
|
|
|
|
constructor(address admin) {
|
|
if (admin == address(0)) {
|
|
revert ZeroAddress();
|
|
}
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(PROCESSOR_ROLE, admin);
|
|
}
|
|
|
|
function setTierLimits(
|
|
uint256 instantMax,
|
|
uint256 standardMax,
|
|
uint256 standardDelaySec,
|
|
uint256 extendedDelaySec
|
|
) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
instantTierMax = instantMax;
|
|
standardTierMax = standardMax;
|
|
standardDelay = standardDelaySec;
|
|
extendedDelay = extendedDelaySec;
|
|
emit TierLimitsUpdated(instantMax, standardMax, standardDelaySec, extendedDelaySec);
|
|
}
|
|
|
|
function enqueueRedemption(
|
|
address beneficiary,
|
|
address token,
|
|
uint256 amount
|
|
) external onlyRole(BRIDGE_ROLE) nonReentrant returns (uint256 requestId) {
|
|
if (beneficiary == address(0) || token == address(0) || amount == 0) {
|
|
revert InvalidAmount();
|
|
}
|
|
|
|
Tier tier = _tierForAmount(amount);
|
|
uint64 readyAt = tier == Tier.Instant ? uint64(block.timestamp) : uint64(block.timestamp + _delayForTier(tier));
|
|
Status status = tier == Tier.Instant ? Status.Ready : Status.Pending;
|
|
|
|
requestId = nextRequestId++;
|
|
requests[requestId] = Request({
|
|
beneficiary: beneficiary,
|
|
token: token,
|
|
amount: amount,
|
|
tier: tier,
|
|
status: status,
|
|
readyAt: readyAt
|
|
});
|
|
|
|
emit RedemptionEnqueued(requestId, beneficiary, token, amount, tier);
|
|
}
|
|
|
|
function fundAndEnqueue(
|
|
address beneficiary,
|
|
address token,
|
|
uint256 amount
|
|
) external nonReentrant returns (uint256 requestId) {
|
|
if (beneficiary == address(0) || token == address(0) || amount == 0) {
|
|
revert InvalidAmount();
|
|
}
|
|
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
|
|
|
|
Tier tier = _tierForAmount(amount);
|
|
uint64 readyAt = tier == Tier.Instant ? uint64(block.timestamp) : uint64(block.timestamp + _delayForTier(tier));
|
|
Status status = tier == Tier.Instant ? Status.Ready : Status.Pending;
|
|
|
|
requestId = nextRequestId++;
|
|
requests[requestId] = Request({
|
|
beneficiary: beneficiary,
|
|
token: token,
|
|
amount: amount,
|
|
tier: tier,
|
|
status: status,
|
|
readyAt: readyAt
|
|
});
|
|
|
|
emit RedemptionEnqueued(requestId, beneficiary, token, amount, tier);
|
|
}
|
|
|
|
function markReady(uint256 requestId) external onlyRole(PROCESSOR_ROLE) {
|
|
Request storage req = requests[requestId];
|
|
if (req.status != Status.Pending) {
|
|
revert InvalidStatus();
|
|
}
|
|
req.status = Status.Ready;
|
|
emit RedemptionReady(requestId);
|
|
}
|
|
|
|
function claim(uint256 requestId) external nonReentrant {
|
|
Request storage req = requests[requestId];
|
|
if (req.status != Status.Ready) {
|
|
revert InvalidStatus();
|
|
}
|
|
if (block.timestamp < req.readyAt) {
|
|
revert NotReady();
|
|
}
|
|
if (msg.sender != req.beneficiary && !hasRole(PROCESSOR_ROLE, msg.sender)) {
|
|
revert("CWRedemptionQueue: not beneficiary");
|
|
}
|
|
|
|
req.status = Status.Claimed;
|
|
IERC20(req.token).safeTransfer(req.beneficiary, req.amount);
|
|
emit RedemptionClaimed(requestId, req.beneficiary, req.amount);
|
|
}
|
|
|
|
function processDueRequests(uint256[] calldata requestIds) external onlyRole(PROCESSOR_ROLE) {
|
|
for (uint256 i = 0; i < requestIds.length; i++) {
|
|
Request storage req = requests[requestIds[i]];
|
|
if (req.status == Status.Pending && block.timestamp >= req.readyAt) {
|
|
req.status = Status.Ready;
|
|
emit RedemptionReady(requestIds[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function _tierForAmount(uint256 amount) internal view returns (Tier) {
|
|
if (amount <= instantTierMax) {
|
|
return Tier.Instant;
|
|
}
|
|
if (amount <= standardTierMax) {
|
|
return Tier.Standard;
|
|
}
|
|
return Tier.Extended;
|
|
}
|
|
|
|
function _delayForTier(Tier tier) internal view returns (uint256) {
|
|
if (tier == Tier.Standard) {
|
|
return standardDelay;
|
|
}
|
|
if (tier == Tier.Extended) {
|
|
return extendedDelay;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|