// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../interfaces/IBurnController.sol"; import "../interfaces/IISO4217WToken.sol"; /** * @title BurnController * @notice Controls burning of ISO-4217 W tokens on redemption * @dev Burn-before-release sequence for on-demand redemption at par */ contract BurnController is IBurnController, AccessControl, ReentrancyGuard { bytes32 public constant REDEEMER_ROLE = keccak256("REDEEMER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); uint256 private _redemptionCounter; mapping(address => bool) public isApprovedToken; mapping(bytes32 => Redemption) public redemptions; struct Redemption { address token; address redeemer; uint256 amount; uint256 timestamp; bool processed; } constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(REDEEMER_ROLE, admin); _grantRole(BURNER_ROLE, admin); } /** * @notice Redeem tokens (burn and release fiat) * @param token Token address * @param from Redeemer address * @param amount Amount to redeem (in token decimals) * @return redemptionId Redemption ID for tracking */ function redeem( address token, address from, uint256 amount ) external override nonReentrant onlyRole(REDEEMER_ROLE) returns (bytes32 redemptionId) { require(isApprovedToken[token], "BurnController: token not approved"); require(amount > 0, "BurnController: zero amount"); require(from != address(0), "BurnController: zero address"); // Check if redemption is allowed require(this.canRedeem(token, amount), "BurnController: redemption not allowed"); // Burn tokens (atomic burn-before-release sequence) IISO4217WToken tokenContract = IISO4217WToken(token); tokenContract.burn(from, amount); // Generate redemption ID _redemptionCounter++; redemptionId = keccak256(abi.encodePacked(token, from, amount, _redemptionCounter, block.timestamp)); // Record redemption redemptions[redemptionId] = Redemption({ token: token, redeemer: from, amount: amount, timestamp: block.timestamp, processed: true }); // Note: Fiat release happens off-chain or via separate payment system // This contract handles the token burn portion emit Redeemed(token, from, amount, redemptionId); } /** * @notice Burn tokens without redemption (emergency/transfer) * @param token Token address * @param from Source address * @param amount Amount to burn */ function burn(address token, address from, uint256 amount) external override nonReentrant onlyRole(BURNER_ROLE) { require(isApprovedToken[token], "BurnController: token not approved"); require(amount > 0, "BurnController: zero amount"); IISO4217WToken tokenContract = IISO4217WToken(token); tokenContract.burn(from, amount); emit Burned(token, from, amount); } /** * @notice Check if redemption is allowed * @param token Token address * @param amount Amount to redeem * @return redeemable True if redemption is allowed */ function canRedeem(address token, uint256 amount) external view override returns (bool redeemable) { if (!isApprovedToken[token]) { return false; } IISO4217WToken tokenContract = IISO4217WToken(token); uint256 totalSupply = tokenContract.totalSupply(); // Redemption is allowed if supply >= amount // Additional checks (e.g., reserve sufficiency) would be added here return totalSupply >= amount; } /** * @notice Approve a token for burning/redemption * @param token Token address */ function approveToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { isApprovedToken[token] = true; } /** * @notice Revoke token approval * @param token Token address */ function revokeToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { isApprovedToken[token] = false; } /** * @notice Get redemption information * @param redemptionId Redemption ID * @return redemption Redemption struct */ function getRedemption(bytes32 redemptionId) external view returns (Redemption memory redemption) { return redemptions[redemptionId]; } }