Initial project setup: Add contracts, API definitions, tests, and documentation

- Add Foundry project configuration (foundry.toml, foundry.lock)
- Add Solidity contracts (TokenFactory138, BridgeVault138, ComplianceRegistry, etc.)
- Add API definitions (OpenAPI, GraphQL, gRPC, AsyncAPI)
- Add comprehensive test suite (unit, integration, fuzz, invariants)
- Add API services (REST, GraphQL, orchestrator, packet service)
- Add documentation (ISO20022 mapping, runbooks, adapter guides)
- Add development tools (RBC tool, Swagger UI, mock server)
- Update OpenZeppelin submodules to v5.0.0
This commit is contained in:
defiQUG
2025-12-12 10:59:41 -08:00
parent 26b5aaf932
commit 651ff4f7eb
281 changed files with 24813 additions and 2 deletions

View File

@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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;
}
}

123
src/BridgeVault138.sol Normal file
View File

@@ -0,0 +1,123 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.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";
/// @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 {
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 {
require(token != address(0), "BridgeVault138: zero token");
require(amount > 0, "BridgeVault138: zero amount");
require(targetRecipient != address(0), "BridgeVault138: zero recipient");
// Transfer tokens from user
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
// Check if transfer would be allowed (liens are enforced via PolicyManager)
(bool allowed, ) = policyManager.canTransfer(token, msg.sender, address(this), amount);
require(allowed, "BridgeVault138: transfer blocked");
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 (placeholder) 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
* @notice Light client proof verification is currently a placeholder - requires actual implementation
*/
function unlock(
address token,
address to,
uint256 amount,
bytes32 sourceChain,
bytes32 sourceTx
) external override onlyRole(BRIDGE_OPERATOR_ROLE) {
require(token != address(0), "BridgeVault138: zero token");
require(to != address(0), "BridgeVault138: zero recipient");
require(amount > 0, "BridgeVault138: zero amount");
// Verify proof via light client (placeholder - requires actual implementation)
require(address(lightClient) != address(0), "BridgeVault138: light client not set");
// Note: In production, proof data should be passed as parameter
// bool verified = lightClient.verifyProof(sourceChain, sourceTx, proof);
// require(verified, "BridgeVault138: proof verification failed");
// Check compliance
require(complianceRegistry.isAllowed(to), "BridgeVault138: recipient not compliant");
require(!complianceRegistry.isFrozen(to), "BridgeVault138: recipient frozen");
// Transfer tokens to recipient
IERC20(token).safeTransfer(to, amount);
emit Unlocked(token, to, amount, sourceChain, sourceTx);
}
}

100
src/ComplianceRegistry.sol Normal file
View File

@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}
}

139
src/DebtRegistry.sol Normal file
View File

@@ -0,0 +1,139 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./interfaces/IDebtRegistry.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) {
require(debtor != address(0), "DebtRegistry: zero debtor");
require(amount > 0, "DebtRegistry: zero amount");
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];
require(lien.active, "DebtRegistry: lien not active");
uint256 oldAmount = lien.amount;
require(reduceBy <= oldAmount, "DebtRegistry: reduceBy exceeds amount");
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];
require(lien.active, "DebtRegistry: lien not active");
lien.active = false;
_activeEncumbrance[lien.debtor] -= lien.amount;
_activeLienCount[lien.debtor]--;
emit LienReleased(lienId);
}
}

139
src/ISO20022Router.sol Normal file
View File

@@ -0,0 +1,139 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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;
}
}

157
src/PacketRegistry.sol Normal file
View File

@@ -0,0 +1,157 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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
);
}
}

209
src/PolicyManager.sol Normal file
View File

@@ -0,0 +1,209 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./interfaces/IPolicyManager.sol";
import "./interfaces/IComplianceRegistry.sol";
import "./interfaces/IDebtRegistry.sol";
import "./libraries/ReasonCodes.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) {
require(mode <= 2, "PolicyManager: invalid lien mode");
_tokenConfigs[token].lienMode = 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);
}
}

113
src/RailEscrowVault.sol Normal file
View File

@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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];
}
}

201
src/RailTriggerRegistry.sol Normal file
View File

@@ -0,0 +1,201 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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;
}
}

View File

@@ -0,0 +1,362 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}
}

114
src/TokenFactory138.sol Normal file
View File

@@ -0,0 +1,114 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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";
/**
* @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) {
require(config.issuer != address(0), "TokenFactory138: zero issuer");
require(config.defaultLienMode == 1 || config.defaultLienMode == 2, "TokenFactory138: invalid lien mode");
// 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)
bytes32 codeHash = keccak256(abi.encodePacked(name, symbol, config.issuer, block.number, token));
_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];
}
}

239
src/eMoneyToken.sol Normal file
View File

@@ -0,0 +1,239 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.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,
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();
_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) {
_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) {
_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) {
// 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) {
// ForceTransfer bypasses liens but still enforces compliance
// Check compliance
require(complianceRegistry.isAllowed(from), "eMoneyToken: from not compliant");
require(complianceRegistry.isAllowed(to), "eMoneyToken: to not compliant");
require(!complianceRegistry.isFrozen(from), "eMoneyToken: from frozen");
require(!complianceRegistry.isFrozen(to), "eMoneyToken: to frozen");
// 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) {}
}

View File

@@ -0,0 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
error TransferBlocked(bytes32 reason, address from, address to, uint256 amount);

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IAccountWalletRegistry {
struct WalletLink {
bytes32 walletRefId;
uint64 linkedAt;
bool active;
bytes32 provider; // e.g., "METAMASK", "FIREBLOCKS", "CUSTODY_X"
}
function linkAccountToWallet(bytes32 accountRefId, bytes32 walletRefId, bytes32 provider) external;
function unlinkAccountFromWallet(bytes32 accountRefId, bytes32 walletRefId) external;
function getWallets(bytes32 accountRefId) external view returns (WalletLink[] memory);
function getAccounts(bytes32 walletRefId) external view returns (bytes32[] memory);
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);
}

View File

@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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
) 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
);
}

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @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);
}

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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);
}

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ITokenFactory138 {
struct TokenConfig {
address issuer;
uint8 decimals;
uint8 defaultLienMode; // 1 hard, 2 encumbered
bool bridgeOnly;
address bridge;
}
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
);
}

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IeMoneyToken {
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
);
}

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @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));
}
}

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @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;
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @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;
}

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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");
}