171 lines
5.1 KiB
Solidity
171 lines
5.1 KiB
Solidity
|
|
// 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();
|
||
|
|
}
|
||
|
|
}
|