Kromatika Finance

CertiKFiles named in the report

CertiK reviewed Kromatika Finance on 26 Feb 2024: 24 findings, 0 critical; 18 marked fixed, 6 open.

No open critical findings
Open the reportOpen Kromatika Finance’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings24

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

SeverityOpenAcknowledgedFixed
Critical000
High201
Medium002
Low4015
High`multicall` Cannot Parse Sender Forwarded From `GFixed

When `executeCall` from `GaslessForwarder` is used to call `multicall` on `MetaSwapRouter`, and `multicall` then delegates to another function like `swap`, the nested call (`swap`) cannot correctly parse the sender address, leading to potential issues. ### `GaslessForwarder` contract ```solidity function executeCall (){ ... (bool success, bytes memory result) = request.targetAddress.call{ value: msg.value }( abi.encodePacked(request.data, sender) ); ... } ``` The `executeCall` will append the actual sender's address to the end of the `calldata`. ### `MetaSwapRouter` contract: ```solidity function isTrustedForwarder(address forwarder) internal view returns(bool) { return trustedForwarders[forwarder]; } function _msgSender() internal virtual override view returns (address payable ret) { if (msg.data.length >= 24 && isTrustedForwarder(msg.sender)) { // At this point we know that the sender is a trusted forwarder, // so we trust that the last bytes of msg.data are the verified sender address. // extract sender address from the end of msg.data assembly { ret := shr(96,calldataload(sub(calldatasize(),20))) } } else { return msg.sender; } } function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { ... for (uint256 i = 0; i < data.length; i++) { (bool success, bytes memory result) = address(this).delegatecall(data[i]); ... } } function swap (...){ ... address payable sender = _msgSender(); ... } ``` When `executeCall` executes `multicall`, and `multicall` in turn executes something like `swap`, the `swap` function expects the sender's address to be at the end of its `calldata`. However, since the sender was appended to the entire `multicall` payload, it won't be where `swap` expects it. This mismatch can lead to reverts or potential exploits, as the `swap` function might interpret incorrect data as the `sender` address. Example Scenario 1. The `executeCall` function sends `calldata` (e.g., `bytes data[1] + sender`) to `multicall`. 2. `multicall` parses only `bytes data[1]` and delegates the call to `swap`, not considering the appended `sender`. 3. During the delegated call to `swap`, `isTrustedForwarder(msg.sender)` remains valid, and swap attempts to parse `sender` from `data[0]`. 4. This can lead to incorrect parsing of the `sender` address, causing reverts or potential exploits.

MediumFunction Calls User-Provided Addresses With No AcFixed

Calling a user provided address is dangerous, especially in a public function with no access control restriction. An attacker could deploy a malicious contract and use the vulnerable function to trigger a call to the malicious contract, potentially stealing user funds or causing other serious damages. File: GaslessInterfaceMulticall.sol ```solidity=29 function approveToken(address token, uint256 amount, address spender) public { IERC20(token).approve(spender, amount); } ``` Without access control, any address that interacts with the contract can call the `approveToken()` function and approve the spending of tokens from the contract's balance on behalf of any spender. This means that anyone, including potentially malicious actors, can gain control over the tokens held by the contract and transfer them to another address without the contract owner's explicit consent. --- File: GaslessSwapAdapter ```solidity=49 TransferHelper.safeApprove( address(data.tokenFrom), data.aggregator, approvalAmount ); ``` The `adapt()` function has no access control, anyone can this function to gain allowance of specified token from this contract. --- File: SwapAggregatorAdapter.sol ```solidity=40 TransferHelper.safeApprove( address(data.tokenFrom), data.aggregator, data.amountFrom ); ``` The `adapt()` function has no access control, anyone can this function to gain allowance of specified token from this contract. **The issue of `adapt()` function is limited since the `SwapAggregatorAdapter` contract is intended to be delegated called by the `FlashWallet` contract. The `SwapAggregatorAdapter` contract itself does not have any tokens.**

MediumPotentially More ETH To Be Swapped Than ExpectedFixed

In the `GaslessSwapAdapter` contract, there is an issue when swapping ETH (`fromToken`) for another token (`toToken`). Currently, the amount of ETH to be swapped is calculated based on the balance of the contract (`address(this)`) and the amount of ETH is sent by the user. However, this can result in more ETH being swapped than the user expects. If the user sends additional ETH during the gasless swap, all of it will be used for the token swap, without any possibility of refunding the excess amount. ```solidity=45 if (!LibERC20Adapter.isTokenETH(data.tokenFrom)) { approvalAmount = address(data.tokenFrom) == address(data.paymentToken) ? data.amountFrom.sub(data.paymentFees) : data.amountFrom; TransferHelper.safeApprove( address(data.tokenFrom), data.aggregator, approvalAmount ); } else { senderAmount = address(data.tokenFrom) == address(data.paymentToken) ? address(this).balance.sub(data.paymentFees) : address(this).balance; } ```

LowCheaper `require` Statements Should Be PerformedFixed

In the `TransferAdapter` contract, function `adapt()` performs a token transfer with a cheaper `require` statement placed after the transfer operation. ```solidity=33 uint256 amountToTransfer; if(LibERC20Adapter.isTokenETH(data.token)){ amountToTransfer = address(this).balance.sub(data.feesAmount); payable(address(data.receiver)).transfer(amountToTransfer); payable(data.validator).transfer(data.feesAmount); } else { amountToTransfer = data.amountFrom.sub(data.feesAmount); _transfer(data.token, payable(data.receiver), amountToTransfer); _transfer(data.token, payable(data.validator), data.feesAmount); } require(amountToTransfer > 0, 'AmountToTransfer is zero'); ```

LowDeflationary Tokens Not Supported for Gasless SwaFixed

In the `GaslessSwapAdapter` contract, it will pay payment fees to compensate validators after swapping is made. The payment token is either `tokenFrom` or `tokenTo` which is directly transferred from the flash wallet to validator. In the `GaslessSwapAdapter` contract, validators are compensated with payment fees after a swap is completed. The payment token used for compensation can either be `tokenFrom` or `tokenTo`, and it is directly transferred from the flash wallet to the validator. If the `paymentToken` is a regular ERC20 token without a transfer fee, then the `paymentFeesAfter` value is calculated as the sum of `paymentFeesBefore` and `paymentFees`. However, if the `paymentToken` is a deflationary token with a transfer fee, the `paymentFeesAfter` value will be less than the sum of `paymentFeesBefore` and `paymentFees`. It is important to note that the design requires the `paymentToken` to disallow transfer fees and not support deflationary tokens in the swap pair. ```solidity=73 uint256 paymentFeesBefore = LibERC20Adapter.getTokenBalanceOf( IERC20(data.paymentToken), data.validator ); _transfer(data.paymentToken, payable(data.validator), data.paymentFees); uint256 paymentFeesAfter = LibERC20Adapter.getTokenBalanceOf( IERC20(data.paymentToken), data.validator ); require(paymentFeesAfter.sub(data.paymentFees) >= paymentFeesBefore, 'GSA_FNM'); ```

LowInconsistent LogicFixed

There is an inconsistency of checking `amountToMin` between regular swap and Gasless swap. In the regular swap, the code verifies that the higher value between the amount of `tokenTo` tokens initially swapped and the actual amount received by the recipient satisfies the minimum requirement specified by `data.amountToMin`. ```solidity=64 require(recipientTokenToBalanceAfter >= recipientTokenToBalanceBefore, 'SAA_NEG'); uint256 diffRecipientToken = recipientTokenToBalanceAfter.sub(recipientTokenToBalanceBefore); outputTokenAmount = outputTokenAmount > diffRecipientToken ? outputTokenAmount : diffRecipientToken; require(outputTokenAmount >= data.amountToMin, 'SAA_MIN'); ``` However, in the Gasless swap, the code only verifies that the amount of `tokenTo` tokens transferred to the recipient after deducting the payment fee meets the `data.amountToMin` requirement. ```solidity require(_transfer( data.tokenTo, context.recipient, transferToBalance ) >= data.amountToMin, 'GSA_MIN'); ``` This inconsistency implies that the Gasless swap may not consider the actual amount received by the recipient but rather focuses on the transferred amount after deducting fees. This difference could impact the accuracy of verifying the minimum requirement in the Gasless swap compared to the regular swap.

LowInefficient Memory ParameterFixed

One or more parameters with `memory` data location are never modified in their functions and those functions are never called internally within the contract. Thus, their data location can be changed to `calldata` to avoid the gas consumption copying from calldata to memory. ```solidity=33 function multicall(Call[] memory calls) public returns (uint256 blockNumber, Result[] memory returnData) { ``` `multicall` has memory location parameters: `calls`.

LowLocked Blockchain Native TokensFixed

The contract has a `receive()` function or payable functions, making it able to receive native tokens. However, it does not have a function to withdraw the funds, which can lead to permanently locked tokens within the contract. ```solidity=47 receive() external payable {} ```

LowLow-level Call UsageFixed

The smart contract contains low-level calls such as `.call()` and `.delegatecall()`. Since these calls bypass some of the automatic checks that Solidity provides, like function type checks, they can introduce vulnerabilities, logic errors, or unexpected behavior. ```solidity=25 (success, resultData) = target.delegatecall(callData); ``` The `FlashWallet` smart contract employs a `delegatecall` to interact with an untrusted callee. The `delegatecall` function enables a contract to invoke a function from another contract while maintaining the caller's state. However, it is crucial to note that this approach requires the storage layout of the caller contract to be fully compatible with the callee contract. If the callee contract is untrusted and malicious, it can potentially manipulate the storage of the caller contract in unexpected ways. This poses a significant risk as the caller's storage can be modified in unintended and potentially harmful ways, compromising the integrity and security of the contract's data. --- ```solidity=77 (bool success, bytes memory result) = request.targetAddress.call{ value: msg.value }( abi.encodePacked(request.data, sender) ); ``` --- ```solidity=40 (bool success, bytes memory ret) = target.call{gas: gasLimit}(abi.encodePacked(callData,msg.sender)); ``` --- ```solidity=14 (bool success, bytes memory result) = address(this).delegatecall(data[i]); ```

LowMissing Input ValidationFixed

In the `TransferAdapter` contract, if a user attempts to transfer tokens to themselves in gasless, it could result in a loss of tokens for the user, as the tokens would be transferred and subsequently received back with payment fees deducted, resulting in a net loss due to payment fees. Hence, it is essential to add a check to ensure that the sender and recipient addresses are not the same. ```solidity=26 TransferData memory data = abi.decode(context.data, (TransferData)); require(data.sender != address(0), 'Sender not valid'); require(address(data.token) != address(0), 'Token not valid'); require(data.validator != address(0), 'Validator not set'); ``` Similarly, in the `GaslessSwapAdapter` contract, if a user attempts to swap the same token for itself, it could result in a loss of tokens for the user due to payment fees. Therefore, it is necessary to ensure that `tokenFrom` and `tokenTo` addresses are not the same. Additionally, the contract currently lacks a validation to confirm that the `paymentToken` is either `tokenFrom` or `tokenTo`. As a result, if `paymentToken` is a different token altogether, no payment fees will be charged. ```solidity=39 GaslessSwapData memory data = abi.decode(context.data, (GaslessSwapData)); ```

Show 8 more
LowMissing Zero Address ValidationFixed

Addresses are not validated before assignment or external calls, potentially allowing the use of zero addresses and leading to unexpected behavior or vulnerabilities. For example, transferring tokens to a zero address can result in a permanent loss of those tokens. ```solidity=117 controller = _controller; ``` - `_controller` is not zero-checked before being used. --- ```solidity=130 controller = _controller; ``` - `_controller` is not zero-checked before being used.

LowNo Update or Delete Mechanism for `adapters`Fixed

In the `MetaSwapRouter` contract, it only allows for the addition of new adapters to the `adapters` mapping. If an existing adapter is not upgradeable and does not utilize a proxy, any fixes or updates required in that adapter would necessitate deploying a new contract. Moreover, the new contract address would need to be manually updated in the adapters mapping. The problem arises from the fact that the contract lacks functionality to update or remove existing adapters. Currently, there is only an `addAdapter()` function available to add new adapters. This limitation becomes problematic, particularly because in most cases, the `adapterId` is hardcoded in the front-end code. Consequently, without the ability to update or remove adapters, the back-end is unable to dynamically modify the contract's state to remain compatible with the front-end code. This lack of flexibility impedes the ability to make necessary fixes or enhancements to the contract's functionality. ```solidity=108 function addAdapter(string calldata adapterId, address payable adapter) external { isAuthorizedController(); require(adapter != address(0), "MS_NULL"); require(adapters[adapterId] == address(0), "MS_EXISTS"); adapters[adapterId] = adapter; } ```

LowPotential Cross-Chain Replay AttackFixed

Signed messages are not properly verified with the current chain ID, thus allowing attackers to perform replay attacks across chains. Hardcoded or cached chain ID values are also vulnerable since a hard fork may occur and change the chain ID in the future. ```solidity=54 bytes32 digest = keccak256(abi.encodePacked( chainId, sender, this, request.validator, request.paymentToken, request.paymentFees, request.tokenGasPrice, request.validTo, request.nonce, request.targetAddress, request.data ) ``` - Reading a state variable `chainId`, which seems to use a cached chain ID value. - Encoding the cached chain ID value `chainId`. ```solidity=84 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); ``` - Encoding data using `abi.encodePacked()`. ```solidity=70 ECDSA.toEthSignedMessageHash(digest), validatorSignature ``` - `ECDSA.toEthSignedMessageHash(digest)` returns the cached chain ID. ```solidity=67 address signer = ecrecover(hash, v, r, s); ``` - Calling `ecrecover` with a hash that does not properly include the chain ID.

LowUnknown Usage of `flashWallet` as RecipientFixed

In the `swap()` function of `MetaSwapRouter` contract, the `recipient` parameter can be adjusted to either the original sender or the `flashWallet` address, allowing tokens to be sent to the `flashWallet` which is a smart contract. ```solidity=74 if (recipient == Constants.MSG_SENDER) recipient = sender; else if (recipient == Constants.ADDRESS_THIS) recipient = address(flashWallet); ```

LowUnused Return ValueFixed

The smart contract does not check or store the return value of an external call in a local or state variable, which may introduce vulnerabilities due to the unhandled outcome. ```solidity=30 IERC20(token).approve(spender, amount); ``` --- ```solidity=58 data.aggregator.functionCallWithValue(data.aggregatorData, senderAmount); ``` --- ```solidity=51 data.aggregator.functionCallWithValue(data.aggregatorData, msg.value); ```

LowUnused State VariableFixed

Some state variables are not used in the codebase. This can lead to incomplete functionality or potential vulnerabilities if these variables are expected to be utilized. Variable `ETH_TOKEN` in `LibERC20Adapter` is never used in `LibERC20Adapter`. ```solidity=13 IERC20 constant internal ETH_TOKEN = IERC20(ETH_TOKEN_ADDRESS); ```

LowUsage of `transfer`/`send` for Sending Native TokFixed

It is not recommended to use Solidity's `transfer()` and `send()` functions for transferring native tokens, since some contracts may not be able to receive the funds. Those functions forward only a fixed amount of gas (2300 specifically) and the receiving contracts may run out of gas before finishing the transfer. Also, EVM instructions' gas costs may increase in the future. Thus, some contracts that can receive now may stop working in the future due to the gas limitation. ```solidity=37 payable(address(data.receiver)).transfer(amountToTransfer); ``` - `TransferAdapter.adapt` uses `transfer()`.

LowUser Probably Get Swapped Token Less Than `amounFixed

In the `SwapAggregatorAdapter` contract, there is a validation mechanism to enforce a minimum amount of swapped token during a regular swap. The requirement stipulates that the quantity of tokens being transferred to the recipient must not fall below the minimum specified in `data.amountToMin`. If the token being swapped is classified as deflationary, a portion of the transfer fee is deducted, resulting in the recipient receiving a reduced amount of tokens (referred to as `diffRecipientToken`) compared to the initially transferred amount (`outputTokenAmount`). However, the present implementation verifies that `outputTokenAmount` exceeds the minimum quantity of tokens for the swap, rather than ensuring that the actual received amount surpasses the minimum requirement. ```solidity=54 uint256 outputTokenAmount = _transfer(data.tokenTo, context.recipient); { // balance after uint256 recipientTokenToBalanceAfter = LibERC20Adapter.getTokenBalanceOf( data.tokenTo, context.recipient ); // check conditions require(recipientTokenToBalanceAfter >= recipientTokenToBalanceBefore, 'SAA_NEG'); uint256 diffRecipientToken = recipientTokenToBalanceAfter.sub(recipientTokenToBalanceBefore); outputTokenAmount = outputTokenAmount > diffRecipientToken ? outputTokenAmount : diffRecipientToken; require(outputTokenAmount >= data.amountToMin, 'SAA_MIN'); } ```

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

  • contracts/lib/MulticallExtended.sol
  • contracts/FlashWallet.sol
  • contracts/lib/Multicall.sol
  • contracts/SwapAggregatorAdapter.sol
  • contracts/GaslessSwapAdapter.sol
  • contracts/TransferAdapter.sol
  • contracts/lib/Constants.sol
  • contracts/MetaSwapRouter.sol
  • contracts/lib/LibERC20Adapter.sol
  • contracts/lib/SelfPermit.sol
  • contracts/GaslessForwarder.sol
  • contracts/GaslessInterfaceMulticall.sol

Part of Kromatika Finance’s security record

Kromatika Finance
Audits1 audit by 1 firmLast audit Feb 2024, by CertiK.
Critical findingsNo open critical findingsIn 1 report.
Open Kromatika Finance’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