// SPDX-License-Identifier: MIT // OpenZeppelin Confidential Contracts (last updated v0.5.0) (token/ERC7984/extensions/ERC7984Rwa.sol) pragma solidity ^0.8.27; import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {IERC7984Rwa} from "./../../../interfaces/IERC7984Rwa.sol"; import {FHESafeMath} from "./../../../utils/FHESafeMath.sol"; import {ERC7984} from "./../ERC7984.sol"; import {ERC7984Freezable} from "./ERC7984Freezable.sol"; import {ERC7984Restricted} from "./ERC7984Restricted.sol"; /** * @dev Extension of {ERC7984} that supports confidential Real World Assets (RWAs). * This interface provides compliance checks, transfer controls and enforcement actions. */ abstract contract ERC7984Rwa is IERC7984Rwa, ERC7984Freezable, ERC7984Restricted, Pausable, Multicall, AccessControl { /// @dev The operation failed because the lost account is the same as the new account. error ERC7984RwaSelfRecoveryNotAllowed(); /** * @dev Accounts granted the agent role have the following permissioned abilities: * * * Mint/Burn to/from a given address (does not require permission) * * Force transfer from a given address (does not require permission) * ** Bypasses pause and restriction checks (not frozen) * * Pause/Unpause the contract * * Block/Unblock a given account * * Set frozen amount of tokens for a given account. */ bytes32 public constant AGENT_ROLE = keccak256("AGENT_ROLE"); /// @dev Checks if the sender is an admin. modifier onlyAdmin() { _checkRole(DEFAULT_ADMIN_ROLE); _; } /// @dev Checks if the sender is an agent. modifier onlyAgent() { _checkRole(AGENT_ROLE); _; } constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); } /// @inheritdoc ERC165 function supportsInterface( bytes4 interfaceId ) public view virtual override(IERC165, ERC7984, AccessControl) returns (bool) { return interfaceId == type(IERC7984Rwa).interfaceId || super.supportsInterface(interfaceId); } /// @dev Returns true if has admin role, false otherwise. function isAdmin(address account) public view virtual returns (bool) { return hasRole(DEFAULT_ADMIN_ROLE, account); } /// @dev Returns true if agent, false otherwise. function isAgent(address account) public view virtual returns (bool) { return hasRole(AGENT_ROLE, account); } /// @dev Adds agent. function addAgent(address account) public virtual onlyAdmin { _grantRole(AGENT_ROLE, account); } /// @dev Removes agent. function removeAgent(address account) public virtual onlyAdmin { _revokeRole(AGENT_ROLE, account); } /// @dev Pauses contract. function pause() public virtual onlyAgent { _pause(); } /// @dev Unpauses contract. function unpause() public virtual onlyAgent { _unpause(); } /// @dev Blocks a user account. function blockUser(address account) public virtual onlyAgent { _blockUser(account); } /// @dev Unblocks a user account. function unblockUser(address account) public virtual onlyAgent { _resetUser(account); } /// @dev Sets confidential frozen for an account with proof. function setConfidentialFrozen( address account, externalEuint64 encryptedAmount, bytes calldata inputProof ) public virtual onlyAgent { _setConfidentialFrozen(account, FHE.fromExternal(encryptedAmount, inputProof)); } /// @dev Sets confidential frozen for an account. function setConfidentialFrozen(address account, euint64 encryptedAmount) public virtual onlyAgent { require( FHE.isAllowed(encryptedAmount, msg.sender), ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, msg.sender) ); _setConfidentialFrozen(account, encryptedAmount); } /// @dev Mints confidential amount of tokens to account with proof. function confidentialMint( address to, externalEuint64 encryptedAmount, bytes calldata inputProof ) public virtual onlyAgent returns (euint64) { euint64 mintedAmount = _mint(to, FHE.fromExternal(encryptedAmount, inputProof)); FHE.allow(mintedAmount, msg.sender); return mintedAmount; } /// @dev Mints confidential amount of tokens to account. function confidentialMint(address to, euint64 encryptedAmount) public virtual onlyAgent returns (euint64) { require( FHE.isAllowed(encryptedAmount, msg.sender), ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, msg.sender) ); euint64 mintedAmount = _mint(to, encryptedAmount); FHE.allow(mintedAmount, msg.sender); return mintedAmount; } /// @dev Burns confidential amount of tokens from account with proof. function confidentialBurn( address account, externalEuint64 encryptedAmount, bytes calldata inputProof ) public virtual onlyAgent returns (euint64) { euint64 burntAmount = _burn(account, FHE.fromExternal(encryptedAmount, inputProof)); FHE.allow(burntAmount, msg.sender); return burntAmount; } /// @dev Burns confidential amount of tokens from account. function confidentialBurn(address account, euint64 encryptedAmount) public virtual onlyAgent returns (euint64) { require( FHE.isAllowed(encryptedAmount, msg.sender), ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, msg.sender) ); euint64 burntAmount = _burn(account, encryptedAmount); FHE.allow(burntAmount, msg.sender); return burntAmount; } /// @inheritdoc IERC7984Rwa function recoverAddress(address lostAccount, address newAccount) public virtual onlyAgent returns (euint64) { require(lostAccount != newAccount, ERC7984RwaSelfRecoveryNotAllowed()); euint64 balance = confidentialBalanceOf(lostAccount); euint64 lostFrozenBalance = confidentialFrozen(lostAccount); if (FHE.isInitialized(lostFrozenBalance)) { _setConfidentialFrozen(lostAccount, euint64.wrap(0)); } euint64 tokensRecovered = _transfer(lostAccount, newAccount, balance); FHE.allow(tokensRecovered, msg.sender); if (FHE.isInitialized(lostFrozenBalance)) { _setConfidentialFrozen( newAccount, FHESafeMath.saturatingAdd(confidentialFrozen(newAccount), FHE.min(tokensRecovered, lostFrozenBalance)) ); _setConfidentialFrozen(lostAccount, FHESafeMath.saturatingSub(lostFrozenBalance, tokensRecovered)); } Restriction restriction = getRestriction(lostAccount); if (restriction == Restriction.BLOCKED) { _blockUser(newAccount); } emit TokensRecovered(lostAccount, newAccount, tokensRecovered); return tokensRecovered; } /// @dev Variant of {forceConfidentialTransferFrom-address-address-euint64} with an input proof. function forceConfidentialTransferFrom( address from, address to, externalEuint64 encryptedAmount, bytes calldata inputProof ) public virtual onlyAgent returns (euint64) { euint64 transferred = _transfer(from, to, FHE.fromExternal(encryptedAmount, inputProof)); FHE.allow(transferred, msg.sender); return transferred; } /** * @dev Force transfer callable by the role {AGENT_ROLE} which transfers tokens from `from` to `to` and * bypasses the {ERC7984Restricted} (only on from) and https://docs.openzeppelin.com/contracts/api/utils#pausable[`++Pausable++`] * checks. Frozen tokens are not transferred and must be unfrozen first. */ function forceConfidentialTransferFrom( address from, address to, euint64 encryptedAmount ) public virtual onlyAgent returns (euint64) { require( FHE.isAllowed(encryptedAmount, msg.sender), ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, msg.sender) ); euint64 transferred = _transfer(from, to, encryptedAmount); FHE.allow(transferred, msg.sender); return transferred; } /// @inheritdoc ERC7984Freezable function confidentialAvailable( address account ) public virtual override(IERC7984Rwa, ERC7984Freezable) returns (euint64) { return super.confidentialAvailable(account); } /// @inheritdoc ERC7984Freezable function confidentialFrozen( address account ) public view virtual override(IERC7984Rwa, ERC7984Freezable) returns (euint64) { return super.confidentialFrozen(account); } /// @inheritdoc Pausable function paused() public view virtual override(IERC7984Rwa, Pausable) returns (bool) { return super.paused(); } /// @inheritdoc ERC7984Restricted function canTransact(address account) public view virtual override(IERC7984Rwa, ERC7984Restricted) returns (bool) { return super.canTransact(account); } /// @dev Internal function which updates confidential balances while performing frozen and restriction compliance checks. function _update( address from, address to, euint64 encryptedAmount ) internal virtual override(ERC7984Freezable, ERC7984Restricted) whenNotPaused returns (euint64) { // frozen and restriction checks performed through inheritance return super._update(from, to, encryptedAmount); } /// @dev Bypasses {ERC7984Restricted} `from` restriction check when performing a forced transfer or token recovery. function _checkSenderRestriction(address account) internal view override { if (_isForceTransfer(msg.sig)) { return; } super._checkSenderRestriction(account); } /// @dev Bypasses {ERC7984Restricted} `to` restriction check when performing a forced transfer or token recovery. function _checkRecipientRestriction(address account) internal view override { if (_isForceTransfer(msg.sig)) { return; } super._checkRecipientRestriction(account); } /// @dev Bypasses {Pausable} check when performing a {forceConfidentialTransferFrom}. function _requireNotPaused() internal view override { if (_isForceTransfer(msg.sig)) { return; } super._requireNotPaused(); } /// @dev Internal function which checks if the current function call should be treated as a force transfer. function _isForceTransfer(bytes4 selector) internal pure returns (bool) { return selector == 0x6c9c3c85 || // bytes4(keccak256("forceConfidentialTransferFrom(address,address,bytes32,bytes)")) selector == 0x44fd6e40 || // bytes4(keccak256("forceConfidentialTransferFrom(address,address,bytes32)")) selector == this.recoverAddress.selector; } /// @dev Restrict overrides of {Context._msgSender}. Please use other account abstraction methods instead. function _msgSender() internal view override returns (address) { return super._msgSender(); } /// @dev Restrict overrides of {Context._msgData}. Please use other account abstraction methods instead. function _msgData() internal view override returns (bytes calldata) { return super._msgData(); } }