Files
smom-dbis-138/contracts/cw-settlement/CWRedemptionQueue.sol

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