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:
149
src/AccountWalletRegistry.sol
Normal file
149
src/AccountWalletRegistry.sol
Normal 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
123
src/BridgeVault138.sol
Normal 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
100
src/ComplianceRegistry.sol
Normal 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
139
src/DebtRegistry.sol
Normal 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
139
src/ISO20022Router.sol
Normal 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
157
src/PacketRegistry.sol
Normal 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
209
src/PolicyManager.sol
Normal 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
113
src/RailEscrowVault.sol
Normal 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
201
src/RailTriggerRegistry.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
362
src/SettlementOrchestrator.sol
Normal file
362
src/SettlementOrchestrator.sol
Normal 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
114
src/TokenFactory138.sol
Normal 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
239
src/eMoneyToken.sol
Normal 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) {}
|
||||
}
|
||||
|
||||
5
src/errors/TokenErrors.sol
Normal file
5
src/errors/TokenErrors.sol
Normal file
@@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
error TransferBlocked(bytes32 reason, address from, address to, uint256 amount);
|
||||
|
||||
33
src/interfaces/IAccountWalletRegistry.sol
Normal file
33
src/interfaces/IAccountWalletRegistry.sol
Normal 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);
|
||||
}
|
||||
|
||||
36
src/interfaces/IBridgeVault138.sol
Normal file
36
src/interfaces/IBridgeVault138.sol
Normal 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
|
||||
);
|
||||
}
|
||||
|
||||
31
src/interfaces/IComplianceRegistry.sol
Normal file
31
src/interfaces/IComplianceRegistry.sol
Normal 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);
|
||||
}
|
||||
|
||||
49
src/interfaces/IDebtRegistry.sol
Normal file
49
src/interfaces/IDebtRegistry.sol
Normal 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);
|
||||
}
|
||||
|
||||
27
src/interfaces/IISO20022Router.sol
Normal file
27
src/interfaces/IISO20022Router.sol
Normal 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);
|
||||
}
|
||||
|
||||
58
src/interfaces/IPacketRegistry.sol
Normal file
58
src/interfaces/IPacketRegistry.sol
Normal 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);
|
||||
}
|
||||
|
||||
45
src/interfaces/IPolicyManager.sol
Normal file
45
src/interfaces/IPolicyManager.sol
Normal 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);
|
||||
}
|
||||
|
||||
31
src/interfaces/IRailEscrowVault.sol
Normal file
31
src/interfaces/IRailEscrowVault.sol
Normal 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);
|
||||
}
|
||||
|
||||
46
src/interfaces/IRailTriggerRegistry.sol
Normal file
46
src/interfaces/IRailTriggerRegistry.sol
Normal 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);
|
||||
}
|
||||
|
||||
33
src/interfaces/ISettlementOrchestrator.sol
Normal file
33
src/interfaces/ISettlementOrchestrator.sol
Normal 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);
|
||||
}
|
||||
|
||||
33
src/interfaces/ITokenFactory138.sol
Normal file
33
src/interfaces/ITokenFactory138.sol
Normal 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
|
||||
);
|
||||
}
|
||||
|
||||
56
src/interfaces/IeMoneyToken.sol
Normal file
56
src/interfaces/IeMoneyToken.sol
Normal 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
|
||||
);
|
||||
}
|
||||
|
||||
51
src/libraries/AccountHashing.sol
Normal file
51
src/libraries/AccountHashing.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
55
src/libraries/ISO20022Types.sol
Normal file
55
src/libraries/ISO20022Types.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
43
src/libraries/RailTypes.sol
Normal file
43
src/libraries/RailTypes.sol
Normal 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;
|
||||
}
|
||||
|
||||
18
src/libraries/ReasonCodes.sol
Normal file
18
src/libraries/ReasonCodes.sol
Normal 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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user