// 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; } }