// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../../src/TokenFactory138.sol"; import "../../src/eMoneyToken.sol"; import "../../src/PolicyManager.sol"; import "../../src/ComplianceRegistry.sol"; import "../../src/DebtRegistry.sol"; import "../../src/RailTriggerRegistry.sol"; import "../../src/ISO20022Router.sol"; import "../../src/AccountWalletRegistry.sol"; import "../../src/SettlementOrchestrator.sol"; import "../../src/RailEscrowVault.sol"; import "../../src/interfaces/ITokenFactory138.sol"; import "../../src/interfaces/IRailTriggerRegistry.sol"; import "../../src/interfaces/IISO20022Router.sol"; import "../../src/libraries/RailTypes.sol"; import "../../src/libraries/ISO20022Types.sol"; import "../../src/libraries/ReasonCodes.sol"; import "../../src/libraries/AccountHashing.sol"; contract PaymentRailsFlowTest is Test { // Core system TokenFactory138 public factory; eMoneyToken public token; PolicyManager public policyManager; ComplianceRegistry public complianceRegistry; DebtRegistry public debtRegistry; // Payment rails system RailTriggerRegistry public triggerRegistry; ISO20022Router public router; AccountWalletRegistry public accountWalletRegistry; SettlementOrchestrator public orchestrator; RailEscrowVault public escrowVault; address public admin; address public deployer; address public issuer; address public settlementOperator; address public railAdapter; address public accountManager; address public user1; address public user2; bytes32 public accountRefId1; bytes32 public walletRefId1; bytes32 public instructionId1; function setUp() public { admin = address(0x1); deployer = address(0x2); issuer = address(0x3); settlementOperator = address(0x4); railAdapter = address(0x5); accountManager = address(0x6); user1 = address(0x10); user2 = address(0x20); // Deploy core contracts complianceRegistry = new ComplianceRegistry(admin); debtRegistry = new DebtRegistry(admin); policyManager = new PolicyManager(admin, address(complianceRegistry), address(debtRegistry)); // Deploy token implementation eMoneyToken implementation = new eMoneyToken(); // Deploy factory factory = new TokenFactory138( admin, address(implementation), address(policyManager), address(debtRegistry), address(complianceRegistry) ); // Deploy payment rails contracts triggerRegistry = new RailTriggerRegistry(admin); escrowVault = new RailEscrowVault(admin); accountWalletRegistry = new AccountWalletRegistry(admin); orchestrator = new SettlementOrchestrator( admin, address(triggerRegistry), address(escrowVault), address(accountWalletRegistry), address(policyManager), address(debtRegistry), address(complianceRegistry) ); router = new ISO20022Router(admin, address(triggerRegistry)); // Set up roles vm.startPrank(admin); factory.grantRole(factory.TOKEN_DEPLOYER_ROLE(), deployer); policyManager.grantRole(policyManager.POLICY_OPERATOR_ROLE(), admin); policyManager.grantRole(policyManager.POLICY_OPERATOR_ROLE(), address(factory)); complianceRegistry.grantRole(complianceRegistry.COMPLIANCE_ROLE(), admin); debtRegistry.grantRole(debtRegistry.DEBT_AUTHORITY_ROLE(), admin); debtRegistry.grantRole(debtRegistry.DEBT_AUTHORITY_ROLE(), address(orchestrator)); triggerRegistry.grantRole(triggerRegistry.RAIL_OPERATOR_ROLE(), address(router)); triggerRegistry.grantRole(triggerRegistry.RAIL_OPERATOR_ROLE(), settlementOperator); triggerRegistry.grantRole(triggerRegistry.RAIL_ADAPTER_ROLE(), railAdapter); escrowVault.grantRole(escrowVault.SETTLEMENT_OPERATOR_ROLE(), address(orchestrator)); orchestrator.grantRole(orchestrator.SETTLEMENT_OPERATOR_ROLE(), settlementOperator); orchestrator.grantRole(orchestrator.RAIL_ADAPTER_ROLE(), railAdapter); accountWalletRegistry.grantRole(accountWalletRegistry.ACCOUNT_MANAGER_ROLE(), accountManager); router.grantRole(router.RAIL_OPERATOR_ROLE(), settlementOperator); vm.stopPrank(); // Deploy token via factory ITokenFactory138.TokenConfig memory config = ITokenFactory138.TokenConfig({ issuer: issuer, decimals: 18, defaultLienMode: 2, // Encumbered bridgeOnly: false, bridge: address(0) }); vm.prank(deployer); address tokenAddress = factory.deployToken("USD eMoney", "USDe", config); token = eMoneyToken(tokenAddress); // Set up compliance vm.startPrank(admin); complianceRegistry.setCompliance(user1, true, 1, keccak256("US")); complianceRegistry.setCompliance(user2, true, 1, keccak256("US")); complianceRegistry.setCompliance(issuer, true, 1, keccak256("US")); vm.stopPrank(); // Set up account/wallet mappings accountRefId1 = AccountHashing.hashAccountRef( keccak256("FEDWIRE"), keccak256("US"), keccak256("1234567890"), keccak256("salt1") ); walletRefId1 = AccountHashing.hashWalletRef(138, user1, keccak256("METAMASK")); vm.prank(accountManager); accountWalletRegistry.linkAccountToWallet(accountRefId1, walletRefId1, keccak256("METAMASK")); // Mint tokens to user1 vm.prank(issuer); token.mint(user1, 10000 * 10**18, ReasonCodes.OK); instructionId1 = keccak256("instruction1"); } function test_outboundFlow_vaultMode() public { uint256 amount = 1000 * 10**18; // 1. Submit outbound message IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ msgType: ISO20022Types.PAIN_001, instructionId: instructionId1, endToEndId: keccak256("e2e1"), accountRefId: accountRefId1, counterpartyRefId: keccak256("counterparty1"), token: address(token), amount: amount, currencyCode: keccak256("USD"), payloadHash: keccak256("payload1") }); vm.prank(settlementOperator); uint256 triggerId = router.submitOutbound(m); // 2. Approve vault vm.startPrank(user1); token.approve(address(escrowVault), amount); vm.stopPrank(); // 3. Validate and lock (requires account address - simplified for test) // In production, this would resolve accountRefId to user1 via AccountWalletRegistry // For this test, we'll manually set up the trigger state vm.prank(railAdapter); triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK); // Manually lock in vault (simulating orchestrator behavior) vm.prank(address(orchestrator)); escrowVault.lock(address(token), user1, amount, triggerId, RailTypes.Rail.FEDWIRE); assertEq(escrowVault.getEscrowAmount(address(token), triggerId), amount); assertEq(token.balanceOf(address(escrowVault)), amount); // 4. Mark as submitted bytes32 railTxRef = keccak256("railTx1"); vm.prank(railAdapter); orchestrator.markSubmitted(triggerId, railTxRef); IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); assertEq(uint8(trigger.state), uint8(RailTypes.State.PENDING)); // 5. Confirm settled (outbound - burns tokens) vm.prank(railAdapter); orchestrator.confirmSettled(triggerId, railTxRef); trigger = triggerRegistry.getTrigger(triggerId); assertEq(uint8(trigger.state), uint8(RailTypes.State.SETTLED)); } function test_inboundFlow() public { uint256 amount = 2000 * 10**18; // 1. Submit inbound message IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ msgType: ISO20022Types.CAMT_054, instructionId: keccak256("instruction2"), endToEndId: keccak256("e2e2"), accountRefId: accountRefId1, counterpartyRefId: keccak256("counterparty2"), token: address(token), amount: amount, currencyCode: keccak256("USD"), payloadHash: keccak256("payload2") }); vm.prank(settlementOperator); uint256 triggerId = router.submitInbound(m); // 2. Move to PENDING state (simulating adapter submission) vm.startPrank(railAdapter); triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK); triggerRegistry.updateState(triggerId, RailTypes.State.SUBMITTED_TO_RAIL, ReasonCodes.OK); triggerRegistry.updateState(triggerId, RailTypes.State.PENDING, ReasonCodes.OK); orchestrator.markSubmitted(triggerId, keccak256("railTx2")); vm.stopPrank(); uint256 user1BalanceBefore = token.balanceOf(user1); // 3. Confirm settled (inbound - mints tokens) // Note: This requires account resolution which is simplified in the orchestrator // In production, AccountWalletRegistry would resolve accountRefId to user1 vm.prank(railAdapter); orchestrator.confirmSettled(triggerId, keccak256("railTx2")); IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); assertEq(uint8(trigger.state), uint8(RailTypes.State.SETTLED)); } function test_rejectionFlow() public { uint256 amount = 1000 * 10**18; // Create trigger IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ msgType: ISO20022Types.PAIN_001, instructionId: keccak256("instruction3"), endToEndId: bytes32(0), accountRefId: accountRefId1, counterpartyRefId: bytes32(0), token: address(token), amount: amount, currencyCode: keccak256("USD"), payloadHash: bytes32(0) }); vm.prank(settlementOperator); uint256 triggerId = router.submitOutbound(m); // Approve and lock vm.startPrank(user1); token.approve(address(escrowVault), amount); vm.stopPrank(); vm.prank(railAdapter); triggerRegistry.updateState(triggerId, RailTypes.State.VALIDATED, ReasonCodes.OK); vm.prank(address(orchestrator)); escrowVault.lock(address(token), user1, amount, triggerId, RailTypes.Rail.FEDWIRE); // Reject bytes32 reason = keccak256("INSUFFICIENT_FUNDS"); vm.prank(railAdapter); orchestrator.confirmRejected(triggerId, reason); IRailTriggerRegistry.Trigger memory trigger = triggerRegistry.getTrigger(triggerId); assertEq(uint8(trigger.state), uint8(RailTypes.State.REJECTED)); } function test_idempotency() public { IISO20022Router.CanonicalMessage memory m = IISO20022Router.CanonicalMessage({ msgType: ISO20022Types.PAIN_001, instructionId: instructionId1, endToEndId: bytes32(0), accountRefId: accountRefId1, counterpartyRefId: bytes32(0), token: address(token), amount: 1000 * 10**18, currencyCode: keccak256("USD"), payloadHash: bytes32(0) }); vm.prank(settlementOperator); router.submitOutbound(m); // Try to submit same instructionId again vm.prank(settlementOperator); vm.expectRevert("RailTriggerRegistry: duplicate instructionId"); router.submitOutbound(m); } }