From 3a098ca47024d62391ccc19a0702955f1a87e806 Mon Sep 17 00:00:00 2001 From: geminilclaw Date: Sat, 21 Mar 2026 17:38:12 +0800 Subject: [PATCH 1/6] chore: Final Phase 1 check and deployment artifacts preparation --- src/PayNodeRouter.sol | 49 +++++++++++++++++++++++++++------------- test/PayNodeRouter.t.sol | 15 +++++++----- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/PayNodeRouter.sol b/src/PayNodeRouter.sol index 4f27edc..094e5d0 100644 --- a/src/PayNodeRouter.sol +++ b/src/PayNodeRouter.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; /** * @title PayNodeRouter @@ -17,7 +17,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; * Stateless design ensures minimal gas costs (no SSTORE for order state). * Protocol takes a fixed 1% fee (100 BPS). */ -contract PayNodeRouter is ReentrancyGuard, Ownable { +contract PayNodeRouter is Ownable2Step, Pausable { using SafeERC20 for IERC20; address public protocolTreasury; @@ -26,20 +26,24 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { uint256 public constant PROTOCOL_FEE_BPS = 100; uint256 public constant MAX_BPS = 10000; - // Redesigned event to match SDK requirements (indexed orderId, token verification) + error InvalidAddress(); + error AmountMustBeGreaterThanZero(); + + // Redesigned event to match SDK requirements (indexed orderId, token verification, chainId) event PaymentReceived( bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, - uint256 fee + uint256 fee, + uint256 chainId ); event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury); constructor(address _protocolTreasury) Ownable(msg.sender) { - require(_protocolTreasury != address(0), "Invalid treasury address"); + if (_protocolTreasury == address(0)) revert InvalidAddress(); protocolTreasury = _protocolTreasury; } @@ -48,12 +52,26 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { * @param _newTreasury The new address for fee collection. */ function updateTreasury(address _newTreasury) external onlyOwner { - require(_newTreasury != address(0), "Invalid treasury"); + if (_newTreasury == address(0)) revert InvalidAddress(); address old = protocolTreasury; protocolTreasury = _newTreasury; emit TreasuryUpdated(old, _newTreasury); } + /** + * @notice Pause the contract in case of emergencies + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpause the contract + */ + function unpause() external onlyOwner { + _unpause(); + } + /** * @dev Process an M2M payment for any ERC20 token. Payer must have already approved this contract. * @param token The ERC20 token address being used for payment (e.g. USDC, USDT). @@ -66,7 +84,7 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { address merchant, uint256 amount, bytes32 orderId - ) external nonReentrant { + ) external whenNotPaused { _processPayment(msg.sender, token, merchant, amount, orderId); } @@ -75,6 +93,7 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { * Allows AI agents to sign locally and pay in a single on-chain transaction. */ function payWithPermit( + address payer, address token, address merchant, uint256 amount, @@ -83,10 +102,10 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { uint8 v, bytes32 r, bytes32 s - ) external nonReentrant { + ) external whenNotPaused { // 1. Consume permit to grant allowance to this router IERC20Permit(token).permit( - msg.sender, + payer, address(this), amount, deadline, @@ -96,7 +115,7 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { ); // 2. Execute the payment split - _processPayment(msg.sender, token, merchant, amount, orderId); + _processPayment(payer, token, merchant, amount, orderId); } /** @@ -109,9 +128,8 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { uint256 amount, bytes32 orderId ) internal { - require(merchant != address(0), "Invalid merchant address"); - require(token != address(0), "Invalid token address"); - require(amount > 0, "Amount must be greater than 0"); + if (merchant == address(0) || token == address(0)) revert InvalidAddress(); + if (amount == 0) revert AmountMustBeGreaterThanZero(); // Calculate 1% fee uint256 fee = (amount * PROTOCOL_FEE_BPS) / MAX_BPS; @@ -125,7 +143,6 @@ contract PayNodeRouter is ReentrancyGuard, Ownable { } // Emit event for SDK webhook listeners - // The SDK MUST verify the 'token' address to prevent fake-token attacks. - emit PaymentReceived(orderId, merchant, payer, token, amount, fee); + emit PaymentReceived(orderId, merchant, payer, token, amount, fee, block.chainid); } } diff --git a/test/PayNodeRouter.t.sol b/test/PayNodeRouter.t.sol index 595fd62..9cab5a2 100644 --- a/test/PayNodeRouter.t.sol +++ b/test/PayNodeRouter.t.sol @@ -41,7 +41,8 @@ contract PayNodeRouterTest is Test { address indexed payer, address token, uint256 amount, - uint256 fee + uint256 fee, + uint256 chainId ); function setUp() public { @@ -68,7 +69,7 @@ contract PayNodeRouterTest is Test { uint256 expectedFee = 1 * 10 ** 6; vm.expectEmit(true, true, true, true); - emit PaymentReceived(orderId, merchant, payer, address(usdc), paymentAmount, expectedFee); + emit PaymentReceived(orderId, merchant, payer, address(usdc), paymentAmount, expectedFee, block.chainid); vm.prank(payer); router.pay(address(usdc), merchant, paymentAmount, orderId); @@ -86,7 +87,7 @@ contract PayNodeRouterTest is Test { uint256 expectedFee = 5 * 10 ** 5; // 0.5 USDT vm.expectEmit(true, true, true, true); - emit PaymentReceived(orderId, merchant, payer, address(usdt), paymentAmount, expectedFee); + emit PaymentReceived(orderId, merchant, payer, address(usdt), paymentAmount, expectedFee, block.chainid); vm.prank(payer); router.pay(address(usdt), merchant, paymentAmount, orderId); @@ -108,10 +109,12 @@ contract PayNodeRouterTest is Test { uint256 expectedFee = 1 * 10 ** 6; vm.expectEmit(true, true, true, true); - emit PaymentReceived(orderId, merchant, payer, address(usdc), paymentAmount, expectedFee); + emit PaymentReceived(orderId, merchant, payer, address(usdc), paymentAmount, expectedFee, block.chainid); - vm.prank(payer); - router.payWithPermit(address(usdc), merchant, paymentAmount, orderId, deadline, v, r, s); + // We use an agent to send the transaction to test the Relayer functionality properly! + address agent = address(uint160(0x12345)); + vm.prank(agent); + router.payWithPermit(payer, address(usdc), merchant, paymentAmount, orderId, deadline, v, r, s); assertEq(usdc.balanceOf(merchant), 99 * 10 ** 6); assertEq(usdc.balanceOf(treasury), 1 * 10 ** 6); From 7dc75e10f8b1ee1fb92d44ea4ae7c4d06d3a9188 Mon Sep 17 00:00:00 2001 From: geminilclaw Date: Sat, 21 Mar 2026 19:08:47 +0800 Subject: [PATCH 2/6] chore: sync v1.1.0 mainnet configuration script --- DEPLOYMENT.md | 61 +++++++++++++++++++++++++++++++++++++++++++++ script/Config.s.sol | 11 ++++++++ 2 files changed, 72 insertions(+) create mode 100644 DEPLOYMENT.md create mode 100644 script/Config.s.sol diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..f434ffd --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,61 @@ +# PayNode Protocol Contract Deployment Guide + +This document provides standardized deployment commands for the PayNode Protocol. + +## ๐Ÿ—‚๏ธ Deployment Constants +- **Compiler Version:** v0.8.20 +- **Optimizer:** Enabled (200 runs) +- **Protocol Treasury:** `0x598bF63F5449876efafa7b36b77Deb2070621C0E` + +--- +## ๐Ÿงช 1. Base Sepolia (Testnet) +Deploy using the specialized deployment script for the testnet. + +- **Current v1.1 Address:** `0xB587Bc36aaCf65962eCd6Ba59e2DA76f2f575408` + +```bash +cd packages/contracts && \ +... +forge script script/DeploySepolia.s.sol:DeploySepolia \ + --rpc-url https://sepolia.base.org \ + --private-key \ + --broadcast \ + -vvvv +``` +*Note: If the official RPC is slow, use `https://base-sepolia-rpc.publicnode.com`.* + +--- + +## ๐Ÿš€ 2. Base Mainnet (Production) +Deploy using the specialized deployment script for the production environment. + +```bash +cd packages/contracts && \ +forge script script/DeployPOM.s.sol:DeployPOM \ + --rpc-url https://mainnet.base.org \ + --private-key \ + --broadcast \ + -vvvv +``` + +--- + +## ๐Ÿ“ 3. Post-Deployment Checklist + +1. **Verify on Basescan:** + - After deployment, note the `PayNodeRouter Deployed to:` address in the console output. + - Go to [Basescan](https://basescan.org/) and search for the address. + - Click "Contract" -> "Verify and Publish". + - Use `Solidity (Single File)` mode. If you need a flattened file, run: + ```bash + forge flatten src/PayNodeRouter.sol > Flattened.sol + ``` + +2. **Update Ecosystem Config:** + Update the `ROUTER_ADDRESS` in the following locations: + - `packages/sdk-js/src/index.ts` + - `packages/sdk-python/paynode_sdk/client.py` + - `apps/paynode-web/.env` (`NEXT_PUBLIC_PAYNODE_ROUTER_ADDRESS`) + +3. **Transfer Ownership (Optional):** + If deploying with a hot wallet, consider transferring ownership to a multisig (Gnosis Safe) using `transferOwnership`. diff --git a/script/Config.s.sol b/script/Config.s.sol new file mode 100644 index 0000000..ef34b6a --- /dev/null +++ b/script/Config.s.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// Generated by scripts/sync-config.py +library Config { + address public constant ROUTER_MAINNET = 0x92e20164FC457a2aC35f53D06268168e6352b200; + address public constant ROUTER_SEPOLIA = 0xB587Bc36aaCf65962eCd6Ba59e2DA76f2f575408; + address public constant TREASURY = 0x598bF63F5449876efafa7b36b77Deb2070621C0E; + address public constant USDC_MAINNET = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address public constant USDC_SEPOLIA = 0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798; +} From 8d2807ce4ab3f44ca7b4d659e3ff575a8fbb440f Mon Sep 17 00:00:00 2001 From: geminilclaw Date: Sat, 21 Mar 2026 19:19:01 +0800 Subject: [PATCH 3/6] refactor: improve code formatting across router contract, deployment script, and tests. --- script/DeploySepolia.s.sol | 2 +- src/PayNodeRouter.sol | 29 +++++------------------------ test/PayNodeRouter.t.sol | 20 +++++++++++--------- 3 files changed, 17 insertions(+), 34 deletions(-) diff --git a/script/DeploySepolia.s.sol b/script/DeploySepolia.s.sol index 4a7d5ba..efd4ec9 100644 --- a/script/DeploySepolia.s.sol +++ b/script/DeploySepolia.s.sol @@ -7,7 +7,7 @@ import {PayNodeRouter} from "../src/PayNodeRouter.sol"; contract DeploySepolia is Script { function run() external { address treasury = 0x598bF63F5449876efafa7b36b77Deb2070621C0E; - + vm.startBroadcast(); PayNodeRouter router = new PayNodeRouter(treasury); console.log("PayNodeRouter Deployed to:", address(router)); diff --git a/src/PayNodeRouter.sol b/src/PayNodeRouter.sol index 094e5d0..292b976 100644 --- a/src/PayNodeRouter.sol +++ b/src/PayNodeRouter.sol @@ -12,7 +12,7 @@ import "@openzeppelin/contracts/utils/Pausable.sol"; * @author AgentPay Protocol (PayNode Labs) * @notice This contract is licensed under Business Source License 1.1. * Commercial use by competitors is restricted for the first 2 years. - * + * * @dev Non-custodial, multi-coin payment router for the Agentic Economy. * Stateless design ensures minimal gas costs (no SSTORE for order state). * Protocol takes a fixed 1% fee (100 BPS). @@ -79,12 +79,7 @@ contract PayNodeRouter is Ownable2Step, Pausable { * @param amount The total payment amount. * @param orderId External tracking ID from the merchant's system (e.g., UUID mapped to bytes32). */ - function pay( - address token, - address merchant, - uint256 amount, - bytes32 orderId - ) external whenNotPaused { + function pay(address token, address merchant, uint256 amount, bytes32 orderId) external whenNotPaused { _processPayment(msg.sender, token, merchant, amount, orderId); } @@ -104,15 +99,7 @@ contract PayNodeRouter is Ownable2Step, Pausable { bytes32 s ) external whenNotPaused { // 1. Consume permit to grant allowance to this router - IERC20Permit(token).permit( - payer, - address(this), - amount, - deadline, - v, - r, - s - ); + IERC20Permit(token).permit(payer, address(this), amount, deadline, v, r, s); // 2. Execute the payment split _processPayment(payer, token, merchant, amount, orderId); @@ -121,13 +108,7 @@ contract PayNodeRouter is Ownable2Step, Pausable { /** * @dev Internal split logic */ - function _processPayment( - address payer, - address token, - address merchant, - uint256 amount, - bytes32 orderId - ) internal { + function _processPayment(address payer, address token, address merchant, uint256 amount, bytes32 orderId) internal { if (merchant == address(0) || token == address(0)) revert InvalidAddress(); if (amount == 0) revert AmountMustBeGreaterThanZero(); @@ -137,7 +118,7 @@ contract PayNodeRouter is Ownable2Step, Pausable { // Execute atomic non-custodial transfers IERC20(token).safeTransferFrom(payer, merchant, merchantAmount); - + if (fee > 0) { IERC20(token).safeTransferFrom(payer, protocolTreasury, fee); } diff --git a/test/PayNodeRouter.t.sol b/test/PayNodeRouter.t.sol index 9cab5a2..55d0cc1 100644 --- a/test/PayNodeRouter.t.sol +++ b/test/PayNodeRouter.t.sol @@ -18,7 +18,7 @@ contract MockToken is ERC20, ERC20Permit { function mint(address to, uint256 amount) external { _mint(to, amount); } - + function decimals() public view override returns (uint8) { return _decimals; } @@ -31,7 +31,7 @@ contract PayNodeRouterTest is Test { address public treasury = address(1); address public merchant = address(2); - + uint256 public payerPrivateKey = 0xA11CE; address public payer; @@ -47,16 +47,16 @@ contract PayNodeRouterTest is Test { function setUp() public { payer = vm.addr(payerPrivateKey); - + // Mock Base USDC (6 decimals) usdc = new MockToken("Base USDC", "USDC", 6); // Mock Tether USD (6 decimals) usdt = new MockToken("Tether USD", "USDT", 6); - + router = new PayNodeRouter(treasury); // Mint initial balances - usdc.mint(payer, 1000 * 10 ** 6); + usdc.mint(payer, 1000 * 10 ** 6); usdt.mint(payer, 1000 * 10 ** 6); } @@ -77,7 +77,7 @@ contract PayNodeRouterTest is Test { assertEq(usdc.balanceOf(merchant), 99 * 10 ** 6); assertEq(usdc.balanceOf(treasury), 1 * 10 ** 6); } - + function test_Pay_USDT() public { uint256 paymentAmount = 50 * 10 ** 6; bytes32 orderId = keccak256("order_agent_usdt_01"); @@ -101,10 +101,12 @@ contract PayNodeRouterTest is Test { bytes32 orderId = keccak256("order_agent_002"); uint256 deadline = block.timestamp + 1 hours; - bytes32 permitTypehash = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - bytes32 structHash = keccak256(abi.encode(permitTypehash, payer, address(router), paymentAmount, usdc.nonces(payer), deadline)); + bytes32 permitTypehash = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 structHash = + keccak256(abi.encode(permitTypehash, payer, address(router), paymentAmount, usdc.nonces(payer), deadline)); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", usdc.DOMAIN_SEPARATOR(), structHash)); - + (uint8 v, bytes32 r, bytes32 s) = vm.sign(payerPrivateKey, digest); uint256 expectedFee = 1 * 10 ** 6; From 32a7a2476f6a1688e2c77c41f123be9a6f514c0f Mon Sep 17 00:00:00 2001 From: geminilclaw Date: Sat, 21 Mar 2026 19:21:03 +0800 Subject: [PATCH 4/6] chore: fix unaliased plain imports in solidity files --- src/MockUSDC.sol | 2 +- src/PayNodeRouter.sol | 10 +++++----- test/PayNodeRouter.t.sol | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/MockUSDC.sol b/src/MockUSDC.sol index e5e0a7f..7d5125b 100644 --- a/src/MockUSDC.sol +++ b/src/MockUSDC.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /** * @title MockUSDC diff --git a/src/PayNodeRouter.sol b/src/PayNodeRouter.sol index 292b976..12051ef 100644 --- a/src/PayNodeRouter.sol +++ b/src/PayNodeRouter.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BSL-1.1 pragma solidity ^0.8.20; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/access/Ownable2Step.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; /** * @title PayNodeRouter diff --git a/test/PayNodeRouter.t.sol b/test/PayNodeRouter.t.sol index 55d0cc1..fdff01f 100644 --- a/test/PayNodeRouter.t.sol +++ b/test/PayNodeRouter.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "forge-std/Test.sol"; -import "../src/PayNodeRouter.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { Test, console } from "forge-std/Test.sol"; +import { PayNodeRouter } from "../src/PayNodeRouter.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; // Mock USDC/USDT with Permit for testing contract MockToken is ERC20, ERC20Permit { From 9397e5c9d17fa7570ba0809a25ba4878f0838f78 Mon Sep 17 00:00:00 2001 From: geminilclaw Date: Sat, 21 Mar 2026 19:27:09 +0800 Subject: [PATCH 5/6] refactor: remove unused console import in PayNodeRouter.t.sol --- test/PayNodeRouter.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PayNodeRouter.t.sol b/test/PayNodeRouter.t.sol index fdff01f..481a566 100644 --- a/test/PayNodeRouter.t.sol +++ b/test/PayNodeRouter.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { PayNodeRouter } from "../src/PayNodeRouter.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; From 6b1019ee6e9a547293a5ec2f08481fb41a6816c4 Mon Sep 17 00:00:00 2001 From: geminilclaw Date: Sat, 21 Mar 2026 19:31:12 +0800 Subject: [PATCH 6/6] style: Standardize import statement formatting by removing spaces around curly braces. --- src/MockUSDC.sol | 2 +- src/PayNodeRouter.sol | 10 +++++----- test/PayNodeRouter.t.sol | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/MockUSDC.sol b/src/MockUSDC.sol index 7d5125b..8d56ab4 100644 --- a/src/MockUSDC.sol +++ b/src/MockUSDC.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /** * @title MockUSDC diff --git a/src/PayNodeRouter.sol b/src/PayNodeRouter.sol index 12051ef..22582dc 100644 --- a/src/PayNodeRouter.sol +++ b/src/PayNodeRouter.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BSL-1.1 pragma solidity ^0.8.20; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; /** * @title PayNodeRouter diff --git a/test/PayNodeRouter.t.sol b/test/PayNodeRouter.t.sol index 481a566..7ed93bc 100644 --- a/test/PayNodeRouter.t.sol +++ b/test/PayNodeRouter.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import { Test } from "forge-std/Test.sol"; -import { PayNodeRouter } from "../src/PayNodeRouter.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {Test} from "forge-std/Test.sol"; +import {PayNodeRouter} from "../src/PayNodeRouter.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; // Mock USDC/USDT with Permit for testing contract MockToken is ERC20, ERC20Permit {