Reach out for an audit or to learn more about Macro
or Message on Telegram

Kwenta 18

Security Audit

September 02, 2024

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Kwenta's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from August 12th to August 23rd, 2024.

The purpose of this audit is to review the source code of certain Kwenta Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.

Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.

Overall Assessment

The following is an aggregation of issues found by the Macro Audit team:

Severity Count Acknowledged Won't Do Addressed
Critical 1 - - 1
High 2 1 - 1
Medium 2 - - 2
Low 2 - 1 1
Code Quality 6 - 2 4

Kwenta was quick to respond to these issues.

Specification

Our understanding of the specification was based on the following sources:

Source Code

The following source code was reviewed during the audit:

Specifically, we audited the following contracts within this repository:

Source Code SHA256
contract/contracts/FeeModule.sol

efc8c57b9e402b32ee628cdbef71bbb513e9ab667b1a1ecea76c9f051f73d2e6

contract/contracts/FeeReimbursementApp.sol

4404a920303e69ec59406819b275bbec296b8837545d030a8917e4c6bc81392d

contract/contracts/FeeReimbursementClaim.sol

9168c76e0fbb37b35a346005c3e87846d7e376ceeb091f4769ddeafdee1414ee

circuit/opv2_volume_fee_circuit.go

55db6eb7e98d65176ed25574ef7da4bfd0c4cc813b99f2618fd52cc1ba111bbd

common/common.go

9daa2d02dca81c54df4525581110cf7e4e1fa0af4624b3f2ffe10caa03990c3f

Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.

Issue Descriptions and Recommendations

Click on an issue to jump to it, or scroll down to see them all.

Security Level Reference

We quantify issues in three parts:

  1. The high/medium/low/spec-breaking impact of the issue:
    • How bad things can get (for a vulnerability)
    • The significance of an improvement (for a code quality issue)
    • The amount of gas saved (for a gas optimization)
  2. The high/medium/low likelihood of the issue:
    • How likely is the issue to occur (for a vulnerability)
  3. The overall critical/high/medium/low severity of the issue.

This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:

Severity Description
(C-x)
Critical

We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost.

(H-x)
High

We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec.

(M-x)
Medium

We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner.

(L-x)
Low

The risk is small, unlikely, or may not relevant to the project in a meaningful way.

Whether or not the project wants to develop a fix is up to the goals and needs of the project.

(Q-x)
Code Quality

The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design.

(I-x)
Informational

Warnings and things to keep in mind when operating the protocol. No immediate action required.

(G-x)
Gas Optimizations

The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it.

Issue Details

C-1

An invalid proof can be created allowing users to get a higher amount of fee rebates

Topic
Under-constrained circuit
Status
Impact
High
Likelihood
High

The whole purpose of the circuit is to proof that an account has generated a certain trading volume and fee over a specific period of time. The period is determined by the StartBlkNum and EndBlkNum given to the circuit as inputs.

The circuit calculates the associated volume and fee based on the following receipts:

  1. Unclaimable receipts
  2. ClaimableOrderFeeFlow receipts
  3. ClaimableExecution receipts

It is important to check that the receipt’s blockNumber is within the valid range of the circuit’s StartBlkNum and EndBlkNum inputs, otherwise, volume and fee might be incorrectly calculated.

For unclaimable receipts, the following range check is done:

uint248.IsZero(uint248.IsLessThan(r.BlockNum, startBlk30DAgo)), // r.BlockNum >= startBlkOneMonthAgo   // @audit-info check 3: is in timerange
uint248.IsLessThan(r.BlockNum, c.StartBlkNum),                  // r.BlockNum < c.StartBlkNum

Reference: opv2_volume_fee_circuit.go#L82-L83

For claimableExecution receipts, the following range check is done:

uint248.AssertIsLessOrEqual(c.StartBlkNum, blockNumber) 
uint248.AssertIsLessOrEqual(blockNumber, c.EndBlkNum)

Reference: opv2_volume_fee_circuit.go#L147-L148

However, for the claimableOrderFeeFlow receipts, being used to calculate the fee amount, a appropriate range check is missing. This can be exploited to receive a higher fee rebate for the account. Invalid proofs can be generated by inserting claimableExecution receipts that don’t fall under the valid blocknumber range (startBlkNum <= blocknumber <= endBlkNum), allowing to add out-of-range claimableExecution receipts for the whole purpose of inflating the fee amount.

This kind of vulnerability can be often seen in ZK circuits and belongs to the category of “under-constrained” circuits. This essentially means that a malicious Prover can create an invalid proof which is subsequently successfully verified by the verifier. This happens as the circuit contains too few Constrains (aka checks).

Remediation to Consider

Add an appropriate blocknumber range check for the claimableOrderFeeFlow receipts, similar to what is done for the ClaimableExecution receipts: startBlkNum <= blocknumber <= endBlkNum

H-1

Improper authentication in FeeReimbursementClaim.claim() function leads to DOS

Topic
Broken integration
Status
Impact
Medium
Likelihood
High

In FeeReimbursementClaim.claim() function, the rebate fee will be transferred to account, which is msg.sender. The function attempts to authenticate msg.sender by calling to smAccount.isAuth():

function claim(address _smartMarginAccount) external {
    address account = msg.sender;

    ...

    IAccount smAccount = IAccount(_smartMarginAccount);
    if (!smAccount.isAuth()) {
        revert Unauthorized();
    }

        ...

    rewardToken.transfer(account, feeRebateOP);

        ...
}

Reference: FeeReimbursementClaim.sol#L93-L121

However, the smAccount.isAuth() function is called by the FeeReimbursementClaim contract, which means msg.sender in the context of the isAuth() function is the FeeReimbursementClaim contract itself. As a result, the actual caller of FeeReimbursementClaim.claim() is not correctly authenticated, instead isAuth() attempts to authenticate the FeeReimbursementClaim contract address:

function isAuth() public view virtual returns (bool) {
    return (msg.sender == owner || delegates[msg.sender]);
}

Reference: Auth.sol#L54-L56

This will cause isAuth() reverting, so the owner of the smAccount can’t claim their rebate fee

Remediations to Consider

Consider checking authentication for the actual caller in the FeeReimbursementClaim.claim() function:

function claim(address _smartMarginAccount) external {
    address account = msg.sender;

    ...

    IAccount smAccount = IAccount(_smartMarginAccount);
-   if (!smAccount.isAuth()) {
+   if (!(smAccount.owner() == account) && !(smAccount.delegates(account)) ) {    
        revert Unauthorized();
    }

        ...
}
H-2

Zero block number in claimable execution receipts should not be allowed

Topic
Under-constrained circuit
Status
Acknowledged
Impact
High
Likelihood
Medium

In the circuit implementation, a zero block number for claimable execution receipt is allowed. When its value is set to zero, it will be initialized to the value of StartBlkNum.

        claimableExecutionReceipts := sdk.RangeUnderlying(receipts, MaxReceipts-MaxClaimableTradesPerCircuit, MaxReceipts)
        // Makse sure claimable execution tx's block numbers are in ascending order
        sdk.AssertSorted(claimableExecutionReceipts, func(a, b sdk.Receipt) sdk.Uint248 {
            return uint248.Or(uint248.IsLessThan(a.BlockNum, b.BlockNum), uint248.IsEqual(a.BlockNum, b.BlockNum))
        })
        sdk.AssertEach(claimableExecutionReceipts, func(r sdk.Receipt) sdk.Uint248 {
=>		blockNumber := uint248.Select(uint248.IsZero(r.BlockNum), c.StartBlkNum, r.BlockNum)
            uint248.AssertIsLessOrEqual(c.StartBlkNum, blockNumber)
            uint248.AssertIsLessOrEqual(blockNumber, c.EndBlkNum)
        ...
        }

However, allowing claimableExecutionReceipts with a 0 block number may lead to unexpected results. For example, if one claimable execution receipt is provided with BlockNum value 0 and another with BlockNum equal to StartBlkNum, both will be processed, and their corresponding fees and volume will be accumulated even though they have identical other properties. If two claimable execution receipts were provided with identical properties, including BlockNum value, the circuit would revert due to the uniqueness assertion being invalidated.

As a result, allowing claimableExecutionReceipts with a 0 value for block number may circumvent input uniqueness constraint.

Remediations to Consider

Update circuit implementation to add proper constraints for the r.BlockNum.

M-1

Chainlink can return stale price info

Topic
Oracle
Status
Impact
High
Likelihood
Low

In FeeReimbursementClaim’s _getChainlinkDataFeedLatestAnswer, a Chainlink price feed is used to retrieve the OP price:

(
    /*uint80 roundID*/
    ,
    int256 data,
    /*uint startedAt*/
    ,
    /*uint timeStamp*/
    ,
    /*uint80 answeredInRound*/
) = dataFeed.latestRoundData();

Reference: FeeReimbursementClaim.sol#L177-L188

However, the returned data is used “as is” without any further sanity checks.

It needs to be noticed, that the call to Chainlink’s price feed using latestRoundData() can return stale data in extreme situations, such as highly volatile market conditions and flash crashes. In such situations, the returned price can significantly deviate from the actual OP price.

Remediations to Consider

Follow best practices when consuming Chainlink price feeds. We have written a post about this.

M-2

Users can be prevented to claim rebates for certain periods

Topic
Protocol Design
Status
Impact
High
Likelihood
Low

In FeeReimbursementApp, the new claim period is calculated in _newClaimPeriod. The goal of the function is to create or extend the claim period with the provided startBlockNumber and endBlockNumber of the new claim period. Additionally, it is designed to prevent double-counting, meaning that the new period must not have any overlapping block numbers with the previous period.

For this to work, the current implementation relies on calling the handleProofResult function in the correct order. If this is not the case and the Brevis service is not calling the function in the correct order, claimPeriods might be left out and cannot be set at a later point in time anymore. Consider the following scenario:

  1. A proof is generated for claimPeriod of
    • startBlockNumber: 1
    • endBlockNumber: 5
  2. Next, a proof is generated for claimPeriod of
    • startBlockNumber: 8
    • endBlockNumber: 15

As a result, the claimPeriod is set to startBlockNumber: 1 and endBlockNumber: 15, implicitly assuming that the users is not eligible for any rebates for block numbers between 5 and 8.

However, a proof for block number 5-8 were not generated due to some outages of the Brevis Service. With the current design, resubmitting a proof for block number 5-8 will revert, effectively preventing the user from claiming any deserved rebates during this period.

Remediations to Consider

Adapt the logic in _newClaimPeriod accordingly so that it doesn’t need the Brevis service to generate the proofs in a consecutive order.

L-1

Call safeTransfer instead of transfer

Topic
Best Practice
Status
Impact
High
Likelihood
Low

In FeeReimbursementClaim’s claim function, transfer is used to send the reward tokens to the user instead of using safeTransfer.

Some ERC20 tokens return false when the call fails instead of reverting. Depending on the rewardToken being used, this could cause claim to successfully run without the user acutal claiming the tokens.

Note that this issue is considered as low severity, since it is expected to only use OP ERC20 token as reward token. However, with the current implementation, any other ERC20 tokens could be specified as reward token.

Remediations to Consider

Use OpenZeppelin’s safeTransfer function.

L-2

Scaling the price assumes 8 decimals

Topic
Oracle
Status
Wont Do
Impact
High
Likelihood
Low

In FeeReimbursementClaim’s _convertUSDtoOP function, the price data returned by the Chainlink price feed is expected to be 8 decimals, which is the case for the OP price feed.

It further scales up opPriceInWei to 18 decimals as follows:

uint256 opPriceInWei = uint256(price) * 1e10; 

Reference: FeeReimbursementClaim.sol#L142

The price is incorrectly calculated when using a price feed with decimals other than 8.

Note that this issue is considered low severity since it is expected to only use the OP price feed. However, with the current implementation, any other price feed could be provided at deployment.

Remediations to Consider

It is recommended to use the value returned by AggregatorV2V3Interface.decimals() to scale the price accordingly.

Response by Kwenta

Won't do - Contract will be deployed with OP as rewardToken, decimals are assumed inside a function called convertUSDtoOP and it is documented

Q-1

Missing requestId in event

Topic
Event
Status
Wont Do
Quality Impact
Low
Topic Event
Impact Low

In FeeReimbursementApp, handleProofResult emits the following event:

emit FeeRebateAccumulated(
    account, feeRebate, volume30D, feeRebateWithRate, startBlockNumber, endBlockNumber
);

Reference: FeeReimbursementApp.sol#L94

The function is called by the Brevis service which passes an associated requestId. The requestId might be useful for tracking down any potential issues.

Remediations to Consider

Consider adding the provided _requestId to the FeeRebateAccumulated event.

Q-2

Inconsistent error handling between contracts

Topic
Best Practice
Status
Quality Impact
Low

FeeReimbursementApp uses revert to return custom errors as well as require statements to handle error messages. Additionally, FeeReimbursementClaim only uses custom errors.

Remediations to Consider

For consistency, consider replacing require statements with custom errors.

Q-3

Nitpicks

Topic
Nitpicks
Status
Quality Impact
Low
Topic Nitpicks
Impact Low
  • In FeeReimbursementApp, rewardToken, rewardTokenDecimals and associated function setRewardToken is not used. Reference: FeeReimbursementApp.sol#L31-L32
  • In FeeReimbursementApp, claimer and associated function setClaimer is not used. Reference: FeeReimbursementApp.sol#L34, FeeReimbursementApp.sol#L165
  • In FeeReimbursementApp, the events MigrationDone and MigrationFinishedForAccount are not used. Reference: FeeReimbursementApp.sol#L55-L56
  • In FeeReimbursementClaim, the defined error InsufficientContractBalance is not used. Reference: FeeReimbursementClaim.sol#L55
  • In FeeReimbursementClaim, dataFeed and sequencerUptimeFeed can both be declared as immutable. Reference: FeeReimbursementClaim.sol#L23-L24
  • In FeeReimbursementClaim, the _convertUSDtoOP specifies opAmount and price as “named return variables”. When using “named return variables”, values need to be assigned to the variables but there is no need to explicitly return the variables themselves. Reference: FeeReimbursementClaim.sol#L132
  • In FeeReimbursementApp, the AlreadySet() event is defined but never used. Reference: FeeReimbursementApp.sol#L26
  • In FeeReimbursementApp, the SafeERC20 import is never used. Consider removing unnecessary import and corresponding library usage using SafeERC20 for IERC20. Reference: FeeReimbursementApp.sol#L29
Q-4

FeeModule unnecessarily inherits Ownable

Topic
Unnecessary code
Status
Quality Impact
Low

FeeModule contract inherits OZ’s Ownable functionality. However, none of that functionality is needed or used.

Consider removing unnecessary contract inheritance.

Q-5

Missing event on significant state change

Topic
Events
Status
Quality Impact
Low

In FeeReimbursementApp, setClaimContract() is owner accessible function that can update the claim contract.

However, this important state change does not have corresponding event emission, which would facilitate off-chain tracking and monitoring.

Consider emitting appropriate event on state change.

Q-6

Hardcoded precision denominator

Topic
Best practices
Status
Wont Do
Quality Impact
Low

In FeeReimbursementApp, handleProofResult() contains the calculation of feeRebate with externally retrieved rebateRate. The implementation assumes that the value of the percentage is within [0,100] and, as a result, uses a hardcoded 100 value as a denominator.

if (feeRebate > 0) {
    percentage = feeRebateTierModule.getFeeRebatePercentage(volume30D);
    feeRebateWithRate = feeRebate * percentage / 100;
}

However, if the percentage value was 1e18, as is commonly used to represent 100%, this hardcoded denominator would be incorrect, and consequently, the final result would be too.

Consider retrieving both percentage and precision (100) from the feeRebateTierModule or providing both volume30D and feeRebate to the feeRebateTierModule to calculate the final result.

Disclaimer

Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.

Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.

The scope of this report and review is limited to a review of only the code presented by the Kwenta team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.