Skip to content

Added router for fission stablepay integration#9

Open
DeveloperAmrit wants to merge 3 commits into
StabilityNexus:mainfrom
DeveloperAmrit:added-fission-router
Open

Added router for fission stablepay integration#9
DeveloperAmrit wants to merge 3 commits into
StabilityNexus:mainfrom
DeveloperAmrit:added-fission-router

Conversation

@DeveloperAmrit

@DeveloperAmrit DeveloperAmrit commented Jan 9, 2026

Copy link
Copy Markdown
Member

I created a new helper smart contract GluonPaymentRouter.sol in src.

  • This contract serves as a payment wrapper that:
  • Accepts a native payment from the user.
  • Performs a Fission operation on the Gluon Vault.
  • Automatically forwards the stable Neutrons to the intended merchant.
  • Returns the volatile/speculative Protons back to the user (the payer), enabling them to retain the “long” position on the asset they just spent.

Summary by CodeRabbit

  • New Features

    • Added a payment router that accepts native ETH, triggers a minting operation, forwards Neutron tokens to a merchant, and returns Proton tokens to the payer.
    • Added a public ABI-like JSON payload describing the protocol's constructor, functions, events, and errors for integration.
  • Tests

    • Added end-to-end tests validating the pay-and-distribute flow, token minting, and correct token routing.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 9, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a Gluon ABI file, a new GluonPaymentRouter contract that calls Gluon.fission and forwards minted Neutron tokens to a merchant and minted Proton tokens back to the caller, plus a Forge test suite with mocks validating the payWithFission flow.

Changes

Cohort / File(s) Summary
Protocol ABI Specification
gluon.json
Adds a full ABI-like JSON array defining constructor, many view/public getters (pricing, tokens, params), state-changing functions (fission, fusion, transmute, updatePriceFeeds, setBetaParams), events, and errors.
Core Implementation
src/GluonPaymentRouter.sol
New GluonPaymentRouter contract plus IERC20 and IGluon interfaces. Implements payWithFission(address merchant, bytes[] calldata updateData) (payable) which records pre-balances, calls gluon.fission with ETH, computes minted amounts by balance differences, transfers minted Neutrons to merchant and minted Protons back to the caller; includes receive() to accept ETH.
Test Suite
test/GluonPaymentRouter.t.sol
Adds Forge tests with MockToken and MockGluon. MockGluon.fission mints fixed amounts; GluonPaymentRouterTest asserts merchant receives Neutrons, caller receives Protons, and router retains no tokens.

Sequence Diagram

sequenceDiagram
    actor User
    participant Router as GluonPaymentRouter
    participant Gluon as IGluon
    participant Neutron as NeutronToken (IERC20)
    participant Proton as ProtonToken (IERC20)
    actor Merchant

    User->>Router: payWithFission(merchant, updateData) + ETH
    Router->>Neutron: balanceOf(Router) (pre)
    Router->>Proton: balanceOf(Router) (pre)
    Router->>Gluon: fission(amountIn, Router, updateData) + ETH
    Gluon->>Router: mint Neutron tokens
    Gluon->>Router: mint Proton tokens
    Router->>Neutron: balanceOf(Router) (post)
    Router->>Neutron: transfer(merchant, mintedNeutron)
    Router->>Proton: balanceOf(Router) (post)
    Router->>Proton: transfer(User, mintedProton)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped in code where tokens split and run,
Gluon sparked fission beneath the sun,
Neutrons to merchants, Protons back to me,
I nibble logs and watch the transfers be—
A joyful hop for functions set free.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing GluonPaymentRouter for Fission-based payments. It is specific, concise, and directly related to the primary addition.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @gluon.json:
- Around line 145-155: The ABI entries for NEUTRON_TOKEN and PROTON_TOKEN
contain a typo in the internalType ("contract Tokeon"); update these
internalType fields to the correct contract name (e.g., "contract Token" or the
actual deployed contract identifier) so they match the real contract type used
by your codebase, ensuring both the NEUTRON_TOKEN and PROTON_TOKEN ABI objects
are corrected.

In @src/GluonPaymentRouter.sol:
- Around line 26-30: The constructor currently assigns IGluon(_gluon) without
validating the input; add a require check at the start of the constructor to
ensure _gluon != address(0) (with a clear revert message like "Gluon: zero
address") before setting gluon, neutron, and proton so the contract cannot be
initialized with address(0), and keep the rest of the assignments to gluon =
IGluon(_gluon), neutron = IERC20(gluon.NEUTRON_TOKEN()), and proton =
IERC20(gluon.PROTON_TOKEN()) unchanged.
- Line 37: The payWithFission function lacks validation of the merchant
parameter which could allow sending value to the zero address and effectively
burn funds; add a require(merchant != address(0), "payWithFission: merchant is
zero address") at the start of payWithFission (the function signature
payWithFission(address merchant, bytes[] calldata updateData)) to reject
zero-address merchants and revert with a clear error message.
🧹 Nitpick comments (4)
src/GluonPaymentRouter.sol (2)

37-54: Consider adding slippage protection for user experience.

The function doesn't allow users to specify minimum amounts of neutrons or protons they expect to receive. This means users have no protection against unfavorable pricing conditions or front-running attacks.

Consider adding parameters for minNeutronOut and minProtonOut and reverting if the actual amounts are less.

💡 Example implementation
-function payWithFission(address merchant, bytes[] calldata updateData) external payable {
+function payWithFission(
+    address merchant, 
+    bytes[] calldata updateData,
+    uint256 minNeutronOut,
+    uint256 minProtonOut
+) external payable {
     require(merchant != address(0), "Invalid merchant address");
     
     // ... fission logic ...
     
     // 2. Determine amounts minted
     uint256 nBal = neutron.balanceOf(address(this));
     uint256 pBal = proton.balanceOf(address(this));
+    
+    require(nBal >= minNeutronOut, "Insufficient neutron output");
+    require(pBal >= minProtonOut, "Insufficient proton output");

     // 3. Forward Neutrons to Merchant
     if (nBal > 0) {

56-58: Consider adding a function to recover stuck ETH.

The receive() function allows the contract to accept ETH, which provides flexibility. However, if ETH gets stuck in the contract (e.g., from failed fission calls or accidental sends), there's no way to recover it.

Consider adding an owner-controlled function to withdraw any accidentally stuck ETH.

test/GluonPaymentRouter.t.sol (2)

44-50: MockGluon oversimplifies fission behavior.

The mock always mints exactly 50 ether of each token regardless of the amountIn or msg.value. While this is sufficient for testing the basic token distribution flow, it doesn't validate:

  • Correct parameter passing to the real Gluon contract
  • Realistic token amount calculations
  • Edge cases like very small or very large inputs

Consider enhancing the mock to mint proportional amounts or adding additional test cases with different mock behaviors.


72-92: Expand test coverage with additional scenarios.

The current test only validates the happy path. Consider adding tests for:

  • Zero address merchant (should revert)
  • Zero payment amount / msg.value
  • Router with pre-existing token balances (to verify the delta calculation fix suggested earlier)
  • Failed token transfers
  • Multiple sequential payments to verify no token accumulation in the router
💡 Example additional test
function testRevertsOnZeroAddressMerchant() public {
    bytes[] memory data = new bytes[](0);
    
    vm.prank(user);
    vm.expectRevert("Invalid merchant address");
    router.payWithFission{value: 1 ether}(address(0), data);
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b68843 and 43af077.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • gluon.json
  • src/GluonPaymentRouter.sol
  • test/GluonPaymentRouter.t.sol
🔇 Additional comments (3)
src/GluonPaymentRouter.sol (2)

4-7: Minimal IERC20 interface is sufficient for this use case.

The interface only includes transfer and balanceOf, which is all this contract needs. This is acceptable and keeps the code lean.


39-39: The usage pattern is correct. The fission function in StableCoin.sol separates oracle fee handling from the fission amount:

  • msg.value (ETH) covers only the oracle fees via pythFee calculation; any excess is refunded
  • amountIn is transferred separately as a BASE_TOKEN via safeTransferFrom, not from msg.value

These are independent concerns, not competing for the same funds. No change needed.

test/GluonPaymentRouter.t.sol (1)

8-24: MockToken implementation is adequate for testing.

The mock correctly implements the minimal IERC20 interface needed for testing. The transfer function relies on Solidity 0.8.0+ automatic underflow protection, which will revert if the sender has insufficient balance.

Comment thread gluon.json
Comment on lines +145 to +155
"name": "NEUTRON_TOKEN",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract Tokeon"
}
],
"stateMutability": "view"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in internal type definition.

Line 151 specifies "internalType": "contract Tokeon" which appears to be a typo. Should this be "contract Token" or match an actual contract name?

The same issue appears at line 190 for PROTON_TOKEN.

📝 Suggested fix
       "name": "",
       "type": "address",
-      "internalType": "contract Tokeon"
+      "internalType": "contract Token"
     }

Apply the same fix at line 190.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"name": "NEUTRON_TOKEN",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract Tokeon"
}
],
"stateMutability": "view"
},
"name": "NEUTRON_TOKEN",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract Token"
}
],
"stateMutability": "view"
},
🤖 Prompt for AI Agents
In @gluon.json around lines 145 - 155, The ABI entries for NEUTRON_TOKEN and
PROTON_TOKEN contain a typo in the internalType ("contract Tokeon"); update
these internalType fields to the correct contract name (e.g., "contract Token"
or the actual deployed contract identifier) so they match the real contract type
used by your codebase, ensuring both the NEUTRON_TOKEN and PROTON_TOKEN ABI
objects are corrected.

Comment thread src/GluonPaymentRouter.sol
Comment thread src/GluonPaymentRouter.sol
Comment thread src/GluonPaymentRouter.sol Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/GluonPaymentRouter.sol:
- Around line 38-56: The payWithFission function lacks reentrancy protection:
import OpenZeppelin's ReentrancyGuard, have the contract inherit
ReentrancyGuard, and add the nonReentrant modifier to payWithFission so the
sequence calling gluon.fission(...) and the external transfers
neutron.transfer(...) and proton.transfer(...) are protected against reentrant
callbacks.
- Around line 38-56: The payWithFission function lacks slippage protection; add
minimum-amount checks by extending the signature to accept uint256 minNeutrons
and uint256 minProtons (or a uint256[2] calldata minAmounts) and after calling
gluon.fission and computing nBal and pBal, require(nBal >= minNeutrons) and
require(pBal >= minProtons) (with clear revert messages) so the tx reverts if
outputs are below user-specified minima; update callers/tests and any function
docs to pass the new params and ensure gas-stable calldata usage.
- Around line 44-45: The current nBal/pBal assignments use
neutron.balanceOf(address(this)) and proton.balanceOf(address(this)) which
capture the contract’s entire token holdings (not just tokens minted by
fission); to fix, record pre-fission balances (e.g., preN =
neutron.balanceOf(address(this)), preP = proton.balanceOf(address(this))) before
calling fission, then after fission compute mintedN =
neutron.balanceOf(address(this)) - preN and mintedP =
proton.balanceOf(address(this)) - preP, and use mintedN/mintedP for all
merchant/payer distributions and checks inside fission handler (update
references to nBal/pBal to use these minted variables and add guards if minted
amounts are zero or less than expected).
🧹 Nitpick comments (6)
src/GluonPaymentRouter.sol (6)

2-2: Consider narrowing the Solidity version pragma.

Using ^0.8.0 allows any 0.8.x version, which can lead to inconsistent compilation and potential issues from compiler-specific bugs. Consider using a narrower range like ^0.8.20 or pinning to a specific version for production contracts.

🔧 Suggested fix
-pragma solidity ^0.8.0;
+pragma solidity ^0.8.20;

22-24: Consider making state variables immutable.

The gluon, neutron, and proton variables are assigned once in the constructor and never modified. Marking them as immutable would save gas on reads and make the intent clearer.

⚡ Proposed optimization
-    IGluon public gluon;
-    IERC20 public neutron;
-    IERC20 public proton;
+    IGluon public immutable gluon;
+    IERC20 public immutable neutron;
+    IERC20 public immutable proton;

26-31: Add validation for token addresses returned from Gluon.

While the constructor validates the _gluon address, it doesn't verify that NEUTRON_TOKEN() and PROTON_TOKEN() return non-zero addresses. If these return address(0), the contract would deploy successfully but fail at runtime during token transfers.

🛡️ Proposed validation
     constructor(address _gluon) {
         require(_gluon != address(0), "Gluon: zero address");
         gluon = IGluon(_gluon);
         neutron = IERC20(gluon.NEUTRON_TOKEN());
         proton = IERC20(gluon.PROTON_TOKEN());
+        require(address(neutron) != address(0), "Neutron: zero address");
+        require(address(proton) != address(0), "Proton: zero address");
     }

38-56: Add events for payment tracking.

The function doesn't emit any events, making it difficult to track payments off-chain or build analytics. Events are essential for monitoring, debugging, and integrating with front-ends.

📡 Proposed addition

Add the event declaration after the state variables:

     IERC20 public proton;
+    
+    event PaymentProcessed(
+        address indexed payer,
+        address indexed merchant,
+        uint256 valueIn,
+        uint256 neutronAmount,
+        uint256 protonAmount
+    );
 
     constructor(address _gluon) {

Then emit the event at the end of the function:

         // 4. Return Protons to Payer (User)
         if (pBal > 0) {
             proton.transfer(msg.sender, pBal);
         }
+        
+        emit PaymentProcessed(msg.sender, merchant, msg.value, nBal, pBal);
     }

59-59: Consider implications of ETH accumulation.

The receive function allows the contract to accept ETH. While this may be necessary for refunds or certain operations, there's no mechanism to withdraw accumulated ETH if it gets stuck. Consider whether this contract should accumulate ETH and if so, add an admin function to recover stuck funds.


49-49: Use SafeERC20 or explicitly check return values for transfer calls.

The transfer calls on lines 49 and 54 don't check return values. While Neutron and Proton tokens (Tokeon contracts extending OpenZeppelin's ERC20) revert on failure and won't silently fail, following best practices and being defensive is still valuable. Consider using the SafeERC20 library (which is already imported in StableCoin.sol, suggesting team familiarity):

Recommended: Use SafeERC20

Add this to the top of the file:

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

Then apply this diff:

+using SafeERC20 for IERC20;
+
 function payWithFission(address merchant, bytes[] calldata updateData) external payable {
     require(merchant != address(0), "payWithFission: merchant is zero address");
     // 1. Perform Fission, minting both tokens to this contract
     gluon.fission{value: msg.value}(msg.value, address(this), updateData);
 
     // 2. Determine amounts minted
     uint256 nBal = neutron.balanceOf(address(this));
     uint256 pBal = proton.balanceOf(address(this));
 
     // 3. Forward Neutrons to Merchant
     if (nBal > 0) {
-        neutron.transfer(merchant, nBal);
+        neutron.safeTransfer(merchant, nBal);
     }
 
     // 4. Return Protons to Payer (User)
     if (pBal > 0) {
-        proton.transfer(msg.sender, pBal);
+        proton.safeTransfer(msg.sender, pBal);
     }
 }

Alternatively, explicitly check return values with require(...).

Also applies to: 54-54

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43af077 and f22e021.

📒 Files selected for processing (1)
  • src/GluonPaymentRouter.sol

Comment thread src/GluonPaymentRouter.sol Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/GluonPaymentRouter.sol:
- Around line 4-7: The IERC20 interface is used but transfer() is called
directly (e.g., in the payment/withdraw paths) which can silently fail for
non-reverting tokens; replace direct ERC20 transfers with OpenZeppelin's
SafeERC20 (import SafeERC20 and using SafeERC20 for IERC20) or at minimum check
the boolean return of IERC20.transfer and revert on false so that calls from
functions like the payment/withdraw functions fail loudly instead of succeeding
with no token movement.
- Around line 38-64: The payWithFission function performs external calls to
gluon.fission and token transfers (gluon.fission, neutron.transfer,
proton.transfer) and needs reentrancy protection; import OpenZeppelin's
ReentrancyGuard, have the contract inherit ReentrancyGuard, and add the
nonReentrant modifier to payWithFission to prevent callbacks during those
external calls; ensure the OZ import matches your pragma and that any
constructor chaining still compiles after adding the inheritance.
- Around line 38-64: Add slippage protection to payWithFission by extending its
signature to accept uint256 minNeutrons and uint256 minProtons, then after
computing mintedNeutrons and mintedProtons require mintedNeutrons >= minNeutrons
and mintedProtons >= minProtons (use clear revert messages like "payWithFission:
neutrons below min" / "payWithFission: protons below min"); keep existing
transfer logic but only perform transfers after the min checks pass; ensure
callers update to pass desired minimums and update any internal calls/tests that
invoke payWithFission.
🧹 Nitpick comments (3)
src/GluonPaymentRouter.sol (3)

26-31: Validate token addresses are non-zero.

The constructor validates the Gluon address but doesn't verify that NEUTRON_TOKEN() and PROTON_TOKEN() return non-zero addresses. If the Gluon contract is misconfigured or returns address(0), the router would be deployed in a broken state.

✅ Proposed fix
 constructor(address _gluon) {
     require(_gluon != address(0), "Gluon: zero address");
     gluon = IGluon(_gluon);
-    neutron = IERC20(gluon.NEUTRON_TOKEN());
-    proton = IERC20(gluon.PROTON_TOKEN());
+    address neutronAddr = gluon.NEUTRON_TOKEN();
+    address protonAddr = gluon.PROTON_TOKEN();
+    require(neutronAddr != address(0) && protonAddr != address(0), "Invalid token addresses");
+    neutron = IERC20(neutronAddr);
+    proton = IERC20(protonAddr);
 }

38-64: Emit an event for payment tracking.

No event is emitted when a payment is processed. Events are crucial for:

  • Off-chain indexing and tracking of payments
  • Merchant reconciliation
  • User transaction history
  • Protocol analytics
📢 Proposed fix: Add payment event

Define the event at the contract level:

event PaymentProcessed(
    address indexed payer,
    address indexed merchant,
    uint256 amountIn,
    uint256 neutronsToMerchant,
    uint256 protonsToPayer
);

Emit it at the end of the function:

     if (mintedProtons > 0) {
         proton.transfer(msg.sender, mintedProtons);
     }
+
+    emit PaymentProcessed(msg.sender, merchant, msg.value, mintedNeutrons, mintedProtons);
 }

67-67: Consider removing or adding withdrawal mechanism.

The receive() function allows the contract to accept ETH, but there's no mechanism to withdraw ETH that might get stuck in the contract. Since payWithFission forwards all msg.value to the Fission operation, it's unclear when this function would be needed.

Consider either:

  1. Removing the receive() function if it's not needed, or
  2. Adding an admin withdrawal function to recover stuck ETH
Example withdrawal function
address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function withdrawStuckETH() external onlyOwner {
    payable(owner).transfer(address(this).balance);
}

Note: You'd need to set owner in the constructor and consider using Ownable from OpenZeppelin.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f22e021 and 014e55a.

📒 Files selected for processing (1)
  • src/GluonPaymentRouter.sol
🔇 Additional comments (2)
src/GluonPaymentRouter.sol (2)

42-50: Well done on the balance tracking fix!

The pre-balance recording (lines 42-43) and minted amount calculation (lines 49-50) correctly isolate newly minted tokens from any existing balance the contract might hold. This addresses the critical bug mentioned in the commit message where the check previously considered all tokens in the contract.

The subtraction is safe from underflow due to Solidity 0.8.0's built-in overflow/underflow protection.


46-46: Clarify BASE_TOKEN sourcing when msg.value is passed as amountIn.

The fission implementation in StableCoin correctly separates oracle fee handling (paid from msg.value) from the amountIn transfer. However, payWithFission passes msg.value as the amountIn parameter while expecting BASE_TOKEN.safeTransferFrom() to transfer the full amount from GluonPaymentRouter. This requires clarification:

  • If BASE_TOKEN is WETH, the contract must wrap the ETH (minus any oracle fees) to WETH before calling fission, or
  • GluonPaymentRouter must be pre-funded with sufficient BASE_TOKEN balance equal to msg.value.

The current code does not show either mechanism, which could cause the safeTransferFrom call to revert due to insufficient balance.

Comment on lines +4 to +7
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Consider using SafeERC20 or checking transfer return values.

The minimal IERC20 interface is defined correctly, but some ERC20 tokens (e.g., USDT on certain chains) return false on failure instead of reverting. Since lines 57 and 62 call transfer() without checking the return value, silent failures could occur where tokens aren't actually transferred but the transaction succeeds.

🔐 Recommended fix: Use OpenZeppelin's SafeERC20

Add the SafeERC20 import at the top of the file:

+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+
 // SPDX-License-Identifier: MIT
 pragma solidity ^0.8.0;

Then update the contract to use SafeERC20:

 contract GluonPaymentRouter {
+    using SafeERC20 for IERC20;
+
     IGluon public gluon;
     IERC20 public neutron;

This will automatically revert if transfers fail, preventing silent failures.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/GluonPaymentRouter.sol around lines 4 - 7, The IERC20 interface is used
but transfer() is called directly (e.g., in the payment/withdraw paths) which
can silently fail for non-reverting tokens; replace direct ERC20 transfers with
OpenZeppelin's SafeERC20 (import SafeERC20 and using SafeERC20 for IERC20) or at
minimum check the boolean return of IERC20.transfer and revert on false so that
calls from functions like the payment/withdraw functions fail loudly instead of
succeeding with no token movement.

Comment on lines +38 to +64
function payWithFission(address merchant, bytes[] calldata updateData) external payable {
require(merchant != address(0), "payWithFission: merchant is zero address");

// 1. Record pre-fission balances to isolate newly minted tokens
uint256 preNeutronBal = neutron.balanceOf(address(this));
uint256 preProtonBal = proton.balanceOf(address(this));

// 2. Perform Fission, minting both tokens to this contract
gluon.fission{value: msg.value}(msg.value, address(this), updateData);

// 3. Calculate only the newly minted amounts
uint256 mintedNeutrons = neutron.balanceOf(address(this)) - preNeutronBal;
uint256 mintedProtons = proton.balanceOf(address(this)) - preProtonBal;

// 4. Verify fission produced tokens
require(mintedNeutrons > 0 || mintedProtons > 0, "payWithFission: no tokens minted");

// 5. Forward Neutrons to Merchant
if (mintedNeutrons > 0) {
neutron.transfer(merchant, mintedNeutrons);
}

// 6. Return Protons to Payer (User)
if (mintedProtons > 0) {
proton.transfer(msg.sender, mintedProtons);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add reentrancy protection.

The function makes multiple external calls (line 46 to gluon.fission, lines 57 and 62 to transfer) without reentrancy protection. A malicious Gluon contract or token contract could call back into payWithFission before the first call completes, potentially manipulating balances or causing unexpected behavior.

🔒 Recommended fix: Add ReentrancyGuard

Import OpenZeppelin's ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

Update the contract:

-contract GluonPaymentRouter {
+contract GluonPaymentRouter is ReentrancyGuard {
     IGluon public gluon;
     IERC20 public neutron;
     IERC20 public proton;

Add the modifier to the function:

-function payWithFission(address merchant, bytes[] calldata updateData) external payable {
+function payWithFission(address merchant, bytes[] calldata updateData) external payable nonReentrant {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/GluonPaymentRouter.sol around lines 38 - 64, The payWithFission function
performs external calls to gluon.fission and token transfers (gluon.fission,
neutron.transfer, proton.transfer) and needs reentrancy protection; import
OpenZeppelin's ReentrancyGuard, have the contract inherit ReentrancyGuard, and
add the nonReentrant modifier to payWithFission to prevent callbacks during
those external calls; ensure the OZ import matches your pragma and that any
constructor chaining still compiles after adding the inheritance.

⚠️ Potential issue | 🟠 Major

Add slippage protection.

Users have no way to specify minimum expected amounts for Neutrons or Protons. This exposes them to:

  • Front-running attacks where an attacker manipulates the Fission output
  • Unfavorable price movements between transaction submission and execution
  • MEV extraction by block proposers
🛡️ Proposed fix: Add minimum amount parameters
 function payWithFission(
     address merchant,
+    uint256 minNeutrons,
+    uint256 minProtons,
     bytes[] calldata updateData
 ) external payable {
     require(merchant != address(0), "payWithFission: merchant is zero address");
     
     // ... existing balance recording and fission call ...
     
     uint256 mintedNeutrons = neutron.balanceOf(address(this)) - preNeutronBal;
     uint256 mintedProtons = proton.balanceOf(address(this)) - preProtonBal;
     
+    require(mintedNeutrons >= minNeutrons, "Insufficient Neutrons minted");
+    require(mintedProtons >= minProtons, "Insufficient Protons minted");
+    
-    require(mintedNeutrons > 0 || mintedProtons > 0, "payWithFission: no tokens minted");
🤖 Prompt for AI Agents
In @src/GluonPaymentRouter.sol around lines 38 - 64, Add slippage protection to
payWithFission by extending its signature to accept uint256 minNeutrons and
uint256 minProtons, then after computing mintedNeutrons and mintedProtons
require mintedNeutrons >= minNeutrons and mintedProtons >= minProtons (use clear
revert messages like "payWithFission: neutrons below min" / "payWithFission:
protons below min"); keep existing transfer logic but only perform transfers
after the min checks pass; ensure callers update to pass desired minimums and
update any internal calls/tests that invoke payWithFission.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant