// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "../bridge/interop/BridgeEscrowVault.sol"; /** * @title TokenizedEUR * @notice ERC-20 tokenized EUR backed 1:1 by reserves on Fabric * @dev Mintable/burnable by Fabric attestation via authorized minter */ contract TokenizedEUR is ERC20, ERC20Burnable, AccessControl, Pausable { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); uint8 private constant DECIMALS = 18; struct FabricAttestation { bytes32 fabricTxHash; string tokenId; uint256 amount; address minter; uint256 timestamp; bytes signature; } mapping(bytes32 => bool) public processedFabricTxs; mapping(string => uint256) public fabricTokenBalances; // Fabric tokenId -> Besu balance event TokenizedEURMinted( address indexed to, uint256 amount, string indexed fabricTokenId, bytes32 fabricTxHash ); event TokenizedEURBurned( address indexed from, uint256 amount, string indexed fabricTokenId, bytes32 fabricTxHash ); event FabricAttestationReceived( bytes32 indexed fabricTxHash, string tokenId, uint256 amount ); error ZeroAmount(); error ZeroAddress(); error InvalidFabricAttestation(); error FabricTxAlreadyProcessed(); error InsufficientFabricBalance(); constructor(address admin) ERC20("Tokenized EUR", "EUR-T") { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(MINTER_ROLE, admin); _grantRole(BURNER_ROLE, admin); _grantRole(ATTESTOR_ROLE, admin); } /** * @notice Mint tokenized EUR based on Fabric attestation * @param to Recipient address * @param amount Amount to mint * @param fabricTokenId Fabric token ID * @param fabricTxHash Fabric transaction hash * @param attestation Attestation from Fabric */ function mintFromFabric( address to, uint256 amount, string memory fabricTokenId, bytes32 fabricTxHash, FabricAttestation calldata attestation ) external onlyRole(MINTER_ROLE) whenNotPaused { if (to == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); if (processedFabricTxs[fabricTxHash]) revert FabricTxAlreadyProcessed(); // Verify attestation (in production, verify signature) if (attestation.fabricTxHash != fabricTxHash) { revert InvalidFabricAttestation(); } if (attestation.amount != amount) { revert InvalidFabricAttestation(); } // Mark Fabric tx as processed processedFabricTxs[fabricTxHash] = true; // Update Fabric token balance mapping fabricTokenBalances[fabricTokenId] += amount; // Mint tokens _mint(to, amount); emit TokenizedEURMinted(to, amount, fabricTokenId, fabricTxHash); emit FabricAttestationReceived(fabricTxHash, fabricTokenId, amount); } /** * @notice Burn tokenized EUR to redeem on Fabric * @param from Address to burn from * @param amount Amount to burn * @param fabricTokenId Fabric token ID * @param fabricTxHash Fabric redemption transaction hash */ function burnForFabric( address from, uint256 amount, string memory fabricTokenId, bytes32 fabricTxHash ) external onlyRole(BURNER_ROLE) whenNotPaused { if (from == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); if (processedFabricTxs[fabricTxHash]) revert FabricTxAlreadyProcessed(); // Check Fabric token balance if (fabricTokenBalances[fabricTokenId] < amount) { revert InsufficientFabricBalance(); } // Mark Fabric tx as processed processedFabricTxs[fabricTxHash] = true; // Update Fabric token balance mapping fabricTokenBalances[fabricTokenId] -= amount; // Burn tokens _burn(from, amount); emit TokenizedEURBurned(from, amount, fabricTokenId, fabricTxHash); } /** * @notice Get Fabric token balance on Besu * @param fabricTokenId Fabric token ID * @return Balance on Besu */ function getFabricTokenBalance(string memory fabricTokenId) external view returns (uint256) { return fabricTokenBalances[fabricTokenId]; } /** * @notice Check if Fabric tx has been processed * @param fabricTxHash Fabric transaction hash * @return True if processed */ function isFabricTxProcessed(bytes32 fabricTxHash) external view returns (bool) { return processedFabricTxs[fabricTxHash]; } /** * @notice Override decimals to return 18 */ function decimals() public pure override returns (uint8) { return DECIMALS; } /** * @notice Pause token transfers */ function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } /** * @notice Unpause token transfers */ function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } }