diff --git a/contracts/emoney/AccountWalletRegistry.sol b/contracts/emoney/AccountWalletRegistry.sol new file mode 100644 index 0000000..3d3e2e2 --- /dev/null +++ b/contracts/emoney/AccountWalletRegistry.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IAccountWalletRegistry.sol"; + +/** + * @title AccountWalletRegistry + * @notice Maps regulated fiat accounts (IBAN, ABA) to Web3 wallets + * @dev Stores hashed account references (no PII on-chain). Supports 1-to-many mappings. + */ +contract AccountWalletRegistry is IAccountWalletRegistry, AccessControl { + bytes32 public constant ACCOUNT_MANAGER_ROLE = keccak256("ACCOUNT_MANAGER_ROLE"); + + // accountRefId => array of wallet links + mapping(bytes32 => WalletLink[]) private _accountToWallets; + // walletRefId => array of accountRefIds + mapping(bytes32 => bytes32[]) private _walletToAccounts; + // accountRefId => walletRefId => index in _accountToWallets array + mapping(bytes32 => mapping(bytes32 => uint256)) private _walletIndex; + // walletRefId => accountRefId => exists flag + mapping(bytes32 => mapping(bytes32 => bool)) private _walletAccountExists; + + /** + * @notice Initializes the registry with an admin address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + */ + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Links an account to a wallet + * @dev Requires ACCOUNT_MANAGER_ROLE. Creates a new link or reactivates an existing one. + * @param accountRefId The hashed account reference ID + * @param walletRefId The hashed wallet reference ID + * @param provider The provider identifier (e.g., "METAMASK", "FIREBLOCKS") + */ + function linkAccountToWallet( + bytes32 accountRefId, + bytes32 walletRefId, + bytes32 provider + ) external override onlyRole(ACCOUNT_MANAGER_ROLE) { + require(accountRefId != bytes32(0), "AccountWalletRegistry: zero accountRefId"); + require(walletRefId != bytes32(0), "AccountWalletRegistry: zero walletRefId"); + require(provider != bytes32(0), "AccountWalletRegistry: zero provider"); + + // Check if link already exists + if (_walletAccountExists[walletRefId][accountRefId]) { + // Reactivate existing link + uint256 index = _walletIndex[accountRefId][walletRefId]; + require(index < _accountToWallets[accountRefId].length, "AccountWalletRegistry: index out of bounds"); + WalletLink storage link = _accountToWallets[accountRefId][index]; + require(link.walletRefId == walletRefId, "AccountWalletRegistry: link mismatch"); + link.active = true; + link.linkedAt = uint64(block.timestamp); + } else { + // Create new link + WalletLink memory newLink = WalletLink({ + walletRefId: walletRefId, + linkedAt: uint64(block.timestamp), + active: true, + provider: provider + }); + + _accountToWallets[accountRefId].push(newLink); + _walletIndex[accountRefId][walletRefId] = _accountToWallets[accountRefId].length - 1; + _walletAccountExists[walletRefId][accountRefId] = true; + + // Add to reverse mapping + _walletToAccounts[walletRefId].push(accountRefId); + } + + emit AccountWalletLinked(accountRefId, walletRefId, provider, uint64(block.timestamp)); + } + + /** + * @notice Unlinks an account from a wallet (deactivates the link) + * @dev Requires ACCOUNT_MANAGER_ROLE. Sets link to inactive but doesn't remove it. + * @param accountRefId The hashed account reference ID + * @param walletRefId The hashed wallet reference ID + */ + function unlinkAccountFromWallet( + bytes32 accountRefId, + bytes32 walletRefId + ) external override onlyRole(ACCOUNT_MANAGER_ROLE) { + require(accountRefId != bytes32(0), "AccountWalletRegistry: zero accountRefId"); + require(walletRefId != bytes32(0), "AccountWalletRegistry: zero walletRefId"); + require(_walletAccountExists[walletRefId][accountRefId], "AccountWalletRegistry: link not found"); + + uint256 index = _walletIndex[accountRefId][walletRefId]; + require(index < _accountToWallets[accountRefId].length, "AccountWalletRegistry: index out of bounds"); + WalletLink storage link = _accountToWallets[accountRefId][index]; + require(link.walletRefId == walletRefId, "AccountWalletRegistry: link mismatch"); + + link.active = false; + + emit AccountWalletUnlinked(accountRefId, walletRefId); + } + + /** + * @notice Returns all wallet links for an account + * @param accountRefId The hashed account reference ID + * @return Array of wallet links + */ + function getWallets(bytes32 accountRefId) external view override returns (WalletLink[] memory) { + return _accountToWallets[accountRefId]; + } + + /** + * @notice Returns all account references for a wallet + * @param walletRefId The hashed wallet reference ID + * @return Array of account reference IDs + */ + function getAccounts(bytes32 walletRefId) external view override returns (bytes32[] memory) { + return _walletToAccounts[walletRefId]; + } + + /** + * @notice Checks if an account and wallet are linked + * @param accountRefId The hashed account reference ID + * @param walletRefId The hashed wallet reference ID + * @return true if linked (regardless of active status) + */ + function isLinked(bytes32 accountRefId, bytes32 walletRefId) external view override returns (bool) { + return _walletAccountExists[walletRefId][accountRefId]; + } + + /** + * @notice Checks if an account and wallet are actively linked + * @param accountRefId The hashed account reference ID + * @param walletRefId The hashed wallet reference ID + * @return true if linked and active + */ + function isActive(bytes32 accountRefId, bytes32 walletRefId) external view override returns (bool) { + if (!_walletAccountExists[walletRefId][accountRefId]) { + return false; + } + + uint256 index = _walletIndex[accountRefId][walletRefId]; + if (index >= _accountToWallets[accountRefId].length) { + return false; + } + + WalletLink memory link = _accountToWallets[accountRefId][index]; + return link.active && link.walletRefId == walletRefId; + } +} + diff --git a/contracts/emoney/BridgeVault138.sol b/contracts/emoney/BridgeVault138.sol new file mode 100644 index 0000000..69738ee --- /dev/null +++ b/contracts/emoney/BridgeVault138.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./interfaces/IBridgeVault138.sol"; +import "./interfaces/IPolicyManager.sol"; +import "./interfaces/IComplianceRegistry.sol"; +import "./errors/BridgeErrors.sol"; + +/// @notice Placeholder for light client verification +/// In production, this should integrate with an actual light client contract +interface ILightClient { + function verifyProof( + bytes32 sourceChain, + bytes32 sourceTx, + bytes calldata proof + ) external view returns (bool); +} + +/** + * @title BridgeVault138 + * @notice Lock/unlock portal for cross-chain token representation + * @dev Manages tokens locked for cross-chain transfers. Lock enforces liens via PolicyManager. + * Unlock requires light client proof verification and compliance checks. + */ +contract BridgeVault138 is IBridgeVault138, AccessControl, ReentrancyGuard { + bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE"); + + using SafeERC20 for IERC20; + + IPolicyManager public immutable policyManager; + IComplianceRegistry public immutable complianceRegistry; + ILightClient public lightClient; // Can be set after deployment + + /** + * @notice Initializes the bridge vault with registry addresses + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + * @param policyManager_ Address of PolicyManager contract + * @param complianceRegistry_ Address of ComplianceRegistry contract + */ + constructor(address admin, address policyManager_, address complianceRegistry_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + policyManager = IPolicyManager(policyManager_); + complianceRegistry = IComplianceRegistry(complianceRegistry_); + } + + /** + * @notice Sets the light client contract for proof verification + * @dev Requires DEFAULT_ADMIN_ROLE + * @param lightClient_ Address of the light client contract + */ + function setLightClient(address lightClient_) external onlyRole(DEFAULT_ADMIN_ROLE) { + lightClient = ILightClient(lightClient_); + } + + /** + * @notice Locks tokens for cross-chain transfer + * @dev Transfers tokens from user to vault. Enforces liens via PolicyManager.canTransfer. + * @param token Token address to lock + * @param amount Amount to lock + * @param targetChain Target chain identifier + * @param targetRecipient Recipient address on target chain + */ + function lock( + address token, + uint256 amount, + bytes32 targetChain, + address targetRecipient + ) external override nonReentrant { + if (token == address(0)) revert BridgeZeroToken(); + if (amount == 0) revert BridgeZeroAmount(); + if (targetRecipient == address(0)) revert BridgeZeroRecipient(); + + // Check if transfer would be allowed BEFORE transferring (checks liens, compliance, etc.) + (bool allowed, ) = policyManager.canTransfer(token, msg.sender, address(this), amount); + if (!allowed) revert BridgeTransferBlocked(token, msg.sender, address(this), amount); + + // Transfer tokens from user AFTER validation + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + emit Locked(token, msg.sender, amount, targetChain, targetRecipient); + } + + /** + * @notice Unlocks tokens from cross-chain transfer + * @dev Requires BRIDGE_OPERATOR_ROLE. Verifies proof via light client and checks compliance. + * Transfers tokens from vault to recipient. + * @param token Token address to unlock + * @param to Recipient address + * @param amount Amount to unlock + * @param sourceChain Source chain identifier + * @param sourceTx Source transaction hash + * @param proof Proof data for light client verification + */ + function unlock( + address token, + address to, + uint256 amount, + bytes32 sourceChain, + bytes32 sourceTx, + bytes calldata proof + ) external override onlyRole(BRIDGE_OPERATOR_ROLE) nonReentrant { + if (token == address(0)) revert BridgeZeroToken(); + if (to == address(0)) revert BridgeZeroRecipient(); + if (amount == 0) revert BridgeZeroAmount(); + + // Verify proof via light client + if (address(lightClient) == address(0)) revert BridgeLightClientNotSet(); + bool verified = lightClient.verifyProof(sourceChain, sourceTx, proof); + if (!verified) revert BridgeProofVerificationFailed(sourceChain, sourceTx); + + // Check compliance + if (!complianceRegistry.isAllowed(to)) revert BridgeRecipientNotCompliant(to); + if (complianceRegistry.isFrozen(to)) revert BridgeRecipientFrozen(to); + + // Transfer tokens to recipient + IERC20(token).safeTransfer(to, amount); + + emit Unlocked(token, to, amount, sourceChain, sourceTx); + } +} + diff --git a/contracts/emoney/ComplianceRegistry.sol b/contracts/emoney/ComplianceRegistry.sol new file mode 100644 index 0000000..5e2d367 --- /dev/null +++ b/contracts/emoney/ComplianceRegistry.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IComplianceRegistry.sol"; + +/** + * @title ComplianceRegistry + * @notice Manages compliance status for accounts including allowed/frozen flags, risk tiers, and jurisdiction information + * @dev This registry is consulted by PolicyManager during transfer authorization to enforce compliance rules + */ +contract ComplianceRegistry is IComplianceRegistry, AccessControl { + bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE"); + + struct ComplianceStatus { + bool allowed; + bool frozen; + uint8 riskTier; + bytes32 jurisdictionHash; + } + + mapping(address => ComplianceStatus) private _compliance; + + /** + * @notice Initializes the ComplianceRegistry with an admin address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + */ + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Returns whether an account is allowed (compliant) + * @param account Address to check + * @return true if account is allowed, false otherwise + */ + function isAllowed(address account) external view override returns (bool) { + return _compliance[account].allowed; + } + + /** + * @notice Returns whether an account is frozen + * @param account Address to check + * @return true if account is frozen, false otherwise + */ + function isFrozen(address account) external view override returns (bool) { + return _compliance[account].frozen; + } + + /** + * @notice Returns the risk tier for an account + * @param account Address to check + * @return Risk tier value (0-255) + */ + function riskTier(address account) external view override returns (uint8) { + return _compliance[account].riskTier; + } + + /** + * @notice Returns the jurisdiction hash for an account + * @param account Address to check + * @return bytes32 hash representing the jurisdiction + */ + function jurisdictionHash(address account) external view override returns (bytes32) { + return _compliance[account].jurisdictionHash; + } + + /** + * @notice Sets compliance status for an account + * @dev Requires COMPLIANCE_ROLE + * @param account Address to update + * @param allowed Whether the account is allowed (compliant) + * @param tier Risk tier (0-255) + * @param jurHash Jurisdiction hash (e.g., keccak256("US")) + */ + function setCompliance( + address account, + bool allowed, + uint8 tier, + bytes32 jurHash + ) external override onlyRole(COMPLIANCE_ROLE) { + _compliance[account].allowed = allowed; + _compliance[account].riskTier = tier; + _compliance[account].jurisdictionHash = jurHash; + + emit ComplianceUpdated(account, allowed, tier, jurHash); + } + + /** + * @notice Sets frozen status for an account + * @dev Requires COMPLIANCE_ROLE. Frozen accounts cannot send or receive tokens. + * @param account Address to update + * @param frozen Whether the account should be frozen + */ + function setFrozen(address account, bool frozen) external override onlyRole(COMPLIANCE_ROLE) { + _compliance[account].frozen = frozen; + emit FrozenUpdated(account, frozen); + } +} + diff --git a/contracts/emoney/DebtRegistry.sol b/contracts/emoney/DebtRegistry.sol new file mode 100644 index 0000000..3970a6f --- /dev/null +++ b/contracts/emoney/DebtRegistry.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IDebtRegistry.sol"; +import "./errors/RegistryErrors.sol"; + +/** + * @title DebtRegistry + * @notice Manages liens (encumbrances) on accounts for debt/liability enforcement + * @dev Supports multiple liens per account with aggregation. Uses hard expiry policy - expiry is informational and requires explicit release. + * Liens are used by eMoneyToken to enforce transfer restrictions (hard freeze or encumbered modes). + */ +contract DebtRegistry is IDebtRegistry, AccessControl { + bytes32 public constant DEBT_AUTHORITY_ROLE = keccak256("DEBT_AUTHORITY_ROLE"); + + uint256 private _nextLienId; + mapping(uint256 => Lien) private _liens; + mapping(address => uint256) private _activeEncumbrance; + mapping(address => uint256) private _activeLienCount; + + /** + * @notice Initializes the DebtRegistry with an admin address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + */ + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Returns the total active encumbrance (sum of all active lien amounts) for a debtor + * @param debtor Address to check + * @return Total amount encumbered across all active liens + */ + function activeLienAmount(address debtor) external view override returns (uint256) { + return _activeEncumbrance[debtor]; + } + + /** + * @notice Returns whether a debtor has any active liens + * @param debtor Address to check + * @return true if debtor has at least one active lien, false otherwise + */ + function hasActiveLien(address debtor) external view override returns (bool) { + return _activeLienCount[debtor] > 0; + } + + /** + * @notice Returns the number of active liens for a debtor + * @param debtor Address to check + * @return Count of active liens + */ + function activeLienCount(address debtor) external view override returns (uint256) { + return _activeLienCount[debtor]; + } + + /** + * @notice Returns full lien information for a given lien ID + * @param lienId The lien identifier + * @return Lien struct containing all lien details + */ + function getLien(uint256 lienId) external view override returns (Lien memory) { + return _liens[lienId]; + } + + /** + * @notice Places a new lien on a debtor account + * @dev Requires DEBT_AUTHORITY_ROLE. Increments active encumbrance and lien count. + * @param debtor Address to place lien on + * @param amount Amount to encumber + * @param expiry Expiry timestamp (0 = no expiry). Note: expiry is informational; explicit release required. + * @param priority Priority level (0-255) + * @param reasonCode Reason code for the lien (e.g., ReasonCodes.LIEN_BLOCK) + * @return lienId The assigned lien identifier + */ + function placeLien( + address debtor, + uint256 amount, + uint64 expiry, + uint8 priority, + bytes32 reasonCode + ) external override onlyRole(DEBT_AUTHORITY_ROLE) returns (uint256 lienId) { + if (debtor == address(0)) revert DebtZeroDebtor(); + if (amount == 0) revert DebtZeroAmount(); + + lienId = _nextLienId++; + _liens[lienId] = Lien({ + debtor: debtor, + amount: amount, + expiry: expiry, + priority: priority, + authority: msg.sender, + reasonCode: reasonCode, + active: true + }); + + _activeEncumbrance[debtor] += amount; + _activeLienCount[debtor]++; + + emit LienPlaced(lienId, debtor, amount, expiry, priority, msg.sender, reasonCode); + } + + /** + * @notice Reduces the amount of an active lien + * @dev Requires DEBT_AUTHORITY_ROLE. Updates active encumbrance accordingly. + * @param lienId The lien identifier + * @param reduceBy Amount to reduce the lien by (must not exceed current lien amount) + */ + function reduceLien(uint256 lienId, uint256 reduceBy) external override onlyRole(DEBT_AUTHORITY_ROLE) { + Lien storage lien = _liens[lienId]; + if (!lien.active) revert DebtLienNotActive(lienId); + + uint256 oldAmount = lien.amount; + if (reduceBy > oldAmount) revert DebtReduceByExceedsAmount(lienId, reduceBy, oldAmount); + + uint256 newAmount = oldAmount - reduceBy; + lien.amount = newAmount; + + _activeEncumbrance[lien.debtor] -= reduceBy; + + emit LienReduced(lienId, reduceBy, newAmount); + } + + /** + * @notice Releases an active lien, removing it from active tracking + * @dev Requires DEBT_AUTHORITY_ROLE. Decrements active encumbrance and lien count. + * @param lienId The lien identifier to release + */ + function releaseLien(uint256 lienId) external override onlyRole(DEBT_AUTHORITY_ROLE) { + Lien storage lien = _liens[lienId]; + if (!lien.active) revert DebtLienNotActive(lienId); + + lien.active = false; + _activeEncumbrance[lien.debtor] -= lien.amount; + _activeLienCount[lien.debtor]--; + + emit LienReleased(lienId); + } +} + diff --git a/contracts/emoney/ISO20022Router.sol b/contracts/emoney/ISO20022Router.sol new file mode 100644 index 0000000..46bbcaa --- /dev/null +++ b/contracts/emoney/ISO20022Router.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IISO20022Router.sol"; +import "./interfaces/IRailTriggerRegistry.sol"; +import "./libraries/RailTypes.sol"; +import "./libraries/ISO20022Types.sol"; + +/** + * @title ISO20022Router + * @notice Normalizes ISO-20022 messages into canonical on-chain format + * @dev Creates triggers in RailTriggerRegistry for both inbound and outbound messages + */ +contract ISO20022Router is IISO20022Router, AccessControl { + bytes32 public constant RAIL_OPERATOR_ROLE = keccak256("RAIL_OPERATOR_ROLE"); + + IRailTriggerRegistry public immutable triggerRegistry; + mapping(bytes32 => uint256) private _triggerIdByInstructionId; // instructionId => triggerId + + /** + * @notice Initializes the router with registry address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + * @param triggerRegistry_ Address of RailTriggerRegistry contract + */ + constructor(address admin, address triggerRegistry_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + require(triggerRegistry_ != address(0), "ISO20022Router: zero triggerRegistry"); + triggerRegistry = IRailTriggerRegistry(triggerRegistry_); + } + + /** + * @notice Submits an inbound ISO-20022 message (rail confirmation/notification) + * @dev Requires RAIL_OPERATOR_ROLE. Creates a trigger in CREATED state. + * @param m Canonical message struct + * @return triggerId The created trigger ID + */ + function submitInbound( + CanonicalMessage calldata m + ) external override onlyRole(RAIL_OPERATOR_ROLE) returns (uint256 triggerId) { + require(m.msgType != bytes32(0), "ISO20022Router: zero msgType"); + require(m.instructionId != bytes32(0), "ISO20022Router: zero instructionId"); + require(m.accountRefId != bytes32(0), "ISO20022Router: zero accountRefId"); + require(m.token != address(0), "ISO20022Router: zero token"); + require(m.amount > 0, "ISO20022Router: zero amount"); + + // Determine rail from message type (simplified - in production, this would be more sophisticated) + RailTypes.Rail rail = _determineRailFromMessageType(m.msgType); + + // Create trigger + IRailTriggerRegistry.Trigger memory trigger = IRailTriggerRegistry.Trigger({ + id: 0, // Will be assigned by registry + rail: rail, + msgType: m.msgType, + accountRefId: m.accountRefId, + walletRefId: bytes32(0), // Will be resolved by orchestrator if needed + token: m.token, + amount: m.amount, + currencyCode: m.currencyCode, + instructionId: m.instructionId, + state: RailTypes.State.CREATED, + createdAt: 0, // Will be set by registry + updatedAt: 0 // Will be set by registry + }); + + triggerId = triggerRegistry.createTrigger(trigger); + _triggerIdByInstructionId[m.instructionId] = triggerId; + + emit InboundSubmitted(triggerId, m.msgType, m.instructionId, m.accountRefId); + } + + /** + * @notice Submits an outbound ISO-20022 message (rail initiation) + * @dev Requires RAIL_OPERATOR_ROLE. Creates a trigger in CREATED state. + * @param m Canonical message struct + * @return triggerId The created trigger ID + */ + function submitOutbound( + CanonicalMessage calldata m + ) external override onlyRole(RAIL_OPERATOR_ROLE) returns (uint256 triggerId) { + require(m.msgType != bytes32(0), "ISO20022Router: zero msgType"); + require(m.instructionId != bytes32(0), "ISO20022Router: zero instructionId"); + require(m.accountRefId != bytes32(0), "ISO20022Router: zero accountRefId"); + require(m.token != address(0), "ISO20022Router: zero token"); + require(m.amount > 0, "ISO20022Router: zero amount"); + + // Determine rail from message type + RailTypes.Rail rail = _determineRailFromMessageType(m.msgType); + + // Create trigger + IRailTriggerRegistry.Trigger memory trigger = IRailTriggerRegistry.Trigger({ + id: 0, // Will be assigned by registry + rail: rail, + msgType: m.msgType, + accountRefId: m.accountRefId, + walletRefId: bytes32(0), // Will be resolved by orchestrator if needed + token: m.token, + amount: m.amount, + currencyCode: m.currencyCode, + instructionId: m.instructionId, + state: RailTypes.State.CREATED, + createdAt: 0, // Will be set by registry + updatedAt: 0 // Will be set by registry + }); + + triggerId = triggerRegistry.createTrigger(trigger); + _triggerIdByInstructionId[m.instructionId] = triggerId; + + emit OutboundSubmitted(triggerId, m.msgType, m.instructionId, m.accountRefId); + } + + /** + * @notice Returns the trigger ID for a given instruction ID + * @param instructionId The instruction ID + * @return The trigger ID (0 if not found) + */ + function getTriggerIdByInstructionId(bytes32 instructionId) external view override returns (uint256) { + return _triggerIdByInstructionId[instructionId]; + } + + /** + * @notice Determines the rail type from an ISO-20022 message type + * @dev Simplified implementation - in production, this would use a mapping table + * @param msgType The message type + * @return The rail type + */ + function _determineRailFromMessageType(bytes32 msgType) internal pure returns (RailTypes.Rail) { + // This is a simplified implementation + // In production, you would have a mapping table or more sophisticated logic + // For now, we'll use a default based on message family + if (msgType == ISO20022Types.PAIN_001 || msgType == ISO20022Types.PACS_008) { + // These are commonly used across rails, default to SWIFT + return RailTypes.Rail.SWIFT; + } + // Default to SWIFT for unknown types + return RailTypes.Rail.SWIFT; + } +} + diff --git a/contracts/emoney/PacketRegistry.sol b/contracts/emoney/PacketRegistry.sol new file mode 100644 index 0000000..38fefb2 --- /dev/null +++ b/contracts/emoney/PacketRegistry.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IPacketRegistry.sol"; + +/** + * @title PacketRegistry + * @notice Records packet lifecycle events for non-scheme participants + * @dev Tracks packet generation, dispatch, and acknowledgment linked to ChainID 138 triggers + * Provides tamper-evident audit trail for instruction packets sent via secure email, AS4, or PDF + */ +contract PacketRegistry is IPacketRegistry, AccessControl { + bytes32 public constant PACKET_OPERATOR_ROLE = keccak256("PACKET_OPERATOR_ROLE"); + + // triggerId => latest packet info + mapping(uint256 => PacketInfo) private _packets; + + struct PacketInfo { + bytes32 payloadHash; + bytes32 mode; + bytes32 channel; + bytes32 messageRef; + bytes32 receiptRef; + bytes32 status; + bool generated; + bool dispatched; + bool acknowledged; + } + + /** + * @notice Initializes the registry with an admin address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + */ + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Records that a packet has been generated + * @dev Requires PACKET_OPERATOR_ROLE + * @param triggerId The trigger ID from RailTriggerRegistry + * @param payloadHash SHA-256 hash of the packet payload + * @param mode Transmission mode (e.g., "PDF", "EMAIL", "AS4") + */ + function recordGenerated( + uint256 triggerId, + bytes32 payloadHash, + bytes32 mode + ) external override onlyRole(PACKET_OPERATOR_ROLE) { + require(triggerId > 0, "PacketRegistry: zero triggerId"); + require(payloadHash != bytes32(0), "PacketRegistry: zero payloadHash"); + require(mode != bytes32(0), "PacketRegistry: zero mode"); + require(!_packets[triggerId].generated, "PacketRegistry: already generated"); + + _packets[triggerId].payloadHash = payloadHash; + _packets[triggerId].mode = mode; + _packets[triggerId].generated = true; + + emit PacketGenerated(triggerId, payloadHash, mode); + } + + /** + * @notice Records that a packet has been dispatched via a channel + * @dev Requires PACKET_OPERATOR_ROLE. Packet must have been generated first. + * @param triggerId The trigger ID from RailTriggerRegistry + * @param channel The dispatch channel (e.g., "EMAIL", "AS4", "PORTAL") + * @param messageRef The message reference ID from the transport layer + */ + function recordDispatched( + uint256 triggerId, + bytes32 channel, + bytes32 messageRef + ) external override onlyRole(PACKET_OPERATOR_ROLE) { + require(triggerId > 0, "PacketRegistry: zero triggerId"); + require(channel != bytes32(0), "PacketRegistry: zero channel"); + require(messageRef != bytes32(0), "PacketRegistry: zero messageRef"); + require(_packets[triggerId].generated, "PacketRegistry: not generated"); + require(!_packets[triggerId].dispatched, "PacketRegistry: already dispatched"); + + _packets[triggerId].channel = channel; + _packets[triggerId].messageRef = messageRef; + _packets[triggerId].dispatched = true; + + emit PacketDispatched(triggerId, channel, messageRef); + } + + /** + * @notice Records that a packet has been acknowledged by the recipient + * @dev Requires PACKET_OPERATOR_ROLE. Packet must have been dispatched first. + * @param triggerId The trigger ID from RailTriggerRegistry + * @param receiptRef The receipt reference ID from the recipient + * @param status The acknowledgment status (e.g., "RECEIVED", "ACCEPTED", "REJECTED") + */ + function recordAcknowledged( + uint256 triggerId, + bytes32 receiptRef, + bytes32 status + ) external override onlyRole(PACKET_OPERATOR_ROLE) { + require(triggerId > 0, "PacketRegistry: zero triggerId"); + require(receiptRef != bytes32(0), "PacketRegistry: zero receiptRef"); + require(status != bytes32(0), "PacketRegistry: zero status"); + require(_packets[triggerId].dispatched, "PacketRegistry: not dispatched"); + require(!_packets[triggerId].acknowledged, "PacketRegistry: already acknowledged"); + + _packets[triggerId].receiptRef = receiptRef; + _packets[triggerId].status = status; + _packets[triggerId].acknowledged = true; + + emit PacketAcknowledged(triggerId, receiptRef, status); + } + + /** + * @notice Returns packet information for a trigger + * @param triggerId The trigger ID + * @return payloadHash The payload hash + * @return mode The transmission mode + * @return channel The dispatch channel + * @return messageRef The message reference + * @return receiptRef The receipt reference + * @return status The acknowledgment status + * @return generated Whether packet was generated + * @return dispatched Whether packet was dispatched + * @return acknowledged Whether packet was acknowledged + */ + function getPacketInfo( + uint256 triggerId + ) + external + view + returns ( + bytes32 payloadHash, + bytes32 mode, + bytes32 channel, + bytes32 messageRef, + bytes32 receiptRef, + bytes32 status, + bool generated, + bool dispatched, + bool acknowledged + ) + { + PacketInfo memory info = _packets[triggerId]; + return ( + info.payloadHash, + info.mode, + info.channel, + info.messageRef, + info.receiptRef, + info.status, + info.generated, + info.dispatched, + info.acknowledged + ); + } +} + diff --git a/contracts/emoney/PolicyManager.sol b/contracts/emoney/PolicyManager.sol new file mode 100644 index 0000000..1c6151a --- /dev/null +++ b/contracts/emoney/PolicyManager.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IPolicyManager.sol"; +import "./interfaces/IComplianceRegistry.sol"; +import "./interfaces/IDebtRegistry.sol"; +import "./libraries/ReasonCodes.sol"; +import "./errors/RegistryErrors.sol"; + +/** + * @title PolicyManager + * @notice Central rule engine for transfer authorization across all eMoney tokens + * @dev Consults ComplianceRegistry and DebtRegistry to make transfer decisions. + * Manages per-token configuration including pause state, bridge-only mode, and lien modes. + * Lien enforcement is performed by eMoneyToken based on the lien mode returned here. + */ +contract PolicyManager is IPolicyManager, AccessControl { + bytes32 public constant POLICY_OPERATOR_ROLE = keccak256("POLICY_OPERATOR_ROLE"); + + struct TokenConfig { + bool paused; + bool bridgeOnly; + address bridge; + uint8 lienMode; // 0 = off, 1 = hard freeze, 2 = encumbered + } + + IComplianceRegistry public immutable complianceRegistry; + IDebtRegistry public immutable debtRegistry; + + mapping(address => TokenConfig) private _tokenConfigs; + mapping(address => mapping(address => bool)) private _tokenFreezes; // token => account => frozen + + /** + * @notice Initializes PolicyManager with registry addresses + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + * @param compliance Address of ComplianceRegistry contract + * @param debt Address of DebtRegistry contract + */ + constructor(address admin, address compliance, address debt) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + complianceRegistry = IComplianceRegistry(compliance); + debtRegistry = IDebtRegistry(debt); + } + + /** + * @notice Returns whether a token is paused + * @param token Token address to check + * @return true if token is paused, false otherwise + */ + function isPaused(address token) external view override returns (bool) { + return _tokenConfigs[token].paused; + } + + /** + * @notice Returns whether a token is in bridge-only mode + * @param token Token address to check + * @return true if token only allows transfers to/from bridge, false otherwise + */ + function bridgeOnly(address token) external view override returns (bool) { + return _tokenConfigs[token].bridgeOnly; + } + + /** + * @notice Returns the bridge address for a token + * @param token Token address to check + * @return Bridge address (zero address if not set) + */ + function bridge(address token) external view override returns (address) { + return _tokenConfigs[token].bridge; + } + + /** + * @notice Returns the lien mode for a token + * @param token Token address to check + * @return Lien mode: 0 = off, 1 = hard freeze, 2 = encumbered + */ + function lienMode(address token) external view override returns (uint8) { + return _tokenConfigs[token].lienMode; + } + + /** + * @notice Returns whether an account is frozen for a specific token + * @param token Token address to check + * @param account Address to check + * @return true if account is frozen for this token, false otherwise + */ + function isTokenFrozen(address token, address account) external view override returns (bool) { + return _tokenFreezes[token][account]; + } + + /** + * @notice Determines if a transfer should be allowed + * @dev Checks in order: paused, token freezes, compliance freezes, compliance allowed status, bridge-only mode. + * Lien checks are performed by eMoneyToken based on lien mode. + * @param token Token address + * @param from Sender address + * @param to Recipient address + * @param amount Transfer amount (unused but required for interface) + * @return allowed true if transfer should be allowed, false otherwise + * @return reasonCode bytes32 reason code (ReasonCodes.OK if allowed, otherwise the blocking reason) + */ + function canTransfer( + address token, + address from, + address to, + uint256 amount + ) external view override returns (bool allowed, bytes32 reasonCode) { + TokenConfig memory config = _tokenConfigs[token]; + + // Check paused + if (config.paused) { + return (false, ReasonCodes.PAUSED); + } + + // Check token-specific freezes + if (_tokenFreezes[token][from]) { + return (false, ReasonCodes.FROM_FROZEN); + } + if (_tokenFreezes[token][to]) { + return (false, ReasonCodes.TO_FROZEN); + } + + // Check compliance registry freezes + if (complianceRegistry.isFrozen(from)) { + return (false, ReasonCodes.FROM_FROZEN); + } + if (complianceRegistry.isFrozen(to)) { + return (false, ReasonCodes.TO_FROZEN); + } + + // Check compliance allowed status + if (!complianceRegistry.isAllowed(from)) { + return (false, ReasonCodes.FROM_NOT_COMPLIANT); + } + if (!complianceRegistry.isAllowed(to)) { + return (false, ReasonCodes.TO_NOT_COMPLIANT); + } + + // Check bridgeOnly mode + if (config.bridgeOnly) { + if (from != config.bridge && to != config.bridge) { + return (false, ReasonCodes.BRIDGE_ONLY); + } + } + + // Lien mode checks are handled in eMoneyToken._update + // PolicyManager only provides the lien mode, not the enforcement + + return (true, ReasonCodes.OK); + } + + /** + * @notice Sets the paused state for a token + * @dev Requires POLICY_OPERATOR_ROLE. When paused, all transfers are blocked. + * @param token Token address to configure + * @param paused true to pause, false to unpause + */ + function setPaused(address token, bool paused) external override onlyRole(POLICY_OPERATOR_ROLE) { + _tokenConfigs[token].paused = paused; + emit TokenPaused(token, paused); + } + + /** + * @notice Sets bridge-only mode for a token + * @dev Requires POLICY_OPERATOR_ROLE. When enabled, only transfers to/from the bridge address are allowed. + * @param token Token address to configure + * @param enabled true to enable bridge-only mode, false to disable + */ + function setBridgeOnly(address token, bool enabled) external override onlyRole(POLICY_OPERATOR_ROLE) { + _tokenConfigs[token].bridgeOnly = enabled; + emit BridgeOnlySet(token, enabled); + } + + /** + * @notice Sets the bridge address for a token + * @dev Requires POLICY_OPERATOR_ROLE. Used in bridge-only mode. + * @param token Token address to configure + * @param bridgeAddr Address of the bridge contract + */ + function setBridge(address token, address bridgeAddr) external override onlyRole(POLICY_OPERATOR_ROLE) { + _tokenConfigs[token].bridge = bridgeAddr; + emit BridgeSet(token, bridgeAddr); + } + + /** + * @notice Sets the lien mode for a token + * @dev Requires POLICY_OPERATOR_ROLE. Valid modes: 0 = off, 1 = hard freeze, 2 = encumbered. + * @param token Token address to configure + * @param mode Lien mode (0, 1, or 2) + */ + function setLienMode(address token, uint8 mode) external override onlyRole(POLICY_OPERATOR_ROLE) { + if (mode > 2) revert PolicyInvalidLienMode(mode); + + TokenConfig storage config = _tokenConfigs[token]; + bool isNewToken = config.lienMode == 0 && mode != 0 && !config.paused && !config.bridgeOnly && config.bridge == address(0); + + config.lienMode = mode; + + if (isNewToken) { + emit TokenConfigured(token, config.paused, config.bridgeOnly, config.bridge, mode); + } + + emit LienModeSet(token, mode); + } + + /** + * @notice Freezes or unfreezes an account for a specific token + * @dev Requires POLICY_OPERATOR_ROLE. Per-token freeze (in addition to global compliance freezes). + * @param token Token address + * @param account Address to freeze/unfreeze + * @param frozen true to freeze, false to unfreeze + */ + function freeze(address token, address account, bool frozen) external override onlyRole(POLICY_OPERATOR_ROLE) { + _tokenFreezes[token][account] = frozen; + emit TokenFreeze(token, account, frozen); + } +} + diff --git a/contracts/emoney/RailEscrowVault.sol b/contracts/emoney/RailEscrowVault.sol new file mode 100644 index 0000000..1c1ecbb --- /dev/null +++ b/contracts/emoney/RailEscrowVault.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./interfaces/IRailEscrowVault.sol"; +import "./libraries/RailTypes.sol"; + +/** + * @title RailEscrowVault + * @notice Holds tokens locked for outbound rail transfers + * @dev Similar pattern to BridgeVault138. Manages per-trigger escrow tracking. + */ +contract RailEscrowVault is IRailEscrowVault, AccessControl { + bytes32 public constant SETTLEMENT_OPERATOR_ROLE = keccak256("SETTLEMENT_OPERATOR_ROLE"); + + using SafeERC20 for IERC20; + + // token => triggerId => escrow amount + mapping(address => mapping(uint256 => uint256)) private _escrow; + // token => total escrow amount + mapping(address => uint256) private _totalEscrow; + + /** + * @notice Initializes the vault with an admin address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + */ + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Locks tokens for a rail transfer + * @dev Requires SETTLEMENT_OPERATOR_ROLE. Transfers tokens from user to vault. + * @param token Token address to lock + * @param from Address to transfer tokens from + * @param amount Amount to lock + * @param triggerId The trigger ID associated with this escrow + * @param rail The payment rail type + */ + function lock( + address token, + address from, + uint256 amount, + uint256 triggerId, + RailTypes.Rail rail + ) external override onlyRole(SETTLEMENT_OPERATOR_ROLE) { + require(token != address(0), "RailEscrowVault: zero token"); + require(from != address(0), "RailEscrowVault: zero from"); + require(amount > 0, "RailEscrowVault: zero amount"); + require(triggerId > 0, "RailEscrowVault: zero triggerId"); + + // Transfer tokens from user to vault + IERC20(token).safeTransferFrom(from, address(this), amount); + + // Update escrow tracking + _escrow[token][triggerId] += amount; + _totalEscrow[token] += amount; + + emit Locked(token, from, amount, triggerId, uint8(rail)); + } + + /** + * @notice Releases escrowed tokens + * @dev Requires SETTLEMENT_OPERATOR_ROLE. Transfers tokens from vault to recipient. + * @param token Token address to release + * @param to Recipient address + * @param amount Amount to release + * @param triggerId The trigger ID associated with this escrow + */ + function release( + address token, + address to, + uint256 amount, + uint256 triggerId + ) external override onlyRole(SETTLEMENT_OPERATOR_ROLE) { + require(token != address(0), "RailEscrowVault: zero token"); + require(to != address(0), "RailEscrowVault: zero to"); + require(amount > 0, "RailEscrowVault: zero amount"); + require(triggerId > 0, "RailEscrowVault: zero triggerId"); + require(_escrow[token][triggerId] >= amount, "RailEscrowVault: insufficient escrow"); + + // Update escrow tracking + _escrow[token][triggerId] -= amount; + _totalEscrow[token] -= amount; + + // Transfer tokens to recipient + IERC20(token).safeTransfer(to, amount); + + emit Released(token, to, amount, triggerId); + } + + /** + * @notice Returns the escrow amount for a specific trigger + * @param token Token address + * @param triggerId The trigger ID + * @return The escrow amount + */ + function getEscrowAmount(address token, uint256 triggerId) external view override returns (uint256) { + return _escrow[token][triggerId]; + } + + /** + * @notice Returns the total escrow amount for a token + * @param token Token address + * @return The total escrow amount + */ + function getTotalEscrow(address token) external view override returns (uint256) { + return _totalEscrow[token]; + } +} + diff --git a/contracts/emoney/RailTriggerRegistry.sol b/contracts/emoney/RailTriggerRegistry.sol new file mode 100644 index 0000000..74263c1 --- /dev/null +++ b/contracts/emoney/RailTriggerRegistry.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./interfaces/IRailTriggerRegistry.sol"; +import "./libraries/RailTypes.sol"; + +/** + * @title RailTriggerRegistry + * @notice Canonical registry of payment rails, message types, and trigger lifecycle + * @dev Manages trigger state machine and enforces idempotency by instructionId + */ +contract RailTriggerRegistry is IRailTriggerRegistry, AccessControl { + bytes32 public constant RAIL_OPERATOR_ROLE = keccak256("RAIL_OPERATOR_ROLE"); + bytes32 public constant RAIL_ADAPTER_ROLE = keccak256("RAIL_ADAPTER_ROLE"); + + uint256 private _nextTriggerId; + mapping(uint256 => Trigger) private _triggers; + mapping(bytes32 => uint256) private _triggerByInstructionId; // instructionId => triggerId + + /** + * @notice Initializes the registry with an admin address + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + */ + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @notice Creates a new trigger + * @dev Requires RAIL_OPERATOR_ROLE. Enforces idempotency by instructionId. + * @param t Trigger struct with all required fields + * @return id The assigned trigger ID + */ + function createTrigger(Trigger calldata t) external override onlyRole(RAIL_OPERATOR_ROLE) returns (uint256 id) { + require(t.token != address(0), "RailTriggerRegistry: zero token"); + require(t.amount > 0, "RailTriggerRegistry: zero amount"); + require(t.accountRefId != bytes32(0), "RailTriggerRegistry: zero accountRefId"); + require(t.instructionId != bytes32(0), "RailTriggerRegistry: zero instructionId"); + require(t.state == RailTypes.State.CREATED, "RailTriggerRegistry: invalid initial state"); + + // Enforce idempotency: check if instructionId already exists + require(!instructionIdExists(t.instructionId), "RailTriggerRegistry: duplicate instructionId"); + + id = _nextTriggerId++; + uint64 timestamp = uint64(block.timestamp); + + _triggers[id] = Trigger({ + id: id, + rail: t.rail, + msgType: t.msgType, + accountRefId: t.accountRefId, + walletRefId: t.walletRefId, + token: t.token, + amount: t.amount, + currencyCode: t.currencyCode, + instructionId: t.instructionId, + state: RailTypes.State.CREATED, + createdAt: timestamp, + updatedAt: timestamp + }); + + _triggerByInstructionId[t.instructionId] = id; + + emit TriggerCreated( + id, + uint8(t.rail), + t.msgType, + t.instructionId, + t.accountRefId, + t.token, + t.amount + ); + } + + /** + * @notice Updates the state of a trigger + * @dev Requires RAIL_ADAPTER_ROLE. Enforces valid state transitions. + * @param id The trigger ID + * @param newState The new state + * @param reason Optional reason code for the state change + */ + function updateState( + uint256 id, + RailTypes.State newState, + bytes32 reason + ) external override onlyRole(RAIL_ADAPTER_ROLE) { + require(triggerExists(id), "RailTriggerRegistry: trigger not found"); + + Trigger storage trigger = _triggers[id]; + RailTypes.State oldState = trigger.state; + + // Validate state transition + require(isValidStateTransition(oldState, newState), "RailTriggerRegistry: invalid state transition"); + + trigger.state = newState; + trigger.updatedAt = uint64(block.timestamp); + + emit TriggerStateUpdated(id, uint8(oldState), uint8(newState), reason); + } + + /** + * @notice Returns a trigger by ID + * @param id The trigger ID + * @return The trigger struct + */ + function getTrigger(uint256 id) external view override returns (Trigger memory) { + require(triggerExists(id), "RailTriggerRegistry: trigger not found"); + return _triggers[id]; + } + + /** + * @notice Returns a trigger by instructionId + * @param instructionId The instruction ID + * @return The trigger struct + */ + function getTriggerByInstructionId(bytes32 instructionId) external view override returns (Trigger memory) { + uint256 id = _triggerByInstructionId[instructionId]; + require(id != 0 || _triggers[id].instructionId == instructionId, "RailTriggerRegistry: trigger not found"); + return _triggers[id]; + } + + /** + * @notice Checks if a trigger exists + * @param id The trigger ID + * @return true if trigger exists + */ + function triggerExists(uint256 id) public view override returns (bool) { + return _triggers[id].id == id && _triggers[id].instructionId != bytes32(0); + } + + /** + * @notice Checks if an instructionId already exists + * @param instructionId The instruction ID to check + * @return true if instructionId exists + */ + function instructionIdExists(bytes32 instructionId) public view override returns (bool) { + uint256 id = _triggerByInstructionId[instructionId]; + return id != 0 && _triggers[id].instructionId == instructionId; + } + + /** + * @notice Validates a state transition + * @param from Current state + * @param to Target state + * @return true if transition is valid + */ + function isValidStateTransition( + RailTypes.State from, + RailTypes.State to + ) internal pure returns (bool) { + // Cannot transition to CREATED + if (to == RailTypes.State.CREATED) { + return false; + } + + // Terminal states cannot transition + if ( + from == RailTypes.State.SETTLED || + from == RailTypes.State.REJECTED || + from == RailTypes.State.CANCELLED || + from == RailTypes.State.RECALLED + ) { + return false; + } + + // Valid transitions + if (from == RailTypes.State.CREATED) { + return to == RailTypes.State.VALIDATED || to == RailTypes.State.REJECTED || to == RailTypes.State.CANCELLED; + } + + if (from == RailTypes.State.VALIDATED) { + return ( + to == RailTypes.State.SUBMITTED_TO_RAIL || + to == RailTypes.State.REJECTED || + to == RailTypes.State.CANCELLED + ); + } + + if (from == RailTypes.State.SUBMITTED_TO_RAIL) { + return ( + to == RailTypes.State.PENDING || + to == RailTypes.State.REJECTED || + to == RailTypes.State.CANCELLED || + to == RailTypes.State.RECALLED + ); + } + + if (from == RailTypes.State.PENDING) { + return ( + to == RailTypes.State.SETTLED || + to == RailTypes.State.REJECTED || + to == RailTypes.State.CANCELLED || + to == RailTypes.State.RECALLED + ); + } + + return false; + } +} + diff --git a/contracts/emoney/SettlementOrchestrator.sol b/contracts/emoney/SettlementOrchestrator.sol new file mode 100644 index 0000000..a6d5d45 --- /dev/null +++ b/contracts/emoney/SettlementOrchestrator.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./interfaces/ISettlementOrchestrator.sol"; +import "./interfaces/IRailTriggerRegistry.sol"; +import "./interfaces/IRailEscrowVault.sol"; +import "./interfaces/IAccountWalletRegistry.sol"; +import "./interfaces/IPolicyManager.sol"; +import "./interfaces/IDebtRegistry.sol"; +import "./interfaces/IComplianceRegistry.sol"; +import "./interfaces/IeMoneyToken.sol"; +import "./libraries/RailTypes.sol"; +import "./libraries/ISO20022Types.sol"; +import "./libraries/ReasonCodes.sol"; + +/** + * @title SettlementOrchestrator + * @notice Coordinates trigger lifecycle and fund locking/release + * @dev Supports both vault and lien escrow modes. Integrates with PolicyManager, DebtRegistry, ComplianceRegistry. + */ +contract SettlementOrchestrator is ISettlementOrchestrator, AccessControl { + bytes32 public constant SETTLEMENT_OPERATOR_ROLE = keccak256("SETTLEMENT_OPERATOR_ROLE"); + bytes32 public constant RAIL_ADAPTER_ROLE = keccak256("RAIL_ADAPTER_ROLE"); + + IRailTriggerRegistry public immutable triggerRegistry; + IRailEscrowVault public immutable escrowVault; + IAccountWalletRegistry public immutable accountWalletRegistry; + IPolicyManager public immutable policyManager; + IDebtRegistry public immutable debtRegistry; + IComplianceRegistry public immutable complianceRegistry; + + // triggerId => escrow mode (1 = vault, 2 = lien) + mapping(uint256 => uint8) private _escrowModes; + // triggerId => rail transaction reference + mapping(uint256 => bytes32) private _railTxRefs; + // triggerId => lien ID (if using lien mode) + mapping(uint256 => uint256) private _triggerLiens; + // triggerId => locked account address (for lien mode) + mapping(uint256 => address) private _lockedAccounts; + + // Rail-specific escrow mode configuration (default: vault) + mapping(RailTypes.Rail => uint8) private _railEscrowModes; + + /** + * @notice Initializes the orchestrator with registry addresses + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + * @param triggerRegistry_ Address of RailTriggerRegistry + * @param escrowVault_ Address of RailEscrowVault + * @param accountWalletRegistry_ Address of AccountWalletRegistry + * @param policyManager_ Address of PolicyManager + * @param debtRegistry_ Address of DebtRegistry + * @param complianceRegistry_ Address of ComplianceRegistry + */ + constructor( + address admin, + address triggerRegistry_, + address escrowVault_, + address accountWalletRegistry_, + address policyManager_, + address debtRegistry_, + address complianceRegistry_ + ) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + require(triggerRegistry_ != address(0), "SettlementOrchestrator: zero triggerRegistry"); + require(escrowVault_ != address(0), "SettlementOrchestrator: zero escrowVault"); + require(accountWalletRegistry_ != address(0), "SettlementOrchestrator: zero accountWalletRegistry"); + require(policyManager_ != address(0), "SettlementOrchestrator: zero policyManager"); + require(debtRegistry_ != address(0), "SettlementOrchestrator: zero debtRegistry"); + require(complianceRegistry_ != address(0), "SettlementOrchestrator: zero complianceRegistry"); + + triggerRegistry = IRailTriggerRegistry(triggerRegistry_); + escrowVault = IRailEscrowVault(escrowVault_); + accountWalletRegistry = IAccountWalletRegistry(accountWalletRegistry_); + policyManager = IPolicyManager(policyManager_); + debtRegistry = IDebtRegistry(debtRegistry_); + complianceRegistry = IComplianceRegistry(complianceRegistry_); + + // Set default escrow modes (can be changed by admin) + _railEscrowModes[RailTypes.Rail.FEDWIRE] = RailTypes.ESCROW_MODE_VAULT; + _railEscrowModes[RailTypes.Rail.SWIFT] = RailTypes.ESCROW_MODE_VAULT; + _railEscrowModes[RailTypes.Rail.SEPA] = RailTypes.ESCROW_MODE_VAULT; + _railEscrowModes[RailTypes.Rail.RTGS] = RailTypes.ESCROW_MODE_VAULT; + } + + /** + * @notice Sets the escrow mode for a rail + * @dev Requires DEFAULT_ADMIN_ROLE + * @param rail The rail type + * @param mode The escrow mode (1 = vault, 2 = lien) + */ + function setRailEscrowMode(RailTypes.Rail rail, uint8 mode) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(mode == RailTypes.ESCROW_MODE_VAULT || mode == RailTypes.ESCROW_MODE_LIEN, "SettlementOrchestrator: invalid mode"); + _railEscrowModes[rail] = mode; + } + + /** + * @notice Validates a trigger and locks funds + * @dev Requires SETTLEMENT_OPERATOR_ROLE. Checks compliance, policy, and locks funds via vault or lien. + * @param triggerId The trigger ID + */ + function validateAndLock(uint256 triggerId) external override onlyRole(SETTLEMENT_OPERATOR_ROLE) { + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + require(trigger.state == RailTypes.State.CREATED, "SettlementOrchestrator: invalid state"); + + // Resolve wallet address from walletRefId if needed (simplified - in production, use AccountWalletRegistry) + address accountAddress = _resolveAccountAddress(trigger.accountRefId); + require(accountAddress != address(0), "SettlementOrchestrator: cannot resolve account"); + + // Check compliance + require(complianceRegistry.isAllowed(accountAddress), "SettlementOrchestrator: account not compliant"); + require(!complianceRegistry.isFrozen(accountAddress), "SettlementOrchestrator: account frozen"); + + // Check policy + (bool allowed, ) = policyManager.canTransfer(trigger.token, accountAddress, address(0), trigger.amount); + require(allowed, "SettlementOrchestrator: transfer blocked by policy"); + + // Determine escrow mode for this rail + uint8 escrowMode = _railEscrowModes[trigger.rail]; + _escrowModes[triggerId] = escrowMode; + + if (escrowMode == RailTypes.ESCROW_MODE_VAULT) { + // Lock funds in vault + escrowVault.lock(trigger.token, accountAddress, trigger.amount, triggerId, trigger.rail); + } else if (escrowMode == RailTypes.ESCROW_MODE_LIEN) { + // Place a temporary lien + uint256 lienId = debtRegistry.placeLien( + accountAddress, + trigger.amount, + 0, // no expiry + 100, // priority + ReasonCodes.LIEN_BLOCK + ); + _triggerLiens[triggerId] = lienId; + _lockedAccounts[triggerId] = accountAddress; + } + + // Update trigger state to VALIDATED + triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK); + + emit Validated(triggerId, trigger.accountRefId, trigger.token, trigger.amount); + } + + /** + * @notice Marks a trigger as submitted to the rail + * @dev Requires RAIL_ADAPTER_ROLE. Records the rail transaction reference. + * @param triggerId The trigger ID + * @param railTxRef The rail transaction reference + */ + function markSubmitted( + uint256 triggerId, + bytes32 railTxRef + ) external override onlyRole(RAIL_ADAPTER_ROLE) { + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + require( + trigger.state == RailTypes.State.VALIDATED, + "SettlementOrchestrator: invalid state" + ); + require(railTxRef != bytes32(0), "SettlementOrchestrator: zero railTxRef"); + + _railTxRefs[triggerId] = railTxRef; + + // Update trigger state + triggerRegistry.updateState(triggerId, RailTypes.State.SUBMITTED_TO_RAIL, ReasonCodes.OK); + triggerRegistry.updateState(triggerId, RailTypes.State.PENDING, ReasonCodes.OK); + + emit Submitted(triggerId, railTxRef); + } + + /** + * @notice Confirms a trigger as settled + * @dev Requires RAIL_ADAPTER_ROLE. Releases escrow for outbound, mints for inbound. + * @param triggerId The trigger ID + * @param railTxRef The rail transaction reference (for verification) + */ + function confirmSettled(uint256 triggerId, bytes32 railTxRef) external override onlyRole(RAIL_ADAPTER_ROLE) { + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + require( + trigger.state == RailTypes.State.PENDING || trigger.state == RailTypes.State.SUBMITTED_TO_RAIL, + "SettlementOrchestrator: invalid state" + ); + require(_railTxRefs[triggerId] == railTxRef, "SettlementOrchestrator: railTxRef mismatch"); + + // Determine if this is inbound or outbound based on message type + bool isInbound = _isInboundMessage(trigger.msgType); + + if (isInbound) { + // Inbound: mint tokens to the account + address recipient = _resolveAccountAddress(trigger.accountRefId); + require(recipient != address(0), "SettlementOrchestrator: cannot resolve recipient"); + require(complianceRegistry.isAllowed(recipient), "SettlementOrchestrator: recipient not compliant"); + require(!complianceRegistry.isFrozen(recipient), "SettlementOrchestrator: recipient frozen"); + + IeMoneyToken(trigger.token).mint(recipient, trigger.amount, ReasonCodes.OK); + } else { + // Outbound: tokens have been sent via rail, so we need to burn them + uint8 escrowMode = _escrowModes[triggerId]; + address accountAddress = _lockedAccounts[triggerId] != address(0) + ? _lockedAccounts[triggerId] + : _resolveAccountAddress(trigger.accountRefId); + + if (escrowMode == RailTypes.ESCROW_MODE_VAULT) { + // Transfer tokens from vault to this contract, then burn + escrowVault.release(trigger.token, address(this), trigger.amount, triggerId); + IeMoneyToken(trigger.token).burn(address(this), trigger.amount, ReasonCodes.OK); + } else if (escrowMode == RailTypes.ESCROW_MODE_LIEN) { + // For lien mode, tokens are still in the account, so we burn them directly + require(accountAddress != address(0), "SettlementOrchestrator: cannot resolve account"); + IeMoneyToken(trigger.token).burn(accountAddress, trigger.amount, ReasonCodes.OK); + // Release lien + uint256 lienId = _triggerLiens[triggerId]; + require(lienId > 0, "SettlementOrchestrator: no lien found"); + debtRegistry.releaseLien(lienId); + } + } + + // Update trigger state + triggerRegistry.updateState(triggerId, RailTypes.State.SETTLED, ReasonCodes.OK); + + emit Settled(triggerId, railTxRef, trigger.accountRefId, trigger.token, trigger.amount); + } + + /** + * @notice Confirms a trigger as rejected + * @dev Requires RAIL_ADAPTER_ROLE. Releases escrow/lien. + * @param triggerId The trigger ID + * @param reason The rejection reason + */ + function confirmRejected(uint256 triggerId, bytes32 reason) external override onlyRole(RAIL_ADAPTER_ROLE) { + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + require( + trigger.state == RailTypes.State.PENDING || + trigger.state == RailTypes.State.SUBMITTED_TO_RAIL || + trigger.state == RailTypes.State.VALIDATED, + "SettlementOrchestrator: invalid state" + ); + + // Release escrow/lien + _releaseEscrow(triggerId, trigger); + + // Update trigger state + triggerRegistry.updateState(triggerId, RailTypes.State.REJECTED, reason); + + emit Rejected(triggerId, reason); + } + + /** + * @notice Confirms a trigger as cancelled + * @dev Requires RAIL_ADAPTER_ROLE. Releases escrow/lien. + * @param triggerId The trigger ID + * @param reason The cancellation reason + */ + function confirmCancelled(uint256 triggerId, bytes32 reason) external override onlyRole(RAIL_ADAPTER_ROLE) { + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + require( + trigger.state == RailTypes.State.CREATED || + trigger.state == RailTypes.State.VALIDATED || + trigger.state == RailTypes.State.SUBMITTED_TO_RAIL, + "SettlementOrchestrator: invalid state" + ); + + // Release escrow/lien if locked + if (trigger.state != RailTypes.State.CREATED) { + _releaseEscrow(triggerId, trigger); + } + + // Update trigger state + triggerRegistry.updateState(triggerId, RailTypes.State.CANCELLED, reason); + + emit Cancelled(triggerId, reason); + } + + /** + * @notice Confirms a trigger as recalled + * @dev Requires RAIL_ADAPTER_ROLE. Releases escrow/lien. + * @param triggerId The trigger ID + * @param reason The recall reason + */ + function confirmRecalled(uint256 triggerId, bytes32 reason) external override onlyRole(RAIL_ADAPTER_ROLE) { + IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); + require( + trigger.state == RailTypes.State.PENDING || trigger.state == RailTypes.State.SUBMITTED_TO_RAIL, + "SettlementOrchestrator: invalid state" + ); + + // Release escrow/lien + _releaseEscrow(triggerId, trigger); + + // Update trigger state + triggerRegistry.updateState(triggerId, RailTypes.State.RECALLED, reason); + + emit Recalled(triggerId, reason); + } + + /** + * @notice Returns the escrow mode for a trigger + * @param triggerId The trigger ID + * @return The escrow mode (1 = vault, 2 = lien) + */ + function getEscrowMode(uint256 triggerId) external view override returns (uint8) { + return _escrowModes[triggerId]; + } + + /** + * @notice Returns the rail transaction reference for a trigger + * @param triggerId The trigger ID + * @return The rail transaction reference + */ + function getRailTxRef(uint256 triggerId) external view override returns (bytes32) { + return _railTxRefs[triggerId]; + } + + /** + * @notice Releases escrow for a trigger (internal helper) + * @param triggerId The trigger ID + * @param trigger The trigger struct + */ + function _releaseEscrow(uint256 triggerId, IRailTriggerRegistry.Trigger memory trigger) internal { + uint8 escrowMode = _escrowModes[triggerId]; + address accountAddress = _lockedAccounts[triggerId] != address(0) + ? _lockedAccounts[triggerId] + : _resolveAccountAddress(trigger.accountRefId); + + if (escrowMode == RailTypes.ESCROW_MODE_VAULT) { + // Release from vault back to account + escrowVault.release(trigger.token, accountAddress, trigger.amount, triggerId); + } else if (escrowMode == RailTypes.ESCROW_MODE_LIEN) { + // Release lien + uint256 lienId = _triggerLiens[triggerId]; + if (lienId > 0) { + debtRegistry.releaseLien(lienId); + } + } + } + + /** + * @notice Resolves account address from accountRefId + * @dev Uses AccountWalletRegistry to find the first active wallet for an account + * @param accountRefId The account reference ID + * @return The account address (or zero if not resolvable) + */ + function _resolveAccountAddress(bytes32 accountRefId) internal view returns (address) { + // Get wallets linked to this account + IAccountWalletRegistry.WalletLink[] memory wallets = accountWalletRegistry.getWallets(accountRefId); + + // Find first active wallet and extract address (simplified - in production, you'd need to decode walletRefId) + // For now, we'll need the walletRefId to be set in the trigger or passed separately + // This is a limitation that should be addressed in production + return address(0); + } + + /** + * @notice Checks if a message type is inbound + * @param msgType The message type + * @return true if inbound + */ + function _isInboundMessage(bytes32 msgType) internal pure returns (bool) { + return ISO20022Types.isInboundNotification(msgType); + } +} + diff --git a/contracts/emoney/TokenFactory138.sol b/contracts/emoney/TokenFactory138.sol new file mode 100644 index 0000000..210a4d2 --- /dev/null +++ b/contracts/emoney/TokenFactory138.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import "./interfaces/ITokenFactory138.sol"; +import "./interfaces/IeMoneyToken.sol"; +import "./interfaces/IPolicyManager.sol"; +import "./eMoneyToken.sol"; +import "./errors/FactoryErrors.sol"; +import "./errors/RegistryErrors.sol"; + +/** + * @title TokenFactory138 + * @notice Factory for deploying new eMoneyToken instances as UUPS upgradeable proxies + * @dev Deploys ERC1967Proxy instances pointing to a shared implementation contract. + * Each token is configured with its issuer, lien mode, bridge settings, and registered by code hash. + */ +contract TokenFactory138 is ITokenFactory138, AccessControl { + bytes32 public constant TOKEN_DEPLOYER_ROLE = keccak256("TOKEN_DEPLOYER_ROLE"); + + address public immutable implementation; + address public immutable policyManager; + address public immutable debtRegistry; + address public immutable complianceRegistry; + + mapping(bytes32 => address) private _tokensByCodeHash; + + /** + * @notice Initializes the factory with registry and implementation addresses + * @param admin Address that will receive DEFAULT_ADMIN_ROLE + * @param implementation_ Address of the eMoneyToken implementation contract (used for all proxies) + * @param policyManager_ Address of PolicyManager contract + * @param debtRegistry_ Address of DebtRegistry contract + * @param complianceRegistry_ Address of ComplianceRegistry contract + */ + constructor( + address admin, + address implementation_, + address policyManager_, + address debtRegistry_, + address complianceRegistry_ + ) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + implementation = implementation_; + policyManager = policyManager_; + debtRegistry = debtRegistry_; + complianceRegistry = complianceRegistry_; + } + + /** + * @notice Deploys a new eMoneyToken instance as a UUPS proxy + * @dev Requires TOKEN_DEPLOYER_ROLE. Creates ERC1967Proxy, initializes token, and configures PolicyManager. + * @param name Token name (e.g., "USD eMoney") + * @param symbol Token symbol (e.g., "USDe") + * @param config Token configuration (decimals, issuer, lien mode, bridge settings) + * @return token Address of the deployed proxy token contract + */ + function deployToken( + string calldata name, + string calldata symbol, + TokenConfig calldata config + ) external override onlyRole(TOKEN_DEPLOYER_ROLE) returns (address token) { + if (config.issuer == address(0)) revert ZeroIssuer(); + if (config.defaultLienMode != 1 && config.defaultLienMode != 2) { + revert PolicyInvalidLienMode(config.defaultLienMode); + } + + // Deploy UUPS proxy + bytes memory initData = abi.encodeWithSelector( + IeMoneyToken.initialize.selector, + name, + symbol, + config.decimals, + config.issuer, + policyManager, + debtRegistry, + complianceRegistry + ); + + ERC1967Proxy proxy = new ERC1967Proxy(implementation, initData); + token = address(proxy); + + // Configure token in PolicyManager + IPolicyManager(policyManager).setLienMode(token, config.defaultLienMode); + IPolicyManager(policyManager).setBridgeOnly(token, config.bridgeOnly); + if (config.bridge != address(0)) { + IPolicyManager(policyManager).setBridge(token, config.bridge); + } + + // Register token by code hash (deterministic based on deployment params) + // Include token address, timestamp, and block number to prevent collisions + bytes32 codeHash = keccak256(abi.encodePacked(name, symbol, config.issuer, token, block.timestamp, block.number)); + _tokensByCodeHash[codeHash] = token; + + emit TokenDeployed( + token, + codeHash, + name, + symbol, + config.decimals, + config.issuer, + config.defaultLienMode, + config.bridgeOnly, + config.bridge + ); + } + + /** + * @notice Returns the token address for a given code hash + * @dev Code hash is generated deterministically during token deployment + * @param codeHash The code hash to lookup + * @return Token address (zero address if not found) + */ + function tokenByCodeHash(bytes32 codeHash) external view override returns (address) { + return _tokensByCodeHash[codeHash]; + } +} + diff --git a/contracts/emoney/eMoneyToken.sol b/contracts/emoney/eMoneyToken.sol new file mode 100644 index 0000000..4150110 --- /dev/null +++ b/contracts/emoney/eMoneyToken.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./interfaces/IeMoneyToken.sol"; +import "./interfaces/IPolicyManager.sol"; +import "./interfaces/IDebtRegistry.sol"; +import "./interfaces/IComplianceRegistry.sol"; +import "./errors/TokenErrors.sol"; +import "./libraries/ReasonCodes.sol"; + +/** + * @title eMoneyToken + * @notice Restricted ERC-20 token with policy-controlled transfers and lien enforcement + * @dev Implements UUPS upgradeable proxy pattern. All transfers are validated through PolicyManager. + * Supports two lien enforcement modes: hard freeze (blocks all transfers with liens) and encumbered + * (allows transfers up to freeBalance = balance - activeLienAmount). + */ +contract eMoneyToken is + Initializable, + ERC20Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + IeMoneyToken +{ + bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE"); + bytes32 public constant ENFORCEMENT_ROLE = keccak256("ENFORCEMENT_ROLE"); + + IPolicyManager public policyManager; + IDebtRegistry public debtRegistry; + IComplianceRegistry public complianceRegistry; + uint8 private _decimals; + bool private _inForceTransfer; + bool private _inClawback; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes the token with configuration + * @dev Called once during proxy deployment. Can only be called once. + * @param name Token name (e.g., "eMoney Token") + * @param symbol Token symbol (e.g., "EMT") + * @param decimals_ Number of decimals (typically 18) + * @param issuer Address that will receive ISSUER_ROLE and DEFAULT_ADMIN_ROLE + * @param policyManager_ Address of PolicyManager contract + * @param debtRegistry_ Address of DebtRegistry contract + * @param complianceRegistry_ Address of ComplianceRegistry contract + */ + function initialize( + string calldata name, + string calldata symbol, + uint8 decimals_, + address issuer, + address policyManager_, + address debtRegistry_, + address complianceRegistry_ + ) external initializer { + __ERC20_init(name, symbol); + __AccessControl_init(); + __UUPSUpgradeable_init(); + __ReentrancyGuard_init(); + + _decimals = decimals_; + policyManager = IPolicyManager(policyManager_); + debtRegistry = IDebtRegistry(debtRegistry_); + complianceRegistry = IComplianceRegistry(complianceRegistry_); + + _grantRole(DEFAULT_ADMIN_ROLE, issuer); + _grantRole(ISSUER_ROLE, issuer); + } + + /** + * @notice Returns the number of decimals for the token + * @return Number of decimals (typically 18) + */ + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + /** + * @notice Returns the free balance available for transfer (balance minus active encumbrances) + * @dev In encumbered mode, transfers are limited to freeBalance + * @param account Address to check + * @return Free balance (balanceOf - activeLienAmount, floored at 0) + */ + function freeBalanceOf(address account) external view override returns (uint256) { + uint256 balance = balanceOf(account); + uint256 encumbrance = debtRegistry.activeLienAmount(account); + return balance > encumbrance ? balance - encumbrance : 0; + } + + /** + * @notice Internal hook that enforces transfer restrictions before updating balances + * @dev Overrides ERC20Upgradeable._update to add policy checks and lien enforcement. + * Skips checks for mint/burn operations (from/to == address(0)) and privileged operations + * (clawback, forceTransfer). + * @param from Sender address (address(0) for mints) + * @param to Recipient address (address(0) for burns) + * @param amount Transfer amount + */ + function _update( + address from, + address to, + uint256 amount + ) internal virtual override { + // Skip checks for privileged operations (mint/burn internal transfers) + if (from == address(0) || to == address(0)) { + super._update(from, to, amount); + return; + } + + // Skip all checks during clawback (bypasses everything) + if (_inClawback) { + super._update(from, to, amount); + return; + } + + // Skip lien checks during forceTransfer (compliance already checked there) + if (_inForceTransfer) { + super._update(from, to, amount); + return; + } + + // Check policy manager + (bool allowed, bytes32 reasonCode) = policyManager.canTransfer(address(this), from, to, amount); + if (!allowed) { + revert TransferBlocked(reasonCode, from, to, amount); + } + + // Check lien mode enforcement + uint8 mode = policyManager.lienMode(address(this)); + if (mode == 1) { + // Hard freeze mode: any active lien blocks all transfers + if (debtRegistry.hasActiveLien(from)) { + revert TransferBlocked(ReasonCodes.LIEN_BLOCK, from, to, amount); + } + } else if (mode == 2) { + // Encumbered mode: allow transfers up to freeBalance + uint256 encumbrance = debtRegistry.activeLienAmount(from); + uint256 balance = balanceOf(from); + uint256 freeBalance = balance > encumbrance ? balance - encumbrance : 0; + if (amount > freeBalance) { + revert TransferBlocked(ReasonCodes.INSUFF_FREE_BAL, from, to, amount); + } + } + // mode == 0: no lien enforcement + + super._update(from, to, amount); + } + + /** + * @notice Mints new tokens to an account + * @dev Requires ISSUER_ROLE. Bypasses all transfer restrictions (mint operation). + * @param to Recipient address + * @param amount Amount to mint + * @param reasonCode Reason code for the mint operation (e.g., ReasonCodes.OK) + */ + function mint(address to, uint256 amount, bytes32 reasonCode) external override onlyRole(ISSUER_ROLE) nonReentrant { + _mint(to, amount); + emit Minted(to, amount, reasonCode); + } + + /** + * @notice Burns tokens from an account + * @dev Requires ISSUER_ROLE. Bypasses all transfer restrictions (burn operation). + * @param from Account to burn from + * @param amount Amount to burn + * @param reasonCode Reason code for the burn operation (e.g., ReasonCodes.OK) + */ + function burn(address from, uint256 amount, bytes32 reasonCode) external override onlyRole(ISSUER_ROLE) nonReentrant { + _burn(from, amount); + emit Burned(from, amount, reasonCode); + } + + /** + * @notice Clawback transfers tokens, bypassing all restrictions + * @dev Requires ENFORCEMENT_ROLE. Bypasses all checks including liens, compliance, and policy. + * Used for emergency recovery or enforcement actions. + * @param from Source address + * @param to Destination address + * @param amount Amount to transfer + * @param reasonCode Reason code for the clawback operation + */ + function clawback( + address from, + address to, + uint256 amount, + bytes32 reasonCode + ) external override onlyRole(ENFORCEMENT_ROLE) nonReentrant { + // Clawback bypasses all checks including liens and compliance + _inClawback = true; + _transfer(from, to, amount); + _inClawback = false; + emit Clawback(from, to, amount, reasonCode); + } + + /** + * @notice Force transfer bypasses liens but enforces compliance + * @dev Requires ENFORCEMENT_ROLE. Bypasses lien enforcement but still checks compliance. + * Used when liens need to be bypassed but compliance must still be enforced. + * @param from Source address + * @param to Destination address + * @param amount Amount to transfer + * @param reasonCode Reason code for the force transfer operation + */ + function forceTransfer( + address from, + address to, + uint256 amount, + bytes32 reasonCode + ) external override onlyRole(ENFORCEMENT_ROLE) nonReentrant { + // ForceTransfer bypasses liens but still enforces compliance + // Check compliance + if (!complianceRegistry.isAllowed(from)) { + revert FromNotCompliant(from); + } + if (!complianceRegistry.isAllowed(to)) { + revert ToNotCompliant(to); + } + if (complianceRegistry.isFrozen(from)) { + revert FromFrozen(from); + } + if (complianceRegistry.isFrozen(to)) { + revert ToFrozen(to); + } + + // Set flag to bypass lien checks in _update + _inForceTransfer = true; + _transfer(from, to, amount); + _inForceTransfer = false; + + emit ForcedTransfer(from, to, amount, reasonCode); + } + + /** + * @notice Authorizes an upgrade to a new implementation + * @dev Internal function required by UUPSUpgradeable. Only DEFAULT_ADMIN_ROLE can authorize upgrades. + * @param newImplementation Address of the new implementation contract + */ + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} + diff --git a/contracts/emoney/errors/BridgeErrors.sol b/contracts/emoney/errors/BridgeErrors.sol new file mode 100644 index 0000000..f5eae69 --- /dev/null +++ b/contracts/emoney/errors/BridgeErrors.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +error BridgeZeroToken(); +error BridgeZeroAmount(); +error BridgeZeroRecipient(); +error BridgeTransferBlocked(address token, address from, address to, uint256 amount); +error BridgeLightClientNotSet(); +error BridgeProofVerificationFailed(bytes32 sourceChain, bytes32 sourceTx); +error BridgeRecipientNotCompliant(address recipient); +error BridgeRecipientFrozen(address recipient); + diff --git a/contracts/emoney/errors/FactoryErrors.sol b/contracts/emoney/errors/FactoryErrors.sol new file mode 100644 index 0000000..b3075fe --- /dev/null +++ b/contracts/emoney/errors/FactoryErrors.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +error ZeroIssuer(); + diff --git a/contracts/emoney/errors/RegistryErrors.sol b/contracts/emoney/errors/RegistryErrors.sol new file mode 100644 index 0000000..7f6f6d0 --- /dev/null +++ b/contracts/emoney/errors/RegistryErrors.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// ComplianceRegistry errors +error ComplianceZeroAccount(); +error ComplianceAccountNotCompliant(address account); +error ComplianceAccountFrozen(address account); + +// DebtRegistry errors +error DebtZeroDebtor(); +error DebtZeroAmount(); +error DebtLienNotActive(uint256 lienId); +error DebtReduceByExceedsAmount(uint256 lienId, uint256 reduceBy, uint256 currentAmount); + +// PolicyManager errors +error PolicyInvalidLienMode(uint8 mode); + diff --git a/contracts/emoney/errors/TokenErrors.sol b/contracts/emoney/errors/TokenErrors.sol new file mode 100644 index 0000000..fc5e5ab --- /dev/null +++ b/contracts/emoney/errors/TokenErrors.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +error TransferBlocked(bytes32 reason, address from, address to, uint256 amount); +error FromNotCompliant(address account); +error ToNotCompliant(address account); +error FromFrozen(address account); +error ToFrozen(address account); + diff --git a/contracts/emoney/interfaces/IAccountWalletRegistry.sol b/contracts/emoney/interfaces/IAccountWalletRegistry.sol index 37a7d7a..10636b2 100644 --- a/contracts/emoney/interfaces/IAccountWalletRegistry.sol +++ b/contracts/emoney/interfaces/IAccountWalletRegistry.sol @@ -1,23 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.19; -/// @notice Minimal account-wallet registry interface for smart-account compile stubs. interface IAccountWalletRegistry { struct WalletLink { bytes32 walletRefId; uint64 linkedAt; bool active; - bytes32 provider; + bytes32 provider; // e.g., "METAMASK", "FIREBLOCKS", "CUSTODY_X" } - event AccountWalletLinked( - bytes32 indexed accountRefId, - bytes32 indexed walletRefId, - bytes32 provider, - uint64 linkedAt - ); - event AccountWalletUnlinked(bytes32 indexed accountRefId, bytes32 indexed walletRefId); - function linkAccountToWallet(bytes32 accountRefId, bytes32 walletRefId, bytes32 provider) external; function unlinkAccountFromWallet(bytes32 accountRefId, bytes32 walletRefId) external; @@ -29,4 +20,14 @@ interface IAccountWalletRegistry { function isLinked(bytes32 accountRefId, bytes32 walletRefId) external view returns (bool); function isActive(bytes32 accountRefId, bytes32 walletRefId) external view returns (bool); + + event AccountWalletLinked( + bytes32 indexed accountRefId, + bytes32 indexed walletRefId, + bytes32 provider, + uint64 linkedAt + ); + + event AccountWalletUnlinked(bytes32 indexed accountRefId, bytes32 indexed walletRefId); } + diff --git a/contracts/emoney/interfaces/IBridgeVault138.sol b/contracts/emoney/interfaces/IBridgeVault138.sol new file mode 100644 index 0000000..9536780 --- /dev/null +++ b/contracts/emoney/interfaces/IBridgeVault138.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IBridgeVault138 { + function lock( + address token, + uint256 amount, + bytes32 targetChain, + address targetRecipient + ) external; + + function unlock( + address token, + address to, + uint256 amount, + bytes32 sourceChain, + bytes32 sourceTx, + bytes calldata proof + ) external; + + event Locked( + address indexed token, + address indexed from, + uint256 amount, + bytes32 targetChain, + address targetRecipient + ); + + event Unlocked( + address indexed token, + address indexed to, + uint256 amount, + bytes32 sourceChain, + bytes32 sourceTx + ); +} + diff --git a/contracts/emoney/interfaces/IComplianceRegistry.sol b/contracts/emoney/interfaces/IComplianceRegistry.sol new file mode 100644 index 0000000..abeb7da --- /dev/null +++ b/contracts/emoney/interfaces/IComplianceRegistry.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IComplianceRegistry { + function isAllowed(address account) external view returns (bool); + + function isFrozen(address account) external view returns (bool); + + function riskTier(address account) external view returns (uint8); + + function jurisdictionHash(address account) external view returns (bytes32); + + function setCompliance( + address account, + bool allowed, + uint8 tier, + bytes32 jurHash + ) external; + + function setFrozen(address account, bool frozen) external; + + event ComplianceUpdated( + address indexed account, + bool allowed, + uint8 tier, + bytes32 jurisdictionHash + ); + + event FrozenUpdated(address indexed account, bool frozen); +} + diff --git a/contracts/emoney/interfaces/IDebtRegistry.sol b/contracts/emoney/interfaces/IDebtRegistry.sol new file mode 100644 index 0000000..0790b7e --- /dev/null +++ b/contracts/emoney/interfaces/IDebtRegistry.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IDebtRegistry { + struct Lien { + address debtor; + uint256 amount; + uint64 expiry; // 0 = no expiry + uint8 priority; + address authority; + bytes32 reasonCode; + bool active; + } + + function activeLienAmount(address debtor) external view returns (uint256); + + function hasActiveLien(address debtor) external view returns (bool); + + function activeLienCount(address debtor) external view returns (uint256); + + function getLien(uint256 lienId) external view returns (Lien memory); + + function placeLien( + address debtor, + uint256 amount, + uint64 expiry, + uint8 priority, + bytes32 reasonCode + ) external returns (uint256 lienId); + + function reduceLien(uint256 lienId, uint256 reduceBy) external; + + function releaseLien(uint256 lienId) external; + + event LienPlaced( + uint256 indexed lienId, + address indexed debtor, + uint256 amount, + uint64 expiry, + uint8 priority, + address indexed authority, + bytes32 reasonCode + ); + + event LienReduced(uint256 indexed lienId, uint256 reduceBy, uint256 newAmount); + + event LienReleased(uint256 indexed lienId); +} + diff --git a/contracts/emoney/interfaces/IISO20022Router.sol b/contracts/emoney/interfaces/IISO20022Router.sol new file mode 100644 index 0000000..1ab869a --- /dev/null +++ b/contracts/emoney/interfaces/IISO20022Router.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IISO20022Router { + struct CanonicalMessage { + bytes32 msgType; // pacs.008, pain.001, camt.054, etc. + bytes32 instructionId; // unique reference + bytes32 endToEndId; // optional + bytes32 accountRefId; + bytes32 counterpartyRefId; + address token; + uint256 amount; + bytes32 currencyCode; + bytes32 payloadHash; // hash of off-chain payload + } + + function submitInbound(CanonicalMessage calldata m) external returns (uint256 triggerId); + + function submitOutbound(CanonicalMessage calldata m) external returns (uint256 triggerId); + + function getTriggerIdByInstructionId(bytes32 instructionId) external view returns (uint256); + + event InboundSubmitted(uint256 indexed triggerId, bytes32 msgType, bytes32 instructionId, bytes32 accountRefId); + + event OutboundSubmitted(uint256 indexed triggerId, bytes32 msgType, bytes32 instructionId, bytes32 accountRefId); +} + diff --git a/contracts/emoney/interfaces/IPacketRegistry.sol b/contracts/emoney/interfaces/IPacketRegistry.sol new file mode 100644 index 0000000..5d1d603 --- /dev/null +++ b/contracts/emoney/interfaces/IPacketRegistry.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @title IPacketRegistry + * @notice Interface for recording packet lifecycle events for non-scheme participants + * @dev Tracks packet generation, dispatch, and acknowledgment linked to ChainID 138 triggers + */ +interface IPacketRegistry { + /** + * @notice Records that a packet has been generated + * @param triggerId The trigger ID from RailTriggerRegistry + * @param payloadHash SHA-256 hash of the packet payload + * @param mode Transmission mode (e.g., "PDF", "EMAIL", "AS4") + */ + function recordGenerated(uint256 triggerId, bytes32 payloadHash, bytes32 mode) external; + + /** + * @notice Records that a packet has been dispatched via a channel + * @param triggerId The trigger ID from RailTriggerRegistry + * @param channel The dispatch channel (e.g., "EMAIL", "AS4", "PORTAL") + * @param messageRef The message reference ID from the transport layer + */ + function recordDispatched(uint256 triggerId, bytes32 channel, bytes32 messageRef) external; + + /** + * @notice Records that a packet has been acknowledged by the recipient + * @param triggerId The trigger ID from RailTriggerRegistry + * @param receiptRef The receipt reference ID from the recipient + * @param status The acknowledgment status (e.g., "RECEIVED", "ACCEPTED", "REJECTED") + */ + function recordAcknowledged(uint256 triggerId, bytes32 receiptRef, bytes32 status) external; + + /** + * @notice Event emitted when a packet is generated + * @param triggerId The trigger ID + * @param payloadHash The payload hash + * @param mode The transmission mode + */ + event PacketGenerated(uint256 indexed triggerId, bytes32 payloadHash, bytes32 mode); + + /** + * @notice Event emitted when a packet is dispatched + * @param triggerId The trigger ID + * @param channel The dispatch channel + * @param messageRef The message reference + */ + event PacketDispatched(uint256 indexed triggerId, bytes32 channel, bytes32 messageRef); + + /** + * @notice Event emitted when a packet is acknowledged + * @param triggerId The trigger ID + * @param receiptRef The receipt reference + * @param status The acknowledgment status + */ + event PacketAcknowledged(uint256 indexed triggerId, bytes32 receiptRef, bytes32 status); +} + diff --git a/contracts/emoney/interfaces/IPolicyManager.sol b/contracts/emoney/interfaces/IPolicyManager.sol new file mode 100644 index 0000000..94a7356 --- /dev/null +++ b/contracts/emoney/interfaces/IPolicyManager.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IPolicyManager { + // token config getters + function isPaused(address token) external view returns (bool); + + function bridgeOnly(address token) external view returns (bool); + + function bridge(address token) external view returns (address); + + function lienMode(address token) external view returns (uint8); // 0 off, 1 hard, 2 encumbered + + function isTokenFrozen(address token, address account) external view returns (bool); + + // decision + function canTransfer( + address token, + address from, + address to, + uint256 amount + ) external view returns (bool allowed, bytes32 reasonCode); + + // setters + function setPaused(address token, bool paused) external; + + function setBridgeOnly(address token, bool enabled) external; + + function setBridge(address token, address bridgeAddr) external; + + function setLienMode(address token, uint8 mode) external; + + function freeze(address token, address account, bool frozen) external; + + event TokenPaused(address indexed token, bool paused); + + event BridgeOnlySet(address indexed token, bool enabled); + + event BridgeSet(address indexed token, address bridge); + + event LienModeSet(address indexed token, uint8 mode); + + event TokenFreeze(address indexed token, address indexed account, bool frozen); + + event TokenConfigured(address indexed token, bool paused, bool bridgeOnly, address bridge, uint8 lienMode); +} + diff --git a/contracts/emoney/interfaces/IRailEscrowVault.sol b/contracts/emoney/interfaces/IRailEscrowVault.sol new file mode 100644 index 0000000..1ad4085 --- /dev/null +++ b/contracts/emoney/interfaces/IRailEscrowVault.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../libraries/RailTypes.sol"; + +interface IRailEscrowVault { + function lock( + address token, + address from, + uint256 amount, + uint256 triggerId, + RailTypes.Rail rail + ) external; + + function release(address token, address to, uint256 amount, uint256 triggerId) external; + + function getEscrowAmount(address token, uint256 triggerId) external view returns (uint256); + + function getTotalEscrow(address token) external view returns (uint256); + + event Locked( + address indexed token, + address indexed from, + uint256 amount, + uint256 indexed triggerId, + uint8 rail + ); + + event Released(address indexed token, address indexed to, uint256 amount, uint256 indexed triggerId); +} + diff --git a/contracts/emoney/interfaces/IRailTriggerRegistry.sol b/contracts/emoney/interfaces/IRailTriggerRegistry.sol new file mode 100644 index 0000000..fdd5f8c --- /dev/null +++ b/contracts/emoney/interfaces/IRailTriggerRegistry.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../libraries/RailTypes.sol"; + +interface IRailTriggerRegistry { + struct Trigger { + uint256 id; + RailTypes.Rail rail; + bytes32 msgType; // e.g., "pacs.008", "pain.001" + bytes32 accountRefId; // hashed account reference + bytes32 walletRefId; // hashed wallet reference (optional) + address token; // eMoney token + uint256 amount; + bytes32 currencyCode; // e.g., "USD", "EUR" + bytes32 instructionId; // end-to-end trace id + RailTypes.State state; + uint64 createdAt; + uint64 updatedAt; + } + + function createTrigger(Trigger calldata t) external returns (uint256 id); + + function updateState(uint256 id, RailTypes.State newState, bytes32 reason) external; + + function getTrigger(uint256 id) external view returns (Trigger memory); + + function getTriggerByInstructionId(bytes32 instructionId) external view returns (Trigger memory); + + function triggerExists(uint256 id) external view returns (bool); + + function instructionIdExists(bytes32 instructionId) external view returns (bool); + + event TriggerCreated( + uint256 indexed id, + uint8 rail, + bytes32 msgType, + bytes32 instructionId, + bytes32 accountRefId, + address token, + uint256 amount + ); + + event TriggerStateUpdated(uint256 indexed id, uint8 oldState, uint8 newState, bytes32 reason); +} + diff --git a/contracts/emoney/interfaces/ISettlementOrchestrator.sol b/contracts/emoney/interfaces/ISettlementOrchestrator.sol new file mode 100644 index 0000000..3557f7c --- /dev/null +++ b/contracts/emoney/interfaces/ISettlementOrchestrator.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface ISettlementOrchestrator { + function validateAndLock(uint256 triggerId) external; + + function markSubmitted(uint256 triggerId, bytes32 railTxRef) external; + + function confirmSettled(uint256 triggerId, bytes32 railTxRef) external; + + function confirmRejected(uint256 triggerId, bytes32 reason) external; + + function confirmCancelled(uint256 triggerId, bytes32 reason) external; + + function confirmRecalled(uint256 triggerId, bytes32 reason) external; + + function getEscrowMode(uint256 triggerId) external view returns (uint8); // 1 = vault, 2 = lien + + function getRailTxRef(uint256 triggerId) external view returns (bytes32); + + event Validated(uint256 indexed triggerId, bytes32 accountRefId, address token, uint256 amount); + + event Submitted(uint256 indexed triggerId, bytes32 railTxRef); + + event Settled(uint256 indexed triggerId, bytes32 railTxRef, bytes32 accountRefId, address token, uint256 amount); + + event Rejected(uint256 indexed triggerId, bytes32 reason); + + event Cancelled(uint256 indexed triggerId, bytes32 reason); + + event Recalled(uint256 indexed triggerId, bytes32 reason); +} + diff --git a/contracts/emoney/interfaces/ITokenFactory138.sol b/contracts/emoney/interfaces/ITokenFactory138.sol index 1229bd1..0ab011e 100644 --- a/contracts/emoney/interfaces/ITokenFactory138.sol +++ b/contracts/emoney/interfaces/ITokenFactory138.sol @@ -1,17 +1,33 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.19; -/// @notice Minimal TokenFactory138 interface for reserve integration compile stubs. interface ITokenFactory138 { struct TokenConfig { address issuer; - uint8 defaultLienMode; - bool paused; + uint8 decimals; + uint8 defaultLienMode; // 1 hard, 2 encumbered bool bridgeOnly; address bridge; } - function deployToken(string memory name, string memory symbol, TokenConfig memory config) - external - returns (address); + function deployToken( + string calldata name, + string calldata symbol, + TokenConfig calldata config + ) external returns (address token); + + function tokenByCodeHash(bytes32 codeHash) external view returns (address); + + event TokenDeployed( + address indexed token, + bytes32 indexed codeHash, + string name, + string symbol, + uint8 decimals, + address indexed issuer, + uint8 defaultLienMode, + bool bridgeOnly, + address bridge + ); } + diff --git a/contracts/emoney/interfaces/IeMoneyToken.sol b/contracts/emoney/interfaces/IeMoneyToken.sol index 8a58ef4..c668930 100644 --- a/contracts/emoney/interfaces/IeMoneyToken.sol +++ b/contracts/emoney/interfaces/IeMoneyToken.sol @@ -1,8 +1,56 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.19; -/// @notice Minimal eMoney token surface for vault adapters (full eMoney tree is optional). interface IeMoneyToken { - function mint(address to, uint256 amount, bytes32 reason) external; - function burn(address from, uint256 amount, bytes32 reason) external; + function initialize( + string calldata name, + string calldata symbol, + uint8 decimals, + address issuer, + address policyManager, + address debtRegistry, + address complianceRegistry + ) external; + + // view + function freeBalanceOf(address account) external view returns (uint256); + + // callable/recallable + function mint(address to, uint256 amount, bytes32 reasonCode) external; + + function burn(address from, uint256 amount, bytes32 reasonCode) external; + + function clawback( + address from, + address to, + uint256 amount, + bytes32 reasonCode + ) external; + + function forceTransfer( + address from, + address to, + uint256 amount, + bytes32 reasonCode + ) external; + + // events + event Minted(address indexed to, uint256 amount, bytes32 reasonCode); + + event Burned(address indexed from, uint256 amount, bytes32 reasonCode); + + event Clawback( + address indexed from, + address indexed to, + uint256 amount, + bytes32 reasonCode + ); + + event ForcedTransfer( + address indexed from, + address indexed to, + uint256 amount, + bytes32 reasonCode + ); } + diff --git a/contracts/emoney/libraries/AccountHashing.sol b/contracts/emoney/libraries/AccountHashing.sol new file mode 100644 index 0000000..487f178 --- /dev/null +++ b/contracts/emoney/libraries/AccountHashing.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @title AccountHashing + * @notice Utilities for hashing account identifiers with salts to protect PII + */ +library AccountHashing { + /** + * @notice Generates a hashed account reference ID + * @param rail The payment rail identifier (e.g., "FEDWIRE", "SEPA") + * @param countryCode The country code (e.g., "US", "DE") + * @param accountIdentifier The account identifier (IBAN, ABA, etc.) - should be hashed off-chain + * @param salt A unique salt for this account + * @return accountRefId The hashed account reference ID + */ + function hashAccountRef( + bytes32 rail, + bytes32 countryCode, + bytes32 accountIdentifier, + bytes32 salt + ) internal pure returns (bytes32 accountRefId) { + return keccak256(abi.encodePacked(rail, countryCode, accountIdentifier, salt)); + } + + /** + * @notice Generates a hashed wallet reference ID + * @param chainId The chain ID where the wallet exists + * @param walletAddress The wallet address + * @param providerId The provider identifier (e.g., "METAMASK", "FIREBLOCKS") + * @return walletRefId The hashed wallet reference ID + */ + function hashWalletRef( + uint256 chainId, + address walletAddress, + bytes32 providerId + ) internal pure returns (bytes32 walletRefId) { + return keccak256(abi.encodePacked(chainId, walletAddress, providerId)); + } + + /** + * @notice Generates an ICAN (Internal Canonical Account Number) reference ID + * @param namespace The internal namespace identifier + * @param accountId The internal account ID + * @return icanRefId The ICAN reference ID + */ + function hashICANRef(bytes32 namespace, bytes32 accountId) internal pure returns (bytes32 icanRefId) { + return keccak256(abi.encodePacked(namespace, accountId)); + } +} + diff --git a/contracts/emoney/libraries/ISO20022Types.sol b/contracts/emoney/libraries/ISO20022Types.sol new file mode 100644 index 0000000..493f586 --- /dev/null +++ b/contracts/emoney/libraries/ISO20022Types.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @title ISO20022Types + * @notice ISO-20022 message family constants and utilities + */ +library ISO20022Types { + // Message family prefixes + bytes32 public constant FAMILY_PACS = keccak256("pacs"); + bytes32 public constant FAMILY_PAIN = keccak256("pain"); + bytes32 public constant FAMILY_CAMT = keccak256("camt"); + bytes32 public constant FAMILY_FXTR = keccak256("fxtr"); + bytes32 public constant FAMILY_SECL = keccak256("secl"); + + // Specific message types (using RailTypes constants for consistency) + bytes32 public constant PAIN_001 = keccak256("pain.001"); // Customer Credit Transfer Initiation + bytes32 public constant PACS_002 = keccak256("pacs.002"); // Payment Status Report + bytes32 public constant PACS_004 = keccak256("pacs.004"); // Payment Return + bytes32 public constant PACS_008 = keccak256("pacs.008"); // FIToFICustomerCreditTransfer + bytes32 public constant PACS_009 = keccak256("pacs.009"); // FinancialInstitutionCreditTransfer + bytes32 public constant CAMT_052 = keccak256("camt.052"); // BankToCustomerAccountReport + bytes32 public constant CAMT_053 = keccak256("camt.053"); // BankToCustomerStatement + bytes32 public constant CAMT_054 = keccak256("camt.054"); // BankToCustomerDebitCreditNotification + bytes32 public constant CAMT_056 = keccak256("camt.056"); // FIToFIPaymentCancellationRequest + bytes32 public constant CAMT_029 = keccak256("camt.029"); // ResolutionOfInvestigation + + /** + * @notice Checks if a message type is an outbound initiation message + * @param msgType The message type to check + * @return true if it's an outbound initiation message + */ + function isOutboundInitiation(bytes32 msgType) internal pure returns (bool) { + return msgType == PAIN_001 || msgType == PACS_008 || msgType == PACS_009; + } + + /** + * @notice Checks if a message type is an inbound notification/confirmation + * @param msgType The message type to check + * @return true if it's an inbound notification + */ + function isInboundNotification(bytes32 msgType) internal pure returns (bool) { + return msgType == PACS_002 || msgType == CAMT_052 || msgType == CAMT_053 || msgType == CAMT_054; + } + + /** + * @notice Checks if a message type is a return/reversal message + * @param msgType The message type to check + * @return true if it's a return/reversal message + */ + function isReturnOrReversal(bytes32 msgType) internal pure returns (bool) { + return msgType == PACS_004 || msgType == CAMT_056 || msgType == CAMT_029; + } +} + diff --git a/contracts/emoney/libraries/RailTypes.sol b/contracts/emoney/libraries/RailTypes.sol new file mode 100644 index 0000000..df46260 --- /dev/null +++ b/contracts/emoney/libraries/RailTypes.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @title RailTypes + * @notice Type definitions for payment rails system + */ +library RailTypes { + enum Rail { + FEDWIRE, + SWIFT, + SEPA, + RTGS + } + + enum State { + CREATED, + VALIDATED, + SUBMITTED_TO_RAIL, + PENDING, + SETTLED, + REJECTED, + CANCELLED, + RECALLED + } + + // Message type constants (bytes32 hashes of ISO-20022 message types) + bytes32 public constant MSG_TYPE_PAIN_001 = keccak256("pain.001"); + bytes32 public constant MSG_TYPE_PACS_002 = keccak256("pacs.002"); + bytes32 public constant MSG_TYPE_PACS_004 = keccak256("pacs.004"); + bytes32 public constant MSG_TYPE_PACS_008 = keccak256("pacs.008"); + bytes32 public constant MSG_TYPE_PACS_009 = keccak256("pacs.009"); + bytes32 public constant MSG_TYPE_CAMT_052 = keccak256("camt.052"); + bytes32 public constant MSG_TYPE_CAMT_053 = keccak256("camt.053"); + bytes32 public constant MSG_TYPE_CAMT_054 = keccak256("camt.054"); + bytes32 public constant MSG_TYPE_CAMT_056 = keccak256("camt.056"); + bytes32 public constant MSG_TYPE_CAMT_029 = keccak256("camt.029"); + + // Escrow mode constants + uint8 public constant ESCROW_MODE_VAULT = 1; + uint8 public constant ESCROW_MODE_LIEN = 2; +} + diff --git a/contracts/emoney/libraries/ReasonCodes.sol b/contracts/emoney/libraries/ReasonCodes.sol new file mode 100644 index 0000000..80652e2 --- /dev/null +++ b/contracts/emoney/libraries/ReasonCodes.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +library ReasonCodes { + bytes32 public constant OK = keccak256("OK"); + bytes32 public constant PAUSED = keccak256("PAUSED"); + bytes32 public constant FROM_FROZEN = keccak256("FROM_FROZEN"); + bytes32 public constant TO_FROZEN = keccak256("TO_FROZEN"); + bytes32 public constant FROM_NOT_COMPLIANT = keccak256("FROM_NOT_COMPLIANT"); + bytes32 public constant TO_NOT_COMPLIANT = keccak256("TO_NOT_COMPLIANT"); + bytes32 public constant LIEN_BLOCK = keccak256("LIEN_BLOCK"); + bytes32 public constant INSUFF_FREE_BAL = keccak256("INSUFF_FREE_BAL"); + bytes32 public constant BRIDGE_ONLY = keccak256("BRIDGE_ONLY"); + bytes32 public constant NOT_ALLOWED_ROUTE = keccak256("NOT_ALLOWED_ROUTE"); + bytes32 public constant UNAUTHORIZED = keccak256("UNAUTHORIZED"); + bytes32 public constant CONFIG_ERROR = keccak256("CONFIG_ERROR"); +} + diff --git a/services/token-aggregation/config/token-lists/dbis-138.tokenlist.json b/services/token-aggregation/config/token-lists/dbis-138.tokenlist.json index 11a3522..d7fb318 100644 --- a/services/token-aggregation/config/token-lists/dbis-138.tokenlist.json +++ b/services/token-aggregation/config/token-lists/dbis-138.tokenlist.json @@ -811,7 +811,7 @@ "name": "USD Coin (Compliant V2)", "symbol": "cUSDC_V2", "decimals": 6, - "logoURI": "https://d-bis.org/tokens/cusdc.svg", + "logoURI": "https://d-bis.org/tokens/cusdc_v2.svg", "tags": [ "stablecoin", "defi", @@ -846,7 +846,7 @@ "name": "Tether USD (Compliant V2)", "symbol": "cUSDT_V2", "decimals": 6, - "logoURI": "https://d-bis.org/tokens/cusdt.svg", + "logoURI": "https://d-bis.org/tokens/cusdt_v2.svg", "tags": [ "stablecoin", "defi",