OKX - Audit 3

CertiKFiles named in the report

CertiK reviewed OKX (OKX - Audit 3) on 16 May 2023: 27 findings, 0 critical; 5 marked fixed, 22 open.

No open critical findings
Open the reportOpen OKX’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings27

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

SeverityOpenAcknowledgedFixed
Critical000
High100
Medium000
Low2105
HighCentralization Related RisksOpen

In the contract `Entrance` the role `_owner` has authority over the functions shown in the diagram below. Any compromise to the `_owner` account may allow the hacker to take advantage of this authority and configure key parameters and manipulate the project. ![](https://accelerator-tasks-prod.acc.corp.certik.com/74536283dfda4244a8dd9f655ff2839b/diagrams/centralization_Entrance-Entrance-_owner.svg) In the contract `Entrance` the role `operator` has authority over the functions shown in the diagram below. Any compromise to the `operator` account may allow the hacker to take advantage of this authority and manipulate the project. ![](https://accelerator-tasks-prod.acc.corp.certik.com/74536283dfda4244a8dd9f655ff2839b/diagrams/centralization_Entrance-Entrance-operator.svg) In addition, the contract `Entrance` is an upgradeable contract, meaning the owner can upgrade the contract without the community's commitment. If an attacker compromises the account, the attacker can change the implementation of the contract and drain tokens from the contract. ----- In the contract `DexRouter` the role `owner` has authority over the following functions: - setApproveProxy() to set approveProxy - setWNativeRelayer() to set the wNativeRelayer - setXBridge() to set the xBridge - initializePMMRouter() to initialize the PMM Router - setPMMFeeConfig() to set the fee rate and receiver of the PMM Router In addition, the role `xBridge` has authority over the following functions: - smartSwapByOrderIdByXBridge() to perform the token swaps with the smartswap - unxswapByOrderIdByXBridge() to perform the token swaps with unxswap - uniswapV3SwapToByXBridge() to perform the token swaps with uniswapV3 - PMMV2SwapByXBridge() to perform the token swaps with PMMV2Swap In addition, the contract `DexRouter` is an upgradeable contract, meaning the owner can upgrade the contract without the community's commitment. If an attacker compromises the account, the attacker can change the implementation of the contract and drain tokens from the contract. ----- In the contract `OkxNFTMarketAggregator` the role `owner` has authority over the following functions: - approveERC20(() to approve the ERC20 tokens to the operator - setMarketRegistry() to set the marketRegistry

LowAllowed Markets in Function `tradeV2()`Open

The function `tradeV2()` allows the users to trade NFTs with specified markets. The comment in line 312 indicates only the market `wyvern` (id is 4 based on the implementation) is allowed to be used in `tradeV2()`, but the implementation in line 313: ```solidity=313 if(tradeDetails[i].marketId != 0 && tradeDetails[i].marketId !=4 && tradeDetails[i].marketId < 7) { ``` also allows the market with id 0.

LowChoice of Token for Parameter `fromToken` For HopOpen

In the function `_exeHop()`, the first hop executed swaps `batchAmount` of `hops[0].fromToken`. It should be the case that `hops[0].fromToken` is the same address as `_baseRequest.fromToken` in `_smartSwapInternal()`, but there are no checks to guarantee this.

LowConditional Check on Message Value Should Be UncoOpen

The function `_transferTokens()` has a check to see if `msg.value` matches the amount of native tokens that have been transferred to other addresses. ```solidity=382 if (msg.value != 0) {require(msg.value == nativeAmount, MetaXInvestErrors.QUANTITY_MISMATCH);} ``` However, this check is only performed if `msg.value != 0`, when it should be always done. If `msg.value == 0` but `nativeAmount > 0`, it is possible for the contract to lose native currency that was stored within it before the current `msg.sender` interacted with it. Since this contract has a `receive()` function, it is expected that it will hold native currency, which can be lost due to the above.

LowFunction State Mutability Can Be Restricted to PuOpen

The functions `bytesToAddress()` and `bytesToBytes4()` do not read any state variable so they can be restricted to pure. In addition, the function `verifyWyvern()` does not modify any state variable, so it can be changed to view.

LowInaccurate Require StatementOpen

The function `shareToAmount()` is used to calculate the withdrawal amounts with the specified LP tokens. After the update in the total supply of the LP, the following check is executed: ```solidity=156 require(totalSupply > amount, MetaXInvestErrors.NOT_ENOUGH); ```` According to UniswapV2 design, `MINIMUM_LIQUIDITY = 10**3` LP tokens have been locked. Therefore, a more precise statement is as follows: ```solidity require(totalSupply > amount + MINIMUM_LIQUIDITY, MetaXInvestErrors.NOT_ENOUGH); ```

LowInconsistent Handling of Other MarketsOpen

In the two functions labeled `trade()`, there is a check on the provided `tradeData` that the `orderToAddress` is the `msg.sender`. However, this check is done for some of the markets instead of all of them.

LowMissing Validation of Token Order in Function `wiOpen

In the implementation of Uniswap V2, the function `IUniswapV2Pair(pool).getReserves()` retrieves the reserves of tokens in which the first reserve corresponds to the token0 and the second one is the token1. The token0 and token1 are recorded according to the rule that address(token0) < address(token1). In the function `withdraw()` and `_withdraw()`, the passed `tokensOut` does not validate whether the tokens are in the order that `address(tokensOut[0].token) < address(tokensOut[1].token)`. If one of them is a zero address, it needs to be converted to the weth address first. ```solidity=50 function withdraw( address pool, TokenOutInfo[] calldata tokensOut, bytes calldata data ) external override { // Check Length require(tokensOut.length == 2, 'Length Wrong'); // Decode feeRate+mode from data // mod=2 for withdraw two tokens;mod=0 for withdraw token0;mod=1 for withdraw token1 (uint256 feeRate, uint256 mode) = _getFeeRate(pool, data); // Read Status uint256 tokenAmount = _balanceOf(pool, address(this)); // Transfer to Pool _transfer(pool, pool, tokenAmount); // remove liquidity (uint256 amount0, uint256 amount1) = IUniswapV2Pair(pool).burn(address(this)); _withdraw(pool, feeRate, mode, tokensOut, amount0, amount1); // Event emit Withdraw(pool, tokenAmount); } ``` In this case, it will revert or transfer the wrong token amounts if the tokens are flipped.

LowMissing Validation of Tokens in Function `amountTOpen

The function `amountToShare()` is used to calculate the liquidity given the amounts of two tokens. However, there is no check that the passed `tokenAmounts` has length 2 and the token order matches with the tokens in the underlying pool that has the requirement `address(token0) < address(token1)`. In the case that one of them is the eth, it needs to be converted to the weth first.

LowMissing Validation That Sum of `batchesAmount` IsOpen

In the function `smartSwapByInvest()`, the `fromToken` balance of the contract is redistributed proportionally with the ratio `batchesAmount[i] / baseRequest.fromTokenAmount` as shown in line 447-449. ```solidity=446 uint256[] memory newBatchesAmount = new uint256[](batchesAmount.length); for (uint256 i = 0; i < batchesAmount.length; ++i) { newBatchesAmount[i] = batchesAmount[i] * amount / baseRequest.fromTokenAmount; } ``` That means the sum of `batchesAmount[i]` should be the same as `baseRequest.fromTokenAmount`, but there is no such validation in the current implementation of `smartSwapByInvest()`.

Show 10 more
LowMissing Zero Address ValidationOpen

Addresses should be checked before assignment to ensure they are not zero addresses.

LowOverwriting FailureOpen

In the function `tradeV2()`, low-level calls are performed in three places: 1. `beforeExecute()`, which transfers tokens from `msg.sender` to `address(this)` 2. During a call to `SEAPORT` 3. `afterExecute()`, which transfers tokens from `address(this)` to `msg.sender` The only way for the process to revert is during 2) if `isAtomic == true` and the call to `SEAPORT` fails and returns data. Hence, it is possible for 1) or 2) to fail but 3) succeeds. Note that 3) returns `true` even if no low-level calls are made, for example, using an unsupported `actionType` will have 3) return `true`.

LowPossible Missing Functionality From `BaseAdapter`Open

The `UniswapV2AdapterMain` contract inherits the `BaseAdapter` abstract contract which contains various functions that perform no operations. `UniswapV2AdapterMain` has overwritten some of the functions but not all of them, namely `unstake()`, `unStakeAndWithdraw()`, and `claimReward()`.

LowPossibly Incorrect feeRate in Contract UniswapV2AOpen

The standard fee charged on the UniswapV2 pool is 3 out of 1000 deducted from the tokens to be swapped. In the contract `UniswapV2AdapterMain`, a custom fee rate is used to calculate `getAmountOut()`, which could result in a different value if the underlying pool uses the standard fee. **Yield-External-Audit/contracts/libraries/MetaXMath.sol** ```solidity=74 function getAmountOut( uint256 amountIn, uint256 reserveIn, uint256 reserveOut, uint256 feeInPrecision, uint256 precision ) internal pure returns (uint256 amountOut) { require( reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY" ); if (amountIn > 0) { uint256 amountInWithFee = amountIn.mul(feeInPrecision); uint256 numerator = amountInWithFee.mul(reserveOut); uint256 denominator = reserveIn.mul(precision).add(amountInWithFee); amountOut = numerator / denominator; } } ```

LowReserves Are Used In Calculation of Liquidity RemOpen

The function `shareToAmount()` is used to calculate the withdrawal amounts with the specified LP tokens. In the calculation of the withdrawal amounts of token0 and token1, the reserves of the pool are used instead of the balances of the pool. ```solidity=152 (uint256 r0, uint256 r1, ) = IUniswapV2Pair(pool).getReserves(); // TotalSupply uint256 totalSupply = _feeLp(r0,r1,pool); require(totalSupply > amount, MetaXInvestErrors.NOT_ENOUGH); uint256 amount0 = r0 * amount / totalSupply; uint256 amount1 = r1 * amount / totalSupply; r0 = r0 - amount0; r1 = r1 - amount1; ... ``` However, in the implementation of the function `burn()` in the UniswapV2, the balances in the pool are used to ensure pro-rata distribution. ```solidity function burn(address to) external lock returns (uint amount0, uint amount1) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings address _token0 = token0; // gas savings address _token1 = token1; // gas savings uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); uint liquidity = balanceOf[address(this)]; bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution ... ```

LowTypo in Variable NameOpen

The variable `reserves` passed in the function `_exeForks()` seems to be `reverse` or `reversed` based on the following observation: - `reserves` is assigned with the value `and(rawData, _REVERSE_MASK)`, as the constant name `_REVERSE_MASK` indicates; - when `reserves` is true, it invokes the `IAdapter(path.mixAdapters[i]).sellQuote()` to sell the token1 in the pool pair; Otherwise, it calls `IAdapter(path.mixAdapters[i]).sellBase()` to sell the token0 in the pool pair. The second point implies that it sells the token1 if reversed and it sells the token0 if not reversed.

LowUnchecked ERC-20 `transfer()`/`transferFrom()` CaOpen

The return value of the transfer()/transferFrom() call is not checked. ```solidity=352 weth.transfer(to, amount); ```

LowUnlocked Compiler VersionOpen

The contracts cited have an 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 differing compiler version numbers. This can lead to 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.

LowUnnecessary Receive FunctionOpen

All contracts in the auditing scope contain a `receive()` function that accepts direct transfers of the native token. However, there is currently no purpose for the contracts to hold native tokens and it is possible for users to take these tokens. Each contract contains mechanisms for a user to take any native tokens held by the contract: - `DexRouter` performs arbitrary swaps and performs no checks on `msg.value`, allowing a user to swap native tokens inside the contract; - `OkxNFTMarketAggregator` returns unused ETH, but performs no checks on `msg.value`, allowing a user to have native tokens inside the contract be viewed as unused ETH; - `Entrance` transfers tokens to arbitrary addresses in `_transferToken()` and the `msg.value` check in `_transferTokens()` can be bypassed if `msg.value == 0`, allowing the transfer of native tokens held inside the contract; - `UniswapV2AdapterMain` expects users to transfer tokens to the contract and deposit them, allowing users to deposit native tokens already held inside the contract. If the transfer of native tokens are meant to be used with a `payable` function, then it would be better to ensure `msg.value` is correct instead of having a `receive()` function.

LowUnprotected InitializerOpen

One or more logic contracts do not protect their initializers. An attacker can call the initializer and assume ownership of the logic contract, whereby she can perform privileged operations that trick unsuspecting users into believing that she is the owner of the upgradeable contract. ```solidity=18 contract Entrance is InvestBase, OwnableUpgradeable, ReentrancyGuardUpgradeable { ``` - `Entrance` is an upgradeable contract that does not protect its initializer. ```solidity=73 function initialize(IWETH _weth) public initializer { ``` - `initialize` is an unprotected initializer function.

Show 2 more
LowUnused EventOpen

The event `SetIfCheck(bool ifCheck)` is declared in the contract `Entrance`, but it has never been used in the codebase.

LowUse of `delegatecall`Open

Anyone is able to use the functions `trade()` and `tradeV2()`, which may make a `delegatecall` to `proxy` with data `tradeData`. ```solidity=191 (bool success, ) = isLib ? proxy.delegatecall(tradeData) : proxy.call{value: ethValue}(tradeData); ``` As `tradeData` is user defined, if `proxy` has functions that can change state variables, state variables of `OkxNFTMarketAggregator`, such as `_owner`, may be changed.

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 (4)

  • contracts/adapters/uniswap/UniswapV2AdapterMain.sol
  • contracts/Aggregator/OkxNFTMarketAggregator.sol
  • contracts/Entrance.sol
  • contracts/8/DexRouter.sol

Part of OKX’s security record

OKX
Audits5 audits by 1 firmLast audit May 2024, by CertiK.
Critical findingsNo open critical findingsAcross the 5 reports, 1 critical finding was reported. It is marked fixed by the firm that reported it.
IncidentIncident 12 Dec 2023Source: De.Fi incident database.
Open OKX’s security record

Security firm

CertiK

1,044 projects reviewed · 1,375 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