// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "./wXRP.sol"; /** * @title MintBurnController * @notice HSM-backed controller for wXRP mint/burn operations * @dev Uses EIP-712 signatures for HSM authorization */ contract MintBurnController is AccessControl, Pausable, EIP712 { using ECDSA for bytes32; bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 private constant MINT_TYPEHASH = keccak256("MintRequest(address to,uint256 amount,bytes32 xrplTxHash,uint256 nonce,uint256 deadline)"); bytes32 private constant BURN_TYPEHASH = keccak256("BurnRequest(address from,uint256 amount,bytes32 xrplTxHash,uint256 nonce,uint256 deadline)"); wXRP public immutable wXRP_TOKEN; mapping(uint256 => bool) public usedNonces; address public hsmSigner; event MintExecuted( address indexed to, uint256 amount, bytes32 xrplTxHash, address executor ); event BurnExecuted( address indexed from, uint256 amount, bytes32 xrplTxHash, address executor ); event HSMSignerUpdated(address oldSigner, address newSigner); struct MintRequest { address to; uint256 amount; bytes32 xrplTxHash; uint256 nonce; uint256 deadline; bytes hsmSignature; } struct BurnRequest { address from; uint256 amount; bytes32 xrplTxHash; uint256 nonce; uint256 deadline; bytes hsmSignature; } error ZeroAmount(); error ZeroAddress(); error InvalidSignature(); error NonceAlreadyUsed(); error DeadlineExpired(); error InvalidNonce(); constructor(address admin, address _wXRP, address _hsmSigner) EIP712("MintBurnController", "1") { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(OPERATOR_ROLE, admin); wXRP_TOKEN = wXRP(_wXRP); hsmSigner = _hsmSigner; } /** * @notice Update HSM signer address * @param newSigner New HSM signer address */ function setHSMSigner(address newSigner) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newSigner == address(0)) revert ZeroAddress(); address oldSigner = hsmSigner; hsmSigner = newSigner; emit HSMSignerUpdated(oldSigner, newSigner); } /** * @notice Execute mint with HSM signature * @param request Mint request with HSM signature */ function executeMint(MintRequest calldata request) external onlyRole(OPERATOR_ROLE) whenNotPaused { if (request.to == address(0)) revert ZeroAddress(); if (request.amount == 0) revert ZeroAmount(); if (block.timestamp > request.deadline) revert DeadlineExpired(); if (usedNonces[request.nonce]) revert NonceAlreadyUsed(); // Verify HSM signature bytes32 structHash = keccak256( abi.encode( MINT_TYPEHASH, request.to, request.amount, request.xrplTxHash, request.nonce, request.deadline ) ); bytes32 hash = _hashTypedDataV4(structHash); if (hash.recover(request.hsmSignature) != hsmSigner) { revert InvalidSignature(); } usedNonces[request.nonce] = true; wXRP_TOKEN.mint(request.to, request.amount, request.xrplTxHash); emit MintExecuted(request.to, request.amount, request.xrplTxHash, msg.sender); } /** * @notice Execute burn with HSM signature * @param request Burn request with HSM signature */ function executeBurn(BurnRequest calldata request) external onlyRole(OPERATOR_ROLE) whenNotPaused { if (request.from == address(0)) revert ZeroAddress(); if (request.amount == 0) revert ZeroAmount(); if (block.timestamp > request.deadline) revert DeadlineExpired(); if (usedNonces[request.nonce]) revert NonceAlreadyUsed(); // Verify HSM signature bytes32 structHash = keccak256( abi.encode( BURN_TYPEHASH, request.from, request.amount, request.xrplTxHash, request.nonce, request.deadline ) ); bytes32 hash = _hashTypedDataV4(structHash); if (hash.recover(request.hsmSignature) != hsmSigner) { revert InvalidSignature(); } usedNonces[request.nonce] = true; wXRP_TOKEN.burnFrom(request.from, request.amount, request.xrplTxHash); emit BurnExecuted(request.from, request.amount, request.xrplTxHash, msg.sender); } /** * @notice Pause controller */ function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } /** * @notice Unpause controller */ function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } }