Added router for fission stablepay integration#9
Conversation
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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
minNeutronOutandminProtonOutand 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
amountInormsg.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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
gluon.jsonsrc/GluonPaymentRouter.soltest/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
transferandbalanceOf, 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 viapythFeecalculation; any excess is refundedamountInis transferred separately as a BASE_TOKEN viasafeTransferFrom, not from msg.valueThese 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
transferfunction relies on Solidity 0.8.0+ automatic underflow protection, which will revert if the sender has insufficient balance.
| "name": "NEUTRON_TOKEN", | ||
| "inputs": [], | ||
| "outputs": [ | ||
| { | ||
| "name": "", | ||
| "type": "address", | ||
| "internalType": "contract Tokeon" | ||
| } | ||
| ], | ||
| "stateMutability": "view" | ||
| }, |
There was a problem hiding this comment.
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.
| "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.
There was a problem hiding this comment.
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.0allows 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.20or 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, andprotonvariables are assigned once in the constructor and never modified. Marking them asimmutablewould 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
_gluonaddress, it doesn't verify thatNEUTRON_TOKEN()andPROTON_TOKEN()return non-zero addresses. If these returnaddress(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
receivefunction 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
transfercalls 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
There was a problem hiding this comment.
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()andPROTON_TOKEN()return non-zero addresses. If the Gluon contract is misconfigured or returnsaddress(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. SincepayWithFissionforwards allmsg.valueto the Fission operation, it's unclear when this function would be needed.Consider either:
- Removing the
receive()function if it's not needed, or- 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
ownerin the constructor and consider using Ownable from OpenZeppelin.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 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 whenmsg.valueis passed asamountIn.The
fissionimplementation in StableCoin correctly separates oracle fee handling (paid frommsg.value) from theamountIntransfer. However,payWithFissionpassesmsg.valueas theamountInparameter while expectingBASE_TOKEN.safeTransferFrom()to transfer the full amount from GluonPaymentRouter. This requires clarification:
- If
BASE_TOKENis WETH, the contract must wrap the ETH (minus any oracle fees) to WETH before callingfission, or- GluonPaymentRouter must be pre-funded with sufficient
BASE_TOKENbalance equal tomsg.value.The current code does not show either mechanism, which could cause the
safeTransferFromcall to revert due to insufficient balance.
| interface IERC20 { | ||
| function transfer(address to, uint256 amount) external returns (bool); | ||
| function balanceOf(address account) external view returns (uint256); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
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.
I created a new helper smart contract GluonPaymentRouter.sol in src.
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.