Numoen

CertiKFiles named in the report

CertiK reviewed Numoen on 7 Dec 2022: 8 findings, 1 critical; 4 marked fixed, 4 open.

1 open critical finding
Open the reportOpen Numoen’s security record

Report hosted on skynet.certik.com.

Statuses are the security firm’s own, from this report. Trustblock does not review code.

Findings8

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical100
High000
Medium000
Low304
CriticalCurve Model Could Lead to ArbitrageNot fixed

In the pair contract, swapping tokens/mint(add liquidity)/burn(remove liquidity) operations need to check the invariant to be consistent. In the following is a code snippet that goes through the current implementation of the `verifyInvariant()` function. ```solidity if (!verifyInvariant(balance0, balance1, liquidity + totalSupply)) revert InvariantError(); ``` ```solidity function verifyInvariant( uint256 r0, uint256 r1, uint256 shares ) public view returns (bool valid) { uint256 scale0 = PRBMathUD60x18.div(PRBMathUD60x18.div(r0, shares), 10**baseScaleFactor); uint256 scale1 = PRBMathUD60x18.div(PRBMathUD60x18.div(r1, shares), 10**speculativeScaleFactor); uint256 a = scale0; uint256 b = PRBMathUD60x18.mul(scale1*, upperBound); uint256 c = PRBMathUD60x18.powu(scale1, 2) / 4; uint256 d = PRBMathUD60x18.powu(upperBound, 2); if (scale1 > 2 * upperBound) revert SpeculativeInvariantError(); return a + b == c + d; } ``` The above code implements the following the following equation: $$a + b = c + d$$ The above equation can be simplified as following steps: Step 1: $$scale0 + scale1*upperBound = \frac{scale1^2}{4} + upperBound^2$$ Step 2: $$scale0 = \frac{scale1^2}{4} - scale1*upperBound + upperBound^2$$ Step 3: $$scale0 = (\frac{scale1}{2} - upperBound)^2$$ The above formula will be part of the curve between [0, 2*upperBound]. However, this model could lead to potential economic attacks. This is because when adding liquidity or removing liquidity, users can adjust the expected output amount of base tokens or speculative tokens to gain more profit. In the following example, an attacker can mint 1 ETH of liquidity and burn 1 ETH of liquidity to gain profits because he can withdraw more tokens from the liquidity than he would normally be allowed. **Proof of concept** 1. Initially, 1 ETH of liquidity is added to the 16 ETH base tokens and 2 speculative tokens. The assumed price of speculative tokens 8. 2. The attacker can add another 1 ETH of liquidity to the collection of 16 ETH base tokens and 2 speculative tokens. 3. Attacker burns his 1 ETH of liquidity along with 9 ETH base tokens and 4 speculative tokens. 4. Since the price of speculative tokens is 8, the attacker actually gains a profit of 7 additional base tokens. ```solidity function testAttackOnCurveModel() public { // Initially, there is 1 liquidity with 16 base tokens and 2 speculative tokens // Suppose current market price is 1:8 _pairMint(16 ether, 2 ether, 1 ether, cuh); //The attacker added 1 liquidity another 16 base tokens and 2 speculative tokens _pairMint(16 ether, 2 ether, 1 ether, cuh); assertEq(pair.totalSupply(), 2 ether); uint baseTokenBalanceBefore = base.balanceOf(address(cuh)); uint speculativeTokenBalanceBefore = speculative.balanceOf(address(cuh)); //The attacker can actually withdraw much more tokens than he added as liquidity pair.burn(cuh, 9 ether , 4 ether , 1 ether); assertEq(pair.totalSupply(), 1 ether); uint baseTokenBalanceAfter = base.balanceOf(address(cuh)); uint speculativeTokenBalanceAfter = speculative.balanceOf(address(cuh)); assertEq(baseTokenBalanceAfter - baseTokenBalanceBefore, 9 ether); assertEq(speculativeTokenBalanceAfter - speculativeTokenBalanceBefore, 4 ether); } ``` **Result** The tests pass which means that the attack is successful based on the current model.\ [PASS] testAttackOnCurveModel() (gas: 272404) **[CertiK, 11/14/2022]:** \ This model does have fancy features and can restrict the boundary of the price. However, due to the decentralized feature of blockchain, those tokens trading/swapping with the pair contract may lead to inconsistency issues and thus cause arbitrage risk. Here are two main concerns regarding the curve model: 1. When the token is also available in AMM like UniSwap/PancakeSwap using xy=k as their curve model, the inconsistency with other AMMs could lead to arbitrage due to different models used (see the above example). 2. The curve itself is not consistent regarding the swapping process (see the following two testcases). ```solidity // case 1: swap 2 ether speculative tokens with 9 ether base tokens function testSwapOnCurveModel() public { // Initially, there is 1 liquidity with 16 base tokens and 2 speculative tokens _pairMint(16 ether, 2 ether, 1 ether, cuh); // To swap 2 ether speculative tokens out, 9 ether base token are required base.mint(cuh, 9 ether); vm.prank(cuh); base.transfer(address(pair),9 ether); uint speculativeBalanceBefore = speculative.balanceOf(address(cuh)); pair.swap(cuh, 0, 2 ether); uint speculativeBalanceAfter = speculative.balanceOf(address(cuh)); // In normal cases, assertEq(speculativeBalanceAfter - speculativeBalanceBefore, 2

LowPayment for minting tokensNot fixed

From the analysis of the contracts, the minting and burning of NRD tokens within the `Lendgine.sol` contract seem to be open to any user to access. No centralized roles have been put in place to control this part of the process. There seems to be a mechanism in place that controls payment in exchange for the minting of tokens. Within the `mint()` function, there is a call to the `msg.sender` which is supposed to be a contract address that contains the function `MintCallback()`. ```sol=136 IMintCallback(msg.sender).MintCallback(amountS, data); ``` Upon further investigation, the `mint()` function is actually calling an implementation of the interface `IMintCallback`. ```sol=9 function MintCallback(uint256 amount0, bytes calldata data) external; ```

LowPotential Front-Running RiskNot fixed

To deposit into the contract, users are supposed to call `mint()` to add liquidity. However, if the `mint()` and `deposit()` calls are invoked in separate transactions, it might lead to front-run attack. In this situation, an attacker can front run the original `deposit()` with his own address. Therefore, the original user might lose his liquidity forever.

LowUnlocked Compiler VersionNot fixed

The contract has unlocked compiler version. An unlocked compiler version in the source code of the contract permits the user to compile it at or above a particular version. This, in turn, leads to differences in the generated bytecode between compilations due to different compiler versions. This can lead to an ambiguity when debugging as compiler specific bugs may occur in the codebase that would be hard to identify over a span of multiple compiler versions rather than a specific one.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (14)

  • src/Pair.sol
  • src/libraries/SafeTransferLib.sol
  • src/Factory.sol
  • src/JumpRate.sol
  • src/libraries/LendgineAddress.sol
  • src/interfaces/ILendgine.sol
  • src/libraries/Position.sol
  • src/interfaces/IMintCallback.sol
  • src/libraries/LiquidityMath.sol
  • src/interfaces/IJumpRate.sol
  • src/interfaces/IFactory.sol
  • src/interfaces/IPair.sol
  • src/Lendgine.sol
  • src/ERC20.sol

Part of Numoen’s security record

Numoen
Audits1 audit by 1 firmLast audit Dec 2022, by CertiK.
Critical findings1 open critical findingReported in “Numoen” by CertiK, Dec 2022. Not marked fixed in that report.
Open Numoen’s security record

Security firm

CertiK

1,044 projects reviewed · 1,393 reports · latest May 2025

See CertiK’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us