Giter Site home page Giter Site logo

2023-06-gfx-judging's Introduction

Issue H-1: Lack of segregation between users' assets and collected fees resulting in loss of funds for the users

Source: #48

Found by

Dug, kutugu, mahyar, mstpr-brainbot, trachev, xiaoming90

Summary

The users' assets are wrongly sent to the owner due to a lack of segregation between users' assets and collected fees, which might result in an irreversible loss of assets for the victims.

Vulnerability Detail

GLX uses the Chainlink Automation to execute the LimitOrderRegistry.performUpkeep function when there are orders that need to be fulfilled. The LimitOrderRegistry contract must be funded with LINK tokens to keep the operation running.

To ensure the LINK tokens are continuously replenished and funded, users must pay a fee denominated in Native ETH or ERC20 WETH tokens on orders claiming as shown below. The collected ETH fee will be stored within the LimitOrderRegistry contract.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L696

File: LimitOrderRegistry.sol
696:     function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
723:         // Transfer tokens owed to user.
724:         tokenOut.safeTransfer(user, owed);
725: 
726:         // Transfer fee in.
727:         address sender = _msgSender();
728:         if (msg.value >= userClaim.feePerUser) {
729:             // refund if necessary.
730:             uint256 refund = msg.value - userClaim.feePerUser;
731:             if (refund > 0) sender.safeTransferETH(refund);
732:         } else {
733:             WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
734:             // If value is non zero send it back to caller.
735:             if (msg.value > 0) sender.safeTransferETH(msg.value);
736:         }
..SNIP..

To retrieve the ETH fee collected, the owner will call the LimitOrderRegistry.withdrawNative function that will send all the Native ETH and ERC20 WETH tokens within the LimitOrderRegistry contract to the owner's address. After executing this function, the Native ETH and ERC20 WETH tokens on this contract will be zero and wiped out.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505

File: LimitOrderRegistry.sol
505:     function withdrawNative() external onlyOwner {
506:         uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
507:         uint256 nativeBalance = address(this).balance;
508:         // Make sure there is something to withdraw.
509:         if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();
510: 
511:         // transfer wrappedNativeBalance if it exists
512:         if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
513:         // transfer nativeBalance if it exists
514:         if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
515:     }

Most owners will automate replenishing the LimitOrderRegistry contract with LINK tokens to ensure its balance does not fall below zero and for ease of maintenance. For instance, a certain percentage of the collected ETH fee (e.g., 50%) will be swapped immediately to LINK tokens on a DEX upon collection and transferred the swapped LINK tokens back to the LimitOrderRegistry contract. The remaining will be spent to cover operation and maintenance costs.

However, the issue is that there are many Uniswap V3 pools where their token pair consists of ETH/WETH. In fact, most large pools in Uniswap V3 will consist of ETH/WETH. For instance, the following Uniswap pools consist of ETH/WETH as one of the pool tokens:

Assume that the owner has configured and setup the LimitOrderRegistry contract to work with the Uniswap DAI/ETH pool, and the current price of the DAI/ETH pool is 1,500 DAI/ETH.

Bob submit a new Buy Limit Order swapping DAI to ETH at the price of 1,000 DAI/ETH. Bob would deposit 1,000,000 DAI to the LimitOrderRegistry contract.

When Bob's Buy Limit Order is ITM and fulfilled, 1000 ETH/WETH will be sent to and stored within the LimitOrderRegistry contract.

The next step that Bob must do to claim the swapped 1000 ETH/WETH is to call the LimitOrderRegistry.claimOrder function, which will collect the fee and transfer the swapped 1000 ETH/WETH to Bob.

Unfortunately, before Bob could claim his swapped ETH/WETH, the LimitOrderRegistry.withdrawNative function is triggered by the owner or the owner's bots. As noted earlier, when the LimitOrderRegistry.withdrawNative function is triggered, all the Native ETH and ERC20 WETH tokens on this contract will be transferred to the owner's address. As a result, Bob's 1000 swapped ETH/WETH stored within the LimitOrderRegistry contract are sent to the owner's address, and the balance of ETH/WETH in the LimitOrderRegistry contract is zero.

When Bob calls the LimitOrderRegistry.claimOrder function, the transaction will revert because insufficient ETH/WETH is left in the LimitOrderRegistry contract.

Unfortunately for Bob, there is no way to recover back his ETH/WETH that is sent to the owner's address. Following outline some of the possible scenarios where this could happen:

  • The owners set up their infrastructure to automatically swap a portion or all the ETH/WETH received to LINK tokens and transfer them to the LimitOrderRegistry contract, and there is no way to retrieve the deposited LINK tokens from the LimitOrderRegistry contract even if the owner wishes to do so as there is no function within the contract to allow this action.
  • The owners set up their infrastructure to automatically swap a small portion of ETH/WETH received to LINK tokens and send the rest of the ETH/WETH to 100 investors/DAO members' addresses. So, it is no guarantee that the investors/DAO members will return the ETH/WETH to Bob.

Impact

Loss of assets for the users

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505

Tool used

Manual Review

Recommendation

Consider implementing one of the following solutions to mitigate the issue:

Solution 1 - Only accept Native ETH as fee

Uniswap V3 pool stored ETH as Wrapped ETH (WETH) ERC20 token internally. When the collect function is called against the pool, WETH ERC20 tokens are returned to the caller. Thus, the most straightforward way to mitigate this issue is to update the contract to collect the fee in Native ETH only.

In this case, there will be a clear segregation between users' assets (WETH) and owner's fee (Native ETH)

function withdrawNative() external onlyOwner {
-    uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
    uint256 nativeBalance = address(this).balance;
    // Make sure there is something to withdraw.
-    if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();
+    if (nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

-    // transfer wrappedNativeBalance if it exists
-    if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
    // transfer nativeBalance if it exists
    if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
}
function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
    // Transfer tokens owed to user.
    tokenOut.safeTransfer(user, owed);

    // Transfer fee in.
    address sender = _msgSender();
    if (msg.value >= userClaim.feePerUser) {
        // refund if necessary.
        uint256 refund = msg.value - userClaim.feePerUser;
        if (refund > 0) sender.safeTransferETH(refund);    
    } else {
-       WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
-       // If value is non zero send it back to caller.
-       if (msg.value > 0) sender.safeTransferETH(msg.value);
+		revert LimitOrderRegistry__InsufficientFee;
    }
..SNIP..

Solution 2 - Define state variables to keep track of the collected fee

Consider defining state variables to keep track of the collected fee so that the fee will not mix up with users' assets.

function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
    // Transfer fee in.
    address sender = _msgSender();
    if (msg.value >= userClaim.feePerUser) {
+    	collectedNativeETHFee += userClaim.feePerUser
        // refund if necessary.
        uint256 refund = msg.value - userClaim.feePerUser;
        if (refund > 0) sender.safeTransferETH(refund);
    } else {
+    	collectedWETHFee += userClaim.feePerUser
        WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
        // If value is non zero send it back to caller.
        if (msg.value > 0) sender.safeTransferETH(msg.value);
    }
..SNIP..
function withdrawNative() external onlyOwner {
-   uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
-   uint256 nativeBalance = address(this).balance;
+	uint256 wrappedNativeBalance = collectedWETHFee;
+	uint256 nativeBalance = collectedNativeETHFee;
+	collectedWETHFee = 0; // clear the fee
+	collectedNativeETHFee = 0; // clear the fee
    // Make sure there is something to withdraw.
    if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

    // transfer wrappedNativeBalance if it exists
    if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
    // transfer nativeBalance if it exists
    if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
}

Discussion

elee1766

I believe solution 2 is the better option here, but will have to consider more before implementation

elee1766

gfx-labs/uniswap-v3-limit-orders#1

elee1766

fix

xiaoming9090

Verified. Fixed in gfx-labs/uniswap-v3-limit-orders#1

Issue H-2: Users' funds could be stolen or locked by malicious or rouge owners

Source: #54

Found by

Dug, mstpr-brainbot, p0wd3r, xiaoming90

Summary

Users' funds could be stolen or locked by malicious or rouge owners.

Vulnerability Detail

In the contest's README, the following was mentioned.

Q: Is the admin/owner of the protocol/contracts TRUSTED or RESTRICTED?

restricted. the owner should not be able to steal funds.

It was understood that the owner is not "trusted" and should not be able to steal funds. Thus, it is fair to assume that the sponsor is keen to know if there are vulnerabilities that could allow the owner to steal funds or, to a lesser extent, lock the user's funds.

Many control measures are implemented within the protocol to prevent the owner from stealing or locking the user's funds.

However, based on the review of the codebase, there are still some "loopholes" that the owner can exploit to steal funds or indirectly cause losses to the users. Following is a list of methods/tricks to do so.

Method 1 - Use the vulnerable withdrawNative function

Once the user's order is fulfilled, the swapped ETH/WETH will be sent to the contract awaiting the user's claim. However, the owner can call the withdrawNative function, which will forward all the Native ETH and Wrapped ETH in the contract to the owner's address due to another bug ("Lack of segregation between users' assets and collected fees resulting in loss of funds for the users") that I highlighted in another of my report.

Method 2 - Add a malicious custom price feed

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L482

File: LimitOrderRegistry.sol
482:     function setFastGasFeed(address feed) external onlyOwner {
483:         fastGasFeed = feed;
484:     }

The owner can create a malicious price feed contract and configure the LimitOrderRegistry to use it by calling the setFastGasFeed function.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L914

File: LimitOrderRegistry.sol
914:     function performUpkeep(bytes calldata performData) external {
915:         (UniswapV3Pool pool, bool walkDirection, uint256 deadline) = abi.decode(
916:             performData,
917:             (UniswapV3Pool, bool, uint256)
918:         );
919: 
920:         if (address(poolToData[pool].token0) == address(0)) revert LimitOrderRegistry__PoolNotSetup(address(pool));
921: 
922:         PoolData storage data = poolToData[pool];
923: 
924:         // Estimate gas cost.
925:         uint256 estimatedFee = uint256(upkeepGasLimit * getGasPrice());

When fulfilling an order, the getGasPrice() function will fetch the gas price from the malicious price feed that will report an extremely high price (e.g., 100000 ETH), causing the estimatedFee to be extremely high. When users attempt to claim the order, they will be forced to pay an outrageous fee, which the users cannot afford to do so. Thus, the users have to forfeit their orders, and they will lose their swapped tokens.

Impact

Users' funds could be stolen or locked by malicious or rouge owners.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L482

Tool used

Manual Review

Recommendation

Consider implementing the following measures to reduce the risk of malicious/rouge owners from stealing or locking the user's funds.

  1. To mitigate the issue caused by the vulnerable withdrawNative function. Refer to my recommendation in my report titled "Lack of segregation between users' assets and collected fees resulting in loss of funds for the users".
  2. To mitigate the issue of the owner adding a malicious custom price feed, consider performing some sanity checks against the value returned from the price feed. For instance, it should not be larger than the MAX_GAS_PRICE constant. If it is larger than MAX_GAS_PRICE constant, fallback to the user-defined gas feed, which is constrained to be less than MAX_GAS_PRICE.

Discussion

elee1766

this is a valid concern.

I think a sanity check on the gas variable makes a lot of sense.

Issue M-1: Owners will incur loss and bad debt if the value of a token crashes

Source: #51

Found by

xiaoming90

Summary

If the value of the swapped tokens crash, many users will choose not to claim the orders, which result in the owner being unable to recoup back the gas fee the owner has already paid for automating the fulfillment of the orders, incurring loss and bad debt.

Vulnerability Detail

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L696

File: LimitOrderRegistry.sol
696:     function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
726:         // Transfer fee in.
727:         address sender = _msgSender();
728:         if (msg.value >= userClaim.feePerUser) {
729:             // refund if necessary.
730:             uint256 refund = msg.value - userClaim.feePerUser;
731:             if (refund > 0) sender.safeTransferETH(refund);
732:         } else {
733:             WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
734:             // If value is non zero send it back to caller.
735:             if (msg.value > 0) sender.safeTransferETH(msg.value);
736:         }

Users only need to pay for the gas cost for fulfilling the order when they claim the order to retrieve the swapped tokens. When the order is fulfilled, the swapped tokens will be sent to and stored in the LimitOrderRegistry contract.

However, in the event that the value of the swapped tokens crash (e.g., Terra's LUNA crash), it makes more economic sense for the users to abandon (similar to defaulting in traditional finance) the orders without claiming the worthless tokens to avoid paying the more expensive fee to the owner.

As a result, many users will choose not to claim the orders, which result in the owner being unable to recoup back the gas fee the owner has already paid for automating the fulfillment of the orders, incurring loss and bad debt.

Impact

Owners might be unable to recoup back the gas fee the owner has already paid for automating the fulfillment of the orders, incurring loss and bad debt.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L696

Tool used

Manual Review

Recommendation

Consider collecting the fee in advance based on a rough estimation of the expected gas fee. When the users claim the order, any excess fee will be refunded, or any deficit will be collected from the users.

In this case, if many users choose to abandon the orders, the owner will not incur any significant losses.

Discussion

elee1766

this is a known issue, but i don't think we will fix it.

owners recognize that gas can possibly be "wasted" under bad network conditions

Issue M-2: Owner unable to collect fulfillment fee from certain users due to revert error

Source: #55

Found by

XDZIBEC, kutugu, mstpr-brainbot, xiaoming90

Summary

Certain users might not be able to call the claimOrder function under certain conditions, resulting in the owner being unable to collect fulfillment fees from the users.

Vulnerability Detail

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L721

File: LimitOrderRegistry.sol
696:     function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
697:         Claim storage userClaim = claim[batchId];
698:         if (!userClaim.isReadyForClaim) revert LimitOrderRegistry__OrderNotReadyToClaim(batchId);
699:         uint256 depositAmount = batchIdToUserDepositAmount[batchId][user];
700:         if (depositAmount == 0) revert LimitOrderRegistry__UserNotFound(user, batchId);
701: 
702:         // Zero out user balance.
703:         delete batchIdToUserDepositAmount[batchId][user];
704: 
705:         // Calculate owed amount.
706:         uint256 totalTokenDeposited;
707:         uint256 totalTokenOut;
708:         ERC20 tokenOut;
709: 
710:         // again, remembering that direction == true means that the input token is token0.
711:         if (userClaim.direction) {
712:             totalTokenDeposited = userClaim.token0Amount;
713:             totalTokenOut = userClaim.token1Amount;
714:             tokenOut = poolToData[userClaim.pool].token1;
715:         } else {
716:             totalTokenDeposited = userClaim.token1Amount;
717:             totalTokenOut = userClaim.token0Amount;
718:             tokenOut = poolToData[userClaim.pool].token0;
719:         }
720: 
721:         uint256 owed = (totalTokenOut * depositAmount) / totalTokenDeposited;
722: 
723:         // Transfer tokens owed to user.
724:         tokenOut.safeTransfer(user, owed);

Assume the following:

  • SHIB has 18 decimals of precision, while USDC has 6.
  • Alice (Small Trader) deposited 10 SHIB while Bob (Big Whale) deposited 100000000 SHIB.
  • The batch order was fulfilled, and it claimed 9 USDC (totalTokenOut)

The following formula and code compute the number of swapped/claimed USDC tokens a user is entitled to.

owed = (totalTokenOut * depositAmount) / totalTokenDeposited
owed = (9 USDC * 10 SHIB) / 100000000 SHIB
owed = (9 * 10^6 * 10 * 10^18) / (100000000 * 10^18)
owed = (9 * 10^6 * 10) / (100000000)
owed = 90000000 / 100000000
owed = 0 USDC (Round down)

Based on the above assumptions and computation, Alice will receive zero tokens in return due to a rounding error in Solidity.

The issue will be aggravated under the following conditions:

  • If the difference in the precision between token0 and token1 in the pool is larger
  • The token is a stablecoin, which will attract a lot of liquidity within a small price range (e.g. $0.95 ~ $1.05)

Note: Some tokens have a low decimal of 2 (e.g., Gemini USD), while others have a high decimal of 24 (e.g., YAM-V2 has 24). Refer to https://github.com/d-xo/weird-erc20#low-decimals

The rounding down to zero is unavoidable in this scenario due to how values are represented. It is not possible to send Alice 0.9 WEI of USDC. The smallest possible amount is 1 WEI.

In this case, it will attempt to transfer a zero amount of tokenOut, which might result in a revert as some tokens disallow the transfer of zero value. As a result, when users call the claimOrder function, it will revert, and the owner will not be able to collect the fulfillment fee from the users.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L724

723:         // Transfer tokens owed to user.
724:         tokenOut.safeTransfer(user, owed);

Impact

When a user cannot call the claimOrder function due to the revert error, the owner will not be able to collect the fulfillment fee from the user, resulting in a loss of fee for the owner.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L724

Tool used

Manual Review

Recommendation

Consider only transferring the assets if the amount is more than zero.

uint256 owed = (totalTokenOut * depositAmount) / totalTokenDeposited;

// Transfer tokens owed to user.
- tokenOut.safeTransfer(user, owed);
+ if (owed > 0) tokenOut.safeTransfer(user, owed);

Discussion

elee1766

i agree - this change should be made.

elee1766

gfx-labs/uniswap-v3-limit-orders#1

elee1766

fix

xiaoming9090

Verified. Fixed in gfx-labs/uniswap-v3-limit-orders#1

Issue M-3: getGasPrice() doesn't check Arbitrum l2 chainlink feed is active

Source: #65

Found by

Avci, Breeje, MohammedRizwan, ginlee, kutugu

Summary

getGasPrice() doesn't check Arbitrum l2 chainlink feed is active

Vulnerability Detail

When utilizing Chainlink in L2 chains like Arbitrum, it's important to ensure that the prices provided are not falsely perceived as fresh, even when the sequencer is down. This vulnerability could potentially be exploited by malicious actors to gain an unfair advantage.

If the sequencer is down, messages cannot be transmitted from L1 to L2, and no L2 transactions are executed. Instead, messages are enqueued in the CanonicalTransactionChain on L1

On the L1 network:

1.A network of node operators runs the external adapter to post the latest sequencer status to the AggregatorProxy contract and relays the status to the Aggregator contract. The Aggregator contract calls the validate function in the OptimismValidator contract.

2.The OptimismValidator contract calls the sendMessage function in the L1CrossDomainMessenger contract. This message contains instructions to call the updateStatus(bool status, uint64 timestamp) function in the sequencer uptime feed deployed on the L2 network.

3.The L1CrossDomainMessenger contract calls the enqueue function to enqueue a new message to the CanonicalTransactionChain.

4.The Sequencer processes the transaction enqueued in the CanonicalTransactionChain contract to send it to the L2 contract.

On the L2 network:

1.The Sequencer posts the message to the L2CrossDomainMessenger contract.

2.The L2CrossDomainMessenger contract relays the message to the OptimismSequencerUptimeFeed contract.

3.The message relayed by the L2CrossDomainMessenger contains instructions to call updateStatus in the OptimismSequencerUptimeFeed contract.

4.Consumers can then read from the AggregatorProxy contract, which fetches the latest round data from the OptimismSequencerUptimeFeed contract.

Impact

could potentially be exploited by malicious actors to gain an unfair advantage.

Code Snippet

 function getGasPrice() public view returns (uint256) {
        // If gas feed is set use it.
        if (fastGasFeed != address(0)) {
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            uint256 timeSinceLastUpdate = block.timestamp - _timestamp;
            // Check answer is not stale.
            if (timeSinceLastUpdate > FAST_GAS_HEARTBEAT) {
                // If answer is stale use owner set value.
                // Multiply by 1e9 to convert gas price to gwei
                return uint256(upkeepGasPrice) * 1e9;
            } else {
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);
                return answer;
            }
        }
        // Else use owner set value.
        return uint256(upkeepGasPrice) * 1e9; // Multiply by 1e9 to convert gas price to gwei
    }

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1447-L1465

Tool used

Manual Review

Recommendation

The recommendation is to implement a check for the sequencer in the L2 version of the contract, and a code example of Chainlink can be found at https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code.

CHAINLINK DOC : https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Same issue in other contests

sherlock-audit/2023-02-bond-judging#1

sherlock-audit/2022-11-sentiment-judging#3

sherlock-audit/2023-01-sentiment-judging#16

Discussion

elee1766

gfx-labs/uniswap-v3-limit-orders#1

elee1766

fix

xiaoming9090

Verified. Fixed in gfx-labs/uniswap-v3-limit-orders#1

2023-06-gfx-judging's People

Contributors

hrishibhat avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2023-06-gfx-judging's Issues

0xGoodess - new upkeepGasLimit or maxFillsPerUpkeep in LimitOrderRegistry is not sync with the maxGasLimit in chainlink automation task for old pool

0xGoodess

medium

new upkeepGasLimit or maxFillsPerUpkeep in LimitOrderRegistry is not sync with the maxGasLimit in chainlink automation task for old pool

Summary

new upkeepGasLimit or maxFillsPerUpkeep in LimitOrderRegistry is not sync with the maxGasLimit in chainlink automation task

Vulnerability Detail

when a new UniswapV3 pool is registered through setupLimitOrder, the current maxFillsPerUpkeep * upkeepGasLimit is used for setting up the max gaslimit for the chainlink automation. However when the upkeepGasLimit or maxFillsPerUpkeep is updated by the owner, the old parameter is still being used for the old pool.

Impact

latest max gaslimit related parameters is not in sync for chainlink automation.

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L379-L381
https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L460-L464

Tool used

Manual Review

Recommendation

There is no easy fix for this issue since chainlink keeper does not have a straight-forward interface for updating configuration. One possible way is to cancel the task first, then re-request using the updated logic/parameter. The limitation is that each existing pool requires a separate call once an update is needed. This does not scale well as the size of the pool grows.

However it's important to sync (or have a way for the owner to sync) the new gas limit for past chainlink automation task, otherwise there is no way for the tradingMnager or external party to track the gas limit for a particular pool.

MohammedRizwan - Chainlink's latestRoundData might return stale or incorrect results(with chainlink reference)

MohammedRizwan

medium

Chainlink's latestRoundData might return stale or incorrect results(with chainlink reference)

Summary

Chainlink's latestRoundData might return stale or incorrect results in LimitOrderRegistry.sol

Vulnerability Detail

Impact

The getGasPrice() function in the contract LimitOrderRegistry.sol fetches the asset price from a Chainlink aggregator using the latestRoundData function. However, there are no checks on roundID, resulting in stale prices. The oracle wrapper calls out to a chainlink oracle receiving the latestRoundData().

Stale prices could put funds at risk. According to Chainlink's documentation, This function does not error if no answer has been reached but returns 0, causing an incorrect price fed to the PriceOracle. The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations. Oracle reliance has historically resulted in crippled on-chain systems, and complications that lead to these outcomes can arise from things as simple as network congestion.

Chainlink reference-
https://docs.chain.link/data-feeds/historical-data#getrounddata-return-values

Chainlink latestRoundData ref

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1450

Tool used

Manual Review

Recommendation

Consider adding missing checks for stale data.

For example:

(uint80 roundID, int256 feedPrice, , uint256 timestamp, uint80 answeredInRound) = feed.latestRoundData();
require(feedPrice > 0, "Chainlink price <= 0"); 
require(answeredInRound >= roundID, "Stale price");
require(timestamp != 0, "Round not complete");

kutugu - The owner can rug pull all wrappedNative token

kutugu

high

The owner can rug pull all wrappedNative token

Summary

The protocol states in the audit document that owner is restricted and should not be able to steal funds.
However, there is a withdrawNative function in the contract that can extract any wrappedNative token.
The wrappedNative may be one token that makes up the LP. When fulfillOrder, both tokens of the lp are kept in the contract. If the owner takes all wrappedNative, the user funds are not guaranteed to be safe.

Vulnerability Detail

    function withdrawNative() external onlyOwner {
        uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
        uint256 nativeBalance = address(this).balance;
        // Make sure there is something to withdraw.
        if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

        // transfer wrappedNativeBalance if it exists
        if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
        // transfer nativeBalance if it exists
        if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
    }

As you can see from the code, there are no restrictions

Impact

The Owner can rug pull all wrappedNative tokens

Code Snippet

Tool used

Manual Review

Recommendation

The contract should maintain an amount of wrappedNative to be extracted by the user, preventing the owner rug pull

Duplicate of #48

kutugu - latestRoundData without checking the L2 sequencer is offline

kutugu

medium

latestRoundData without checking the L2 sequencer is offline

Summary

The protocol will be deployed on the L2 network. There is a sequencer in arbitrum and optimism. When the sequencer goes offline, the oracle data will not be updated.
Note that this is not the same as freshness, because there are situations where the gas price data changes substantially, but the latest data is not updated because the sequencer is offline, and the old data used is still in the heartbeat range.

Vulnerability Detail

    function getGasPrice() public view returns (uint256) {
        // If gas feed is set use it.
        if (fastGasFeed != address(0)) {
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            uint256 timeSinceLastUpdate = block.timestamp - _timestamp;
            // Check answer is not stale.
            if (timeSinceLastUpdate > FAST_GAS_HEARTBEAT) {
                // If answer is stale use owner set value.
                // Multiply by 1e9 to convert gas price to gwei
                return uint256(upkeepGasPrice) * 1e9;
            } else {
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);
                return answer;
            }
        }
        // Else use owner set value.
        return uint256(upkeepGasPrice) * 1e9; // Multiply by 1e9 to convert gas price to gwei
    }

As you can see from the code here, the data is read directly without checking the sequencer state

Impact

When the L2 sequencer goes offline, an inaccurate old price can be used

Code Snippet

Tool used

Manual Review

Recommendation

Check the L2 sequencer is offline or not

Duplicate of #65

mstpr-brainbot - Owner can take the users claimable WETH balances

mstpr-brainbot

high

Owner can take the users claimable WETH balances

Summary

In the current contract setup, when an order is completed, users' claimable balance increases and waits idle until claimed by the users. They can pay the claim fee using either ETH or WETH. However, the contract design allows the owner to withdraw the entire WETH balance using the withdrawNative() function. This feature can be exploited to steal any unclaimed WETH balance of users, causing errors in accounting and rendering the contract unusable.

Vulnerability Detail

When an order is fulfilled by keepers or anyone, the claimable balance of the users of the particular order is increased and stands idle until users claims it. When a user claims the claimable balance user either pays the claim fee with ETH or WETH. Those WETH that's been collected in the contract can be swept by the owner by calling the withdrawNative(). However, withdrawNative function claims the entire WETH balance of the contract. If any user has a claimable WETH balance waiting in the contract owner can call withdrawNative() to steal them. Furthermore, this will make the user to not be able to claim its claimable balance and all the accounting will go wrong inside the contract.

Impact

Not only owner can steal the user funds, this would also make the contract inoperable. Since the users claimable amount is not available in the contract, as soon as some other range order goes through the same user can now claim making the newly fulfilled order users wait. In addition to that, any fees that's collected in terms of WETH will also be accounting completely wrong since the balances already withdrawn.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505-L515

Tool used

Manual Review

Recommendation

Duplicate of #48

mstpr-brainbot - Users can claim zero assets due to decimal differences between pool tokens

mstpr-brainbot

medium

Users can claim zero assets due to decimal differences between pool tokens

Summary

Divison rounding can make the users claimable amount "0".

Vulnerability Detail

When an user claims the fulfilled order, the amount they can claim is calculated as follows:

uint256 owed = (totalTokenOut * depositAmount) / totalTokenDeposited;

whereas, depositAmount and totalTokenDeposited is in terms of tokenX and totalTokenOut is in terms of tokenY

Assume that the tokenX has 18 decimals and tokenY has 2 decimals.

Assume totalTokenDeposited is 100M X tokens, 1026
Assume users depositAmount is 10K X tokens, 10
22
Assume totalTokenOut is 10 Y token, 10**3

according to formula user gets 103 * 1022 / 10**26 = 0, because solidity works with integer roundings.

Impact

Since the protocol should work with any Uniswap pool tokens, these tokens can have decimals like 0-2-4-6-8-11-18 and in case of a pair where there is a decimal difference this scenario is very likely to happen.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L696-L739

Tool used

Manual Review

Recommendation

Use a precision factor to calculate precised amounts for users

Duplicate of #55

0xStalin - Decreasing liquidity from an LP Position is suceptible to a sandwich attack which could lead to users receiving less tokens than what they should because there is no slippage protection

0xStalin

medium

Decreasing liquidity from an LP Position is suceptible to a sandwich attack which could lead to users receiving less tokens than what they should because there is no slippage protection

Summary

The parameters used to decrease liquidity from LP Positions in the NonFungiblePositionManager contract diasbles the slippage protection that is built-in the NonfungiblePositionManager contract because amount0Min and amount1Min are sent as 0.

Vulnerability Detail

A certain percentage of liquidity from the LP Position is decreased each time an Order is Cancelled or Fullfiled but there is no slippage protection to prevent Liquidity Providers from receiving less tokens than what they really should receive. The issue is caused because the variables amount0Min and amount1Min are sent as 0. of the parameters that are sent to the NonFungiblePositionManager:decreaseLiquidity()

function _takeFromPosition(
      uint256 target,
      UniswapV3Pool pool,
      uint256 liquidityPercent,
      uint256 deadline
  ) internal returns (uint128, uint128) {
      ...

      NonFungiblePositionManager.DecreaseLiquidityParams memory params = NonFungiblePositionManager
          .DecreaseLiquidityParams({
              tokenId: target,
              liquidity: liquidity,
              //@audit-issue => Will disable the slippage protection that is built-in the NonFungiblePositionManager:decreaseLiquidity() function
              amount0Min: 0,
              amount1Min: 0,
              deadline: deadline
          });

      ...
  }

Attackers can manipulate the price of ticks with not huge amounts of liquidity

  • The LimitOrderRegistry::_takeFromPosition() calls the NonFungiblePositionManager::decreaseLiquidity(), and internally in the NonFungiblePositionManager contract, the process to actually decrease the liquidity is as follows:

Impact

Users can lose tokens because of sandwich attacks when Cancelling their orders or when their orders are fullfiled because the slippage protection mechanism that is built-in the NonFungiblePositionManager::decreaseLiquidity() is disabled due to incorrectly sending the arguments params.amount0Min and params.amount1Min set to 0.

Code Snippet

Tool used

Manual Review & NonfungiblePositionManager UniswapV3 Contract

Recommendation

  • Instead of sending the arguments params.amount0Min and params.amount1Min set to 0, make sure to implement a mechanism that allows the contract to compute a minimum amount of tokens to receive after decreasing the liquidity from the LP Position, otherwise, user's funds are at risk .

MohammedRizwan - Solmate safeTransferLib.sol functions does not check the codesize of the token address, which may lead to fund loss

MohammedRizwan

medium

Solmate safeTransferLib.sol functions does not check the codesize of the token address, which may lead to fund loss

Summary

Solmate safeTransferLib.sol functions does not check the codesize of the token address, which may lead to fund loss

Vulnerability Detail

Impact

In LimitOrderRegistry.sol contract, Most of the functions have used the safeTransfer() and safeTransferFrom() doesn't check the existence of code at the token address. This is a known issue while using solmate's libraries.

Per the Solmate safeTransferLib.sol,

Note that none of the functions in this library check that a token has code at all! .....

Hence using safeTransferLib.sol library may lead to miscalculation of funds and may lead to loss of funds , because if safetransfer() and safetransferfrom() are called on a token address that doesn't have contract in it, it will always return success, bypassing the return value check. Due to this protocol will think that funds has been transferred and successful , and records will be accordingly calculated, but in reality funds were never transferred.

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L7

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L499

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L579

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L724

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L855-L860

Tool used

Manual Review

Recommendation

Use openzeppelin's safeERC20 which takes care of token code existence.

kutugu - The oracle price update has a threshold, which is different from the actual price

kutugu

medium

The oracle price update has a threshold, which is different from the actual price

Summary

The chainlink oracle price update has a threshold, which is different from the actual price. Direct use of the oracle price may lead to inaccurate gasPrice calculations, and low charges may cause the protocol runs out of funds..

Vulnerability Detail

    function getGasPrice() public view returns (uint256) {
        // If gas feed is set use it.
        if (fastGasFeed != address(0)) {
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            uint256 timeSinceLastUpdate = block.timestamp - _timestamp;
            // Check answer is not stale.
            if (timeSinceLastUpdate > FAST_GAS_HEARTBEAT) {
                // If answer is stale use owner set value.
                // Multiply by 1e9 to convert gas price to gwei
                return uint256(upkeepGasPrice) * 1e9;
            } else {
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);
                return answer;
            }
        }
        // Else use owner set value.
        return uint256(upkeepGasPrice) * 1e9; // Multiply by 1e9 to convert gas price to gwei
    }

When tx is executed over chainlink automation's network, the tokens stored in the app are consumed, and the protocol will charge the user a fee to compensate for the cost of maintaining the chainlink automation application.
However, there is an update threshold for the oracle gas price. If the gas price is calculated directly using the oracle price, the actual gas price may be 0.5% higher than that of the actual, may cause the protocol runs out of funds.

Impact

The inaccurate gas price can result in the protocol losing money

Code Snippet

Tool used

Manual Review

Recommendation

Add 0.5% more than the value oracle returned

0xGoodess - performUpkeep has a upkeep fee depletion problem if any claim reverts due to fee too little

0xGoodess

medium

performUpkeep has a upkeep fee depletion problem if any claim reverts due to fee too little

Summary

performUpkeep has a upkeep fee depletion problem if any claim reverts due to fee too little

Vulnerability Detail

TradeManager has a registration on chainlink bot if the user desires to set it up by setting initialUpkeepFunds to non-zero value during initialise. The bot works by keep hitting checkUpkeep in the contract, and if the first bool is true, then it would call performUpkeep.

The logic in both checkUpkeep and performUpkeep rely on isOrderReadyForClaim, which is consistent. Once performUpkeep is done successfully, checkUpkeep would return false during a normal workflow.

However, when performUpkeep does not pass through due to gas being too little, the transaction would revert. Since Chainlink bot is not responsible for a reverting tx when the signal is correct (namely checkUpkeep is returning true), thus the bot could still deduce Upkeep fee from the account.

Impact

1 reverting claim in the claim set due to fee too little would completely depletes all upkeep fee since bot would keep calling performUpkeep and revert.

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/TradeManager.sol#L266

Tool used

Manual Review

Recommendation

add a bool performUpkeepFailed on checkUpkeep such that if a performUpkeep fails once, the signal turns into false and becomes unactionable since it would yield the same reverting result unless there are system changes like gasPrice change(s) in the limitOrderRegistry; the upkeep should be bricked anyway before the user intervenes.

function checkUpkeep(bytes calldata) external view returns (bool upkeepNeeded, bytes memory performData) {
     if(performUpkeepFailed)  {
             upkeepNeeded = false;
             return;
}

add a setter for user to toggle performUpkeepFailed.

SanketKogekar - No checks if the price returned by the chainlink oracle is up to date which can lead to fetching incorrect gas price

SanketKogekar

medium

No checks if the price returned by the chainlink oracle is up to date which can lead to fetching incorrect gas price

Summary

Possible loss of funds since there is no check to verify that the price returned by the chainlink oracle is up to date, which can lead to fetching incorrect gas price.

Vulnerability Detail

The estimatedFee which is passed tot the function _fulfillOrder is fetched from getGasPrice function which uses Chainlink oracle to get the price. The problem is that answer and other returned values from oracle are not validated and the obtained price is directly used.

Impact

Medium: Loss of funds for the protocol.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1447-L1465

Tool used

Manual Review

Recommendation

Add this type of checks:

`function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
    (uint80 roundID, int256 price, uint256 timestamp, uint256 updatedAt, )= registry.latestRoundData(base, quote);
    require(updatedAt >= roundID, "Stale price");
    require(timestamp != 0,"Round not complete");
    require(price > 0, "invalid price");

    // Extend the decimals to 1e18.
    return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
}`

0xeix - > instead of >= should be used when refunding in LimitOrderRegistry

0xeix

high

> instead of >= should be used when refunding in LimitOrderRegistry

Summary

In LimitOrderRegistry.sol, there is claimOrder() function in the end of which user can get the refund if his msg.value >= userClaim.feePerUser. Instead, > sign should be used

Vulnerability Detail

claimOrder() function allows a user to claim their tokens once their limit order has been fulfilled. Additionally, the users can get their refund if msg.value sent >= userClaim.feePerUser. However, >= doesn't make any sense as it will always revert after due to the subsequent checks (refund will be 0 if msg.value == userClaim.feePerUser and refund will never be > 0 so it doesn't make any sense to check for >=)

Impact

The function will always revert if msg.value == userClaim.feePerUser

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L728-731

Tool used

Manual Review

Recommendation

Change on:

if (msg.value > userClaim.feePerUser)

ginlee - getGasPrice() doesn't check If Arbitrum sequencer is down in Chainlink feeds

ginlee

medium

getGasPrice() doesn't check If Arbitrum sequencer is down in Chainlink feeds

Summary

When utilizing Chainlink in L2 chains like Arbitrum, it's important to ensure that the prices provided are not falsely perceived as fresh, even when the sequencer is down. This vulnerability could potentially be exploited by malicious actors to gain an unfair advantage.

Vulnerability Detail

There is no check:

  function getGasPrice() public view returns (uint256) {
        // If gas feed is set use it.
        if (fastGasFeed != address(0)) {
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            uint256 timeSinceLastUpdate = block.timestamp - _timestamp;
            // Check answer is not stale.
            if (timeSinceLastUpdate > FAST_GAS_HEARTBEAT) {
                // If answer is stale use owner set value.
                // Multiply by 1e9 to convert gas price to gwei
                return uint256(upkeepGasPrice) * 1e9;
            } else {
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);        
                return answer;
            }
        }
        // Else use owner set value.
        return uint256(upkeepGasPrice) * 1e9; // Multiply by 1e9 to convert gas price to gwei
    }

Impact

could potentially be exploited by malicious actors to gain an unfair advantage.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1447-L1462

Tool used

Manual Review

Recommendation

code example of Chainlink:
https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

constructor() {
        priceFeed = AggregatorV2V3Interface(
            0xD702DD976Fb76Fffc2D3963D037dfDae5b04E593
        );
        sequencerUptimeFeed = AggregatorV2V3Interface(
            0x371EAD81c9102C9BF4874A9075FFFf170F2Ee389
        );
    }

 // Check the sequencer status and return the latest price
    function getLatestPrice() public view returns (int) {
        // prettier-ignore
        (
            /*uint80 roundID*/,
            int256 answer,
            uint256 startedAt,
            /*uint256 updatedAt*/,
            /*uint80 answeredInRound*/
        ) = sequencerUptimeFeed.latestRoundData();


 // Answer == 0: Sequencer is up
        // Answer == 1: Sequencer is down
        bool isSequencerUp = answer == 0;
        if (!isSequencerUp) {
            revert SequencerDown();
        }


// Make sure the grace period has passed after the sequencer is back up.
        uint256 timeSinceUp = block.timestamp - startedAt;
        if (timeSinceUp <= GRACE_PERIOD_TIME) {
            revert GracePeriodNotOver();
        }

Duplicate of #65

trachev - High - Owner can accidentally withdraw users' funds

trachev

high

High - Owner can accidentally withdraw users' funds

Summary

The contract owner can accidentally withdraw user funds, due to the withdrawNative function.

Vulnerability Detail

A key functionality of the contract is that the owner can withdraw gas fees derived from users, who have called the claimOrder function. These fees can often be provided as WETH. The problem occurs due to the fact that in Uniswap V3 it is allowed to place limit orders on pools, where funds can be exchanged for WETH. Therefore after a fulfilled order, which has exchanged funds for WETH, the user's WETH will first be transfered to the contract, until the users individually calls claimOrder to acquire their exchanged funds. Before the claimOrder function has been called, the owner can accidentally evoke the withdrawNative function, which will withdraw gas per user fees, INCLUDING the WETH that has just been transfered to the contract, which belongs to users.
Here is an example:

  1. Alice adds a new order which will eventually swap some token for WETH after the order is ITM (in the money).
  2. The current exhange price moves and Alice's order is now ITM and can be fulfilled. So the performUpkeep function gets called, which consequently calls _fulfillOrder, which transfers the amountOut, in this case WETH, to the contract.
  3. Now in order for Alice to claim the funds, she just swapped, claimOrder needs to be called. But before she is able to do that, the owner decides to collect gas per user fees (this are different fees from the swap fees) and calls withdrawNative.
  4. Now Alice's funds are accidentally taken by the owner, and whenever she decides to call claimOrder the call would fail, due to lack of funds, or in some cases take funds that belong to other users.

Impact

Due to the fact that a substantial amount of funds can easily be taken by the owner, even though he may not be malicious, this vulnerability can have detrimental effects. A claimOrder transaction does not need to be frontran, nor does withdrawNative need to be called immediately after an order has been fulfilled, in order for the accident to occur. Furthermore, a lack of funds will occur and other users will also not be able to claim their liquidity.

Code Snippet

Below we can see that it is expected for the WRAPPED_NATIVE ERC20 address to be WETH's address.

 // @notice the token addres of the wrapped native token
    ERC20 public immutable WRAPPED_NATIVE; // Mainnet 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

The function that the owner can use to withdraw the WETH:

function withdrawNative() external onlyOwner {
        uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
        uint256 nativeBalance = address(this).balance;
        // Make sure there is something to withdraw.
        if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

        // transfer wrappedNativeBalance if it exists
        if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
        // transfer nativeBalance if it exists
        if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
    }

This is where fulfilled orders are transfered to the contract, where the params include recipient, pointing to address(this):
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1400

collectParams = NonFungiblePositionManager.CollectParams({
      tokenId: target,
      recipient: address(this),
      amount0Max: amount0Max,
      amount1Max: amount1Max
});
POSITION_MANAGER.collect(collectParams);

Tool used

Manual Review

Recommendation

Keep track of how much WETH the contract owes to users and subtract it from the entire WETH balance of the contract when calling withdrawNative.

Duplicate of #48

Dug - Protocol is unable to support any pools that include wrapped native tokens

Dug

medium

Protocol is unable to support any pools that include wrapped native tokens

Summary

The audit details indicate that the protocol should be able to support any ERC20 tokens which trade on Uniswap v3.

However, the protocol, as currently implemented, is unable to support any pools that user the wrapped native token as part of the trading pair as it will result in the owner being unable to withdraw fees without draining all pending claims in that token.

Vulnerability Detail

The main reason the protocol is unable to support trading pairs that include the wrapped native token is in how fee accounting in performed in the contract.

When a claim is made, the fee is taken in the native token or the wrapped, ERC20 version of it. This is done in the claimOrder function.

        if (msg.value >= userClaim.feePerUser) {
            // refund if necessary.
            uint256 refund = msg.value - userClaim.feePerUser;
            if (refund > 0) sender.safeTransferETH(refund);
        } else {
            WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
            // If value is non zero send it back to caller.
            if (msg.value > 0) sender.safeTransferETH(msg.value);
        }

The fee is held in the contract until withdrawn from the owner via the withdrawNative function.

    function withdrawNative() external onlyOwner {
        uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
        uint256 nativeBalance = address(this).balance;
        // Make sure there is something to withdraw.
        if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

        // transfer wrappedNativeBalance if it exists
        if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
        // transfer nativeBalance if it exists
        if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
    }

This function withdraws the entire balance of native tokens from the contract.

This conflicts with how token balances are held in the contract as orders are fulfilled. When an order if fulfilled the token is held in the contract until claimed by users.

This means that fulfilling any order that uses the wrapped native token will result in the contract holding a balance of the wrapped native token. The owner is then unable to withdraw their fees from the contract without also withdrawing all pending claims in that wrapped native token.

Impact

To support trading pairs that use the wrapped native token, the owner will be unable to withdraw fees, representing a loss of revenue for the protocol.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505-L515

Tool used

Manual Review

Recommendation

Use the tokenSwapFees mapping to account for the fees in the native token. This will allow the owner to withdraw their fees without withdrawing the pending claims.

        if (msg.value >= userClaim.feePerUser) {
            // refund if necessary.
            uint256 refund = msg.value - userClaim.feePerUser;
            if (refund > 0) sender.safeTransferETH(refund);
        } else {
            WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
+           tokenToSwapFees[WRAPPED_NATIVE] += userClaim.feePerUser;
            // If value is non zero send it back to caller.
            if (msg.value > 0) sender.safeTransferETH(msg.value);
        }
    function withdrawNative() external onlyOwner {
        uint256 wrappedNativeBalance = tokenToSwapFees[WRAPPED_NATIVE];
        uint256 nativeBalance = address(this).balance;
        // Make sure there is something to withdraw.
        if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

        // transfer wrappedNativeBalance if it exists
-       if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
+       if (wrappedNativeBalance > 0) {
+           tokenToSwapFees[WRAPPED_NATIVE] = 0;
+           WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
+       }
        // transfer nativeBalance if it exists
        if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
    }

Duplicate of #48

rugpull_detector - Invisible stop order can frontrun already placed limit order with slightly better price, thus lowering chance of limit order being fulfilled.

rugpull_detector

medium

Invisible stop order can frontrun already placed limit order with slightly better price, thus lowering chance of limit order being fulfilled.

Summary

Let's say that user A created limit order to sell WETH at 2000 USDC.
user B has created stop order to sell WETH at 1999 USDC, but this stop order is not visible to the market and only fulfilled by the bot that monitors current price.

When WETH' price keeps rising above 1990, then stop order fulfillment bot will create an limit order at 1999 by overriding user A's limit order with slightly better price.
As Chainlink automation keeper can process only 20 orders at one epch, user B's order get processed first and user A might have missed opportunity to fulfill its order.

Accidently price crashes to 1990 at next block.
user A have to wait another bullish moment to fulfill his order.
When user B has benefitted by fulling his stop order at the expense of user B.

Vulnerability Detail

To fulfill orders, performUpkeep() start searching from centerHead or centerTail, which is last processed tick.

function performUpkeep(bytes calldata performData) external {
    ...

  uint256 target = walkDirection ? data.centerHead : data.centerTail;
  ...

  for (uint256 i; i < maxFillsPerUpkeep; ++i) {
    if (target == 0) break;
    BatchOrder storage order = orderBook[target];
    ...

It will make most eligible order based on currentTick not to be processed first.

Impact

Stop order creator might prevent limit order to be fulfilled.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/59a83c6423834b9173b9c063ec1e505c5d9f1bbb/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L931

Tool used

Manual Review

Recommendation

Searching linked list should start from currentTick, rather than from centerHead and centerTail, which is last processed tick.

function performUpkeep(bytes calldata performData) external {
    ...

-     uint256 target = walkDirection ? data.centerHead : data.centerTail;
+    (uint256 head, uint256 tail) = _findSpot(pool, 0, currentTick, direction);
+    uint256 target = walkDirection ? head : tail;
  ...

  for (uint256 i; i < maxFillsPerUpkeep; ++i) {
    if (target == 0) break;
    BatchOrder storage order = orderBook[target];
    ...

mstpr-brainbot - Users may not cancel their orders if order goes ITM and back to OTM

mstpr-brainbot

medium

Users may not cancel their orders if order goes ITM and back to OTM

Summary

An order (OTM) can go to ITM and back to OTM without keepers noticing. In this case, cancelling the OTM order is impossible.

Vulnerability Detail

When an order is OTM it can go ITM and then back to OTM in a same block. Keepers are checking the checkUpkeep every block or so, they are not checking specific tx. If an order goes ITM and OTM back, there will be some fees accrued to that position in both assets. If the user tries to cancel the quote user will fail due to these lines

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L854-L862

Impact

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L750-L863

Tool used

Manual Review

Recommendation

Vagner - The protocol would not work with BNB like coins that revert on 0 values, even if it is stated that it should work with any coin traded on UniswapV3

Vagner

medium

The protocol would not work with BNB like coins that revert on 0 values, even if it is stated that it should work with any coin traded on UniswapV3

Summary

LimitOrderRegistry.sol checks after every interaction with the POSITION_MANAGER if the allowance is greater than 0 and approves to 0 if that's the case but in the case of BNB this would revert all the time.

Vulnerability Detail

BNB on ethereum mainnet reverts on every function if the value provided is 0, as you can see here https://etherscan.io/token/0xB8c77482e45F1F44dE1745F52C74426C631bDD52#code#L94 so if the protocol intends to interact with BNB, a token which is supported on UniswapV3 and has a high market cap, this would revert because of how the protocol tries to approve to 0 after IncreaseLiquidityParams or mint called on POSITION_MANAGER.

Impact

This would have medium impact since some tokens supported on UniswapV3 would not be able to interact with the protocol as it was stated

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1197-L1201
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1243-L1247

Tool used

Manual Review

Recommendation

The easiest way to solve this problem would be to use the SafeERC20.sol from OpenZeppelin instead of SafeTransferLib from solmate since the big difference between this two is the fact that the safe approve functions from OpenZeppelin always approves to 0, in the case where normal approve reverts so it will solve the problem with tokens that have the Race Condition check into their approve function like USDT https://github.com/OpenZeppelin/openzeppelin-contracts/blob/996168f1f114645b01a1c2e6909c9d98ec451203/contracts/token/ERC20/utils/SafeERC20.sol#L79-L82, so if you use the OpenZeppelin version there is no need to check the allowances after.

Dug - Owner can steal funds via malicious `fastGasFeed` contract

Dug

medium

Owner can steal funds via malicious fastGasFeed contract

Summary

The audit criteria has the owner as restricted and states that 'the owner should not be able to steal funds'. However, the owner can steal large amounts from claims by deploying and registering a malicious fastGasFeed contract that returns a very high gas price.

Vulnerability Detail

The LimitOrderRegistry has a setGasFeed that allows the owner to set any contract as the gas feed.

    function setFastGasFeed(address feed) external onlyOwner {
        fastGasFeed = feed;
    }

Fees are dynamically calculated using this gas feed in the performUpkeep function.

    function performUpkeep(bytes calldata performData) external {
        ...
        uint256 estimatedFee = uint256(upkeepGasLimit * getGasPrice());

This means that the owner can swap out the fastGasFeed contract at anytime as a way to charge very high, illegitimate fees to unsuspecting users, effectively stealing their funds.

Additionally, the owner can manipulate the minimumAssets amount. By raising this amount, they are able to increase how much they can steal. This is because fees can be inflated to the value of the smallest order in the batch.

Once setting a malicious fastGasFeed, the owner can call performUpkeep then claimOrder for each user in the batch, ensuring that the inflated fees are collected.

Impact

The owner can steal funds from users.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L482-L484

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L914-L962

Tool used

Manual Review

Recommendation

If the owner is restricted, they should not be able to set the gas feed independently.

Duplicate of #54

MohammedRizwan - In LimitOrderRegistry.sol, deadline using functions does not verify that the deadline has passed/expired

MohammedRizwan

medium

In LimitOrderRegistry.sol, deadline using functions does not verify that the deadline has passed/expired

Summary

In LimitOrderRegistry.sol, deadline using functions does not verify that the deadline has passed/expired

Vulnerability Detail

Impact

In LimitOrderRegistry.sol contract, deadline is used in functions like newOrder(), cancelOrder(), _mintPosition(), etc to protect replay protection in contract. The issue here deadline is not validating if the newOrder/cancelOrder/mintPosition is still running or not, Without deadline verification the functions keeps allowing to be made after the stipulated time or deadline. The deadline variable used in the functions so that the order/minting should not be validated after the deadline is passed. deadline is ensuring the order/mint as an expiry for that particular duration and helping to void the order once it is passed. Some functions of LimitOrderRegistry.sol contract does use the deadline parameter but does not verify it.

File: src/LimitOrderRegistry.sol

560    function newOrder(
561        UniswapV3Pool pool,
562        int24 targetTick,
563        uint128 amount,
564        bool direction,
        uint256 startingNode,
        uint256 deadline
    ) external whenNotShutdown returns (uint128) {
File: src/LimitOrderRegistry.sol

750    function cancelOrder(
751        UniswapV3Pool pool,
752        int24 targetTick,
753        bool direction,
754        uint256 deadline
755    ) external returns (uint128 amount0, uint128 amount1, uint128 batchId) {
File: src/LimitOrderRegistry.sol

1159    function _mintPosition(
1160        PoolData memory data,
1161        int24 upper,
1162        int24 lower,
1163        uint128 amount0,
1164        uint128 amount1,
1165        bool direction,
1166        uint256 deadline
1167    ) internal returns (uint256) {
File: src/LimitOrderRegistry.sol

    function _addToPosition(
        PoolData memory data,
        uint256 positionId,
        uint128 amount0,
        uint128 amount1,
        bool direction,
        uint256 deadline
    ) internal {
File: src/LimitOrderRegistry.sol

1299    function _fulfillOrder(
1300        uint256 target,
1301        UniswapV3Pool pool,
1302        BatchOrder storage order,
1303        uint256 estimatedFee,
1304        uint256 deadline
1305    ) internal {

Take an example of how Uniswap has verified the deadline on functions.

    modifier ensure(uint deadline) {
        require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
        _;
    }

Reference link

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L560-L568

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L750-L755

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1299-L1305

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1213-L1223

Tool used

Manual Review

Recommendation

Add below check where deadline is used in functions.

+        require(deadline >= block.timestamp, 'LimitOrderRegistry: Expired');

ginlee - Solmate safetransfer and safetransferfrom does not check the code size of the token address, which may lead to funding loss

ginlee

medium

Solmate safetransfer and safetransferfrom does not check the code size of the token address, which may lead to funding loss

Summary

Vulnerability Detail

The safetransfer and safetransferfrom don't check the existence of code at the token address. This is a known issue while using solmate's libraries. Hence this may lead to miscalculation of funds and may lead to loss of funds, because if safetransfer() and safetransferfrom() are called on a token address that doesn't have a contract in it, it will always return success, bypassing the return value check.

Impact

Due to this protocol will think that funds have been transferred successfully, and records will be accordingly calculated, but in reality, funds were never transferred. So this will lead to miscalculation and possibly loss of funds

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L398
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L499
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L512
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L579
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L724
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L733
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L855
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L859

Tool used

Manual Review

Recommendation

Use openzeppelin's safeERC20 or implement a code existence check

mstpr-brainbot - Owner can steal the LINK tokens from chainlink upkeep registry contract

mstpr-brainbot

medium

Owner can steal the LINK tokens from chainlink upkeep registry contract

Summary

Owner of the LimitOrderRegistry contract can steal the LINK tokens in the keeper registry contract.

Vulnerability Detail

In order for the order books to be automated, chainlink keepers needed. However, the funding of keepers with LINK tokens are not covered in the LimitOrderRegistry contract. There is only the initial funding been made by the owner. Anyone can fund a keeper job ID in chainlink, so if community decides to fund the keeper, they can do it since it doesn't have any access control meaning that anyone can fund the keepers with LINK tokens. However, the keep up admin can withdraw LINK tokens to any address which is the owner of the LimitOrderRegistry contract. Therefore, if the community funds the keeper the owner can steal these LINK tokens.

Impact

image As we see in the Sherlock docs for the contest, owner shouldn't be able to steal user funds. Although the funding of keepers work is not clearly mentioned in the docs the owner can potentially steal all the LINK that the keeper registry has. Since anyone can fund the keeper with LINK and owner can withdraw it, I'll categorize this as owner can steal tokens.

Code Snippet

Chainlink, withdrawing keeper LINK funds

https://github.com/smartcontractkit/chainlink/blob/cff29c0b86fb8e53db9088c1e4bf60427bb43a0a/contracts/src/v0.8/automation/1_2/KeeperRegistry1_2.sol#L323-L347

Chainlink, funding the keep up ID
https://github.com/smartcontractkit/chainlink/blob/cff29c0b86fb8e53db9088c1e4bf60427bb43a0a/contracts/src/v0.8/automation/1_2/KeeperRegistry1_2.sol#L293-L298

Tool used

Manual Review

Recommendation

Make the owner of the keep up ID address(this), and add functionality to manage the keeper related functions

mstpr-brainbot - Owner can take advantage of the keeper contracts functions to harm users

mstpr-brainbot

medium

Owner can take advantage of the keeper contracts functions to harm users

Summary

Owner can call few functions in keeper registry to harm users.

Vulnerability Detail

The owner of the upkeep job in keeper registry is the owner of the LimitOrderRegistry. There are some functions that are crucial for the healthy maintenance of the LimitOrderRegistry contract. If owner is malicious, owner can cancel the upkeep for a pool that would make that pools keepers to not work. What's worse is that it is not possible to re-register the pool to upkeep because of these lines

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L393

Impact

Although the owner can't really steal the funds from users directly, owner can mess with the keeper settings. If owner manages to deactivate all the upkeep jobs the fees that owner takes when user claims is basically a stolen fund since the keeper service is permanently closed.

There are bunch of keeper functions that owner can take advantage to harm users. I don't want to put single issues for all of them so I'll list the potential dangerous functions for owner to call to harm the users.

Code Snippet

Functions in keeper contract that owner can take advantage

https://github.com/smartcontractkit/chainlink/blob/1ec921f4aa288dbdd7e67df151b4adc8b4a339eb/contracts/src/v0.8/automation/2_0/KeeperRegistryLogic2_0.sol#L176-L205

https://github.com/smartcontractkit/chainlink/blob/1ec921f4aa288dbdd7e67df151b4adc8b4a339eb/contracts/src/v0.8/automation/2_0/KeeperRegistryLogic2_0.sol#L223-L256

https://github.com/smartcontractkit/chainlink/blob/1ec921f4aa288dbdd7e67df151b4adc8b4a339eb/contracts/src/v0.8/automation/2_0/KeeperRegistryLogic2_0.sol#L302-L359

Tool used

Manual Review

Recommendation

If the idea of the contract is to provide a fully decentralized and permissionless software, keeper admin functions should be in a contract aswell and there should be some validations on how owner can calls them.

MohammedRizwan - Approve 0 first for tokens like USDT

MohammedRizwan

medium

Approve 0 first for tokens like USDT

Summary

Approve 0 first for tokens like USDT

Vulnerability Detail

Impact

Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. For example Tether (USDT)’s approve() function will revert if the current approval is not zero, to protect against front-running changes of approvals. Link to usdt contract reference(SLOC 199-209)

approve is actually vulnerable to a sandwich attack as explained in the following document and this check for allowance doesn't actually avoid it.

Reference document link- https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit

In ERC20, front running attack is possible via approve() function,

Reference link for better understanding- https://blog.smartdec.net/erc20-approve-issue-in-simple-words-a41aaf47bca6

In the protocol, all functions using safeApprove() must be first approved by zero. Below functions are called to make ERC20 approvals. But it does not approve 0 first.

File: src/LimitOrderRegistry.sol

    function _mintPosition(
        PoolData memory data,
        int24 upper,
        int24 lower,
        uint128 amount0,
        uint128 amount1,
        bool direction,
        uint256 deadline
    ) internal returns (uint256) {
        if (direction) data.token0.safeApprove(address(POSITION_MANAGER), amount0);
        else data.token1.safeApprove(address(POSITION_MANAGER), amount1);
    
       // Some code

and

File: src/LimitOrderRegistry.sol

    function _addToPosition(
        PoolData memory data,
        uint256 positionId,
        uint128 amount0,
        uint128 amount1,
        bool direction,
        uint256 deadline
    ) internal {
        if (direction) data.token0.safeApprove(address(POSITION_MANAGER), amount0);
        else data.token1.safeApprove(address(POSITION_MANAGER), amount1);

       // Some code

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1168-L1169

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1223-L1224

Tool used

Manual Review

Recommendation

Approve 0 first and Use OpenZeppelin’s SafeERC20.

mstpr-brainbot - New orders can be created without chainlink upkeep job

mstpr-brainbot

medium

New orders can be created without chainlink upkeep job

Summary

The addition of a new pool can lead to premature order creation because the process doesn't wait for keeper registration confirmation. This could result in a shortage of chainlink keepers, causing potential issues for users creating orders immediately after a new pool is registered.

Vulnerability Detail

Consider the scenario where a new pool is added to our system. As part of this process, a new upkeep job is also created in the Keeper Registry. However, this procedure isn't instantaneous. When the registerUpkeep function is invoked, the request is initially set to 'pending' status, and it requires approval from the registrar owner to finalize the upkeep registration.

The twist here is that the current code allows users to place orders as soon as the new pool is added to the LimitOrderRegistry contract, even without waiting for the keeper registrar's approval. Consequently, any users who rush to create new orders immediately after a new pool is registered could face an unexpected shortage of Chainlink keepers. This is not in line with the desired behavior outlined in the contract's business logic.

Impact

Code Snippet

https://github.com/smartcontractkit/chainlink/blob/b698a3fc24884bf1dfe48c82c26340e005eee0e8/contracts/src/v0.8/automation/2_0/KeeperRegistrar2_0.sol#L352-L408

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L391-L443

Tool used

Manual Review

Recommendation

mstpr-brainbot - Owner can steal users WETH-ETH by setting gasPrice higher

mstpr-brainbot

high

Owner can steal users WETH-ETH by setting gasPrice higher

Summary

Owner can make a dummy chainlink interface contract to inflate the gas price to get more fees from users.

Vulnerability Detail

When some positions are fulfilled by the performUpkeep call the fee is calculated as follows

uint256 estimatedFee = uint256(upkeepGasLimit * getGasPrice());

getGasPrice() function is also follows:

function getGasPrice() public view returns (uint256) {
        // If gas feed is set use it.
        if (fastGasFeed != address(0)) {
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            uint256 timeSinceLastUpdate = block.timestamp - _timestamp;
            // Check answer is not stale.
            if (timeSinceLastUpdate > FAST_GAS_HEARTBEAT) {
                // If answer is stale use owner set value.
                // Multiply by 1e9 to convert gas price to gwei
                return uint256(upkeepGasPrice) * 1e9;
            } else {
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);
                return answer;
            }
        }
        // Else use owner set value.
        return uint256(upkeepGasPrice) * 1e9; // Multiply by 1e9 to convert gas price to gwei
    }

From the above snippet we can say that if the fastGasFeed address is settable by owner, which is, any _answer can be returned. Owner can make a fake chainlink interfaced contract and returns the price and the timestamps as legit values such that the contract accepts them. Finally, the estimated fee will be calculated as abnormally high value hence, the owner can take an inflated fee amount from the users. Especially if the user is paying the gas via WETH and has a max approval, owner can take advantage of this and sets the gasPrice to something very high such that when the user claims the order, owner receives the entire balance of WETH that the user holds.

Impact

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L482-L484

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L914-L962

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1447-L1465

Tool used

Manual Review

Recommendation

Duplicate of #54

xiaoming90 - Malicious users could dilute and reduce the fee

xiaoming90

high

Malicious users could dilute and reduce the fee

Summary

Malicious users could dilute the feePerUser to reduce the fee they must pay when claiming orders. This lead to a loss of assets due to lesser fees being collected for the owner and a loss of opportunity costs for the users.

Vulnerability Detail

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1309

File: LimitOrderRegistry.sol
1299:     function _fulfillOrder(
1300:         uint256 target,
1301:         UniswapV3Pool pool,
1302:         BatchOrder storage order,
1303:         uint256 estimatedFee,
1304:         uint256 deadline
1305:     ) internal {
1306:         // Save fee per user in Claim Struct.
1307:         uint256 totalUsers = order.userCount;
1308:         Claim storage newClaim = claim[order.batchId];
1309:         newClaim.feePerUser = uint128(estimatedFee / totalUsers);
1310:         newClaim.pool = pool;
..SNIP..

As per Line 1309, all users within a batch order share the gas cost for fulfilling the order. The cost is split equally based on the number the users.

However, the problem is that if the minimum asset is not optimal (lower than expected), it will be cost-effective for a malicious user to use multiple different wallet addresses to submit the small order to dilute the feePerUser.

Assume the following:

  • upkeepGasLimit = 300_000
  • upkeepGasPrice = 150
  • 1 ETH worth 2000 USD

In this case, the estimatedFee for fulfilling each batch order will be computed as follows:

estimatedFee = upkeepGasLimit * gasPrice
estimatedFee = 300,000 * 150 Gwei = 45,000,000 Gwei = 0.045 ETH

The estimatedFee will be 0.045 ETH, which is worth 90 USD.

If Bob is the only user within the batch order, he has to pay 90 USD worth of ETH all by himself when claiming the order.

If the minimum asset of a new order is set to 5 USDC, Bob could use two unique wallet addresses to create a new order within the same batch order by depositing a minimum sum of 5 USDC. In total, he spent 10 USDC to carry out the attack.

Subseqently, the estimatedFee will be divided by 3 users, which results in a feePerUser of 30 USD. Thus, when Bob claims his order, he only needs to pay 30 USD instead of 90 USD as a fee (60 USD cheaper)

Bob has no intention of claiming the two small orders that he used for the exploitation. Thus, he will forfeit around 10 USD worth of assets. In addition, he will not be paying 60 USD worth of the fee to the owner since he will not call the claimOrder function.

In summary, Bob saved 50 USD (90 - 30 - 10) through this exploit, and the owner lost around 60 USD of the fee.

Impact

The immediate impact is a loss of assets for the owner due to lesser fees being collected from the users.

If the losses are significant, it will indirectly affect the owner's ability to replenish the LINK tokens needed to fund the Chainlink Automation. If the LINK tokens were insufficient, the user's orders might not be fulfilled when they are already ITM, potentially leading to a loss of opportunity costs for the users. In addition, since fees are only charged on fulfilled orders, fewer fulfilled orders also mean there will be lesser fees collected.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1309

Tool used

Manual Review

Recommendation

One potential solution is to collect the fee in advance based on the current number of users in a batch order. This should provide a rough estimation of the expected feePerUser when the order is fulfilled. When the users claim the order, any excess fee will be refunded, or any deficit will be collected from the users.

This would minimize the owner's loss during an attack. Additionally, it would make the attack unprofitable regardless of the minimum sum configured by the owner, which will benefit users who might not know how to compute the optimal minimum asset for each token to mitigate this risk.

Also, the optimal minimum asset to mitigate this risk might differ from the optimal minimum asset needed to prevent users from spamming small orders.

Vagner - The slippage in `_mintPosition` and `_addToPosition` is hardcoded and too strict which may lead to the protocol being unusable even in low volatility periods

Vagner

medium

The slippage in _mintPosition and _addToPosition is hardcoded and too strict which may lead to the protocol being unusable even in low volatility periods

Summary

The function that increases the liquidity into a UniswapV3 pool uses a hardcoded value of slippage which is set to 0.01% of the amount which can be too low most of the time.

Vulnerability Detail

The functions that creates new positions or increase the liquidity of a position are _mintPosition and _addToPosition , both of them calculates the amount0Min/amount1Min as a hardcoded value, being 99.91% of the amount0/amount1 values https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1226-L1227
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1172-L1173
This can have multiple implication but the most important one would be that this slippage value can be too low for most of the pools so any interaction with any of the UniswapV3 pools could not work or revert most of the time, even in some low volatility periods where the values of the pools could change by a little. Consider the fact that even UniswapV3 states that if you use slippage values lower than 0.05% the transaction may revert, their default slippage tolerance being 0.1%.

Impact

This would have a medium impact since the protocol can be unusable multiple times because of how strict the slippage is.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1226-L1227
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1172-L1173

Tool used

Manual Review

Recommendation

There are different ways to solve this.

The first one is to let users determine the maximum slippage they're willing to take, the protocol front-end should set the recommended value for them or the second one is have a slippage control parameters that's set by the owner that can be changed anytime. Personally I would suggest the first one since the second one require another person instead of the user.

0xeix - safeTransferETH function has incorrect parameters

0xeix

high

safeTransferETH function has incorrect parameters

Summary

In LimitOrderRegistry.sol, safeTransferETH is used multiple times but it doesn't have "to" parameter specified.

Vulnerability Detail

Solmate SafeTransferLib library has internal safeTransferETH() function with "to" and "value" parameters. However, in the LimitOrderRegistry, safeTransferETH() doesn't have different implementation so it uses the basic one. That means that function doesn't work as it should be as "to" is not specified

Impact

The funds will not be transferred to address(this) as it's not specified as "to" when using safeTransferETH()

Code Snippet

safeTransferETH() from solmate:

https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol#L15

safeTransferETH() usage in LimitOrderRegistry.sol:

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L514
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L731
https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L735

Tool used

Manual Review

Recommendation

Add address(this) parameter specified as "to" in safeTransferETH

pep7siup - Contract operator lacks address-zero validations

pep7siup

medium

Contract operator lacks address-zero validations

Summary

The LimitOrderRegistry contract has a vulnerability where certain functions lack address-zero checks for specific parameters. This issue can potentially result in lost funds and render the contract inoperable if any of these parameters are mistakenly set to the zero address.

Vulnerability Detail

The vulnerability is presented in the constructor of the LimitOrderRegistry contract. The following parameters are not checked for the zero address:

  • _owner
  • _positionManager
  • wrappedNative
  • link
  • _registrar
  • _fastGasFeed
constructor(
        address _owner,
        NonFungiblePositionManager _positionManager,
        ERC20 wrappedNative,
        LinkTokenInterface link,
        IKeeperRegistrar _registrar,
        address _fastGasFeed
    ) Owned(_owner) {
        POSITION_MANAGER = _positionManager;
        WRAPPED_NATIVE = wrappedNative;
        LINK = link;
        registrar = _registrar;
        fastGasFeed = _fastGasFeed;
    }

If any of these parameters are set to the zero address, it can lead to unintended behavior and potential loss of funds. The contract may become inoperable, as certain operations rely on these addresses.

Impact

The failure to check for the zero address in these parameters can have severe consequences. It can result in the loss of funds and make the contract non-functional, affecting its intended operations.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/59a83c6423834b9173b9c063ec1e505c5d9f1bbb/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L350-L363

Tool used

Manual Review

Recommendation

It is recommended to implement address-zero checks for the above-mentioned parameters.

0xeix - Missing input validation allows adversary to get the owed amount for another user when claiming orders

0xeix

high

Missing input validation allows adversary to get the owed amount for another user when claiming orders

Summary

In LimitOrderRegistry.sol, there is claimOrder() function where the owed to the user amount is calculated and sent to the user afterwards. The problem is that it doesn't send anything to the msg.sender but the user specified in the parameters

Vulnerability Detail

When claiming orders in LimitOrderRegistry, user can specify "batchId" and "user" parameters. However, it only checks the depositAmount like this and nothing else:

uint256 depositAmount = batchIdToUserDepositAmount[batchId][user];

After that, only owed amount is calculated based on the batchId and there is no any validation on how the batchId is related to the user specified in the parameters

Impact

Users can lose their money because of missing validation

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L696-724

Tool used

Manual Review

Recommendation

Add input validation for the user parameter to make sure that batchId actually relates to the msg.sender

XDZIBEC - Missing Update of Order Linked List Pointers in _removeOrderFromList

XDZIBEC

medium

Missing Update of Order Linked List Pointers in _removeOrderFromList

Summary

the removal of an order from a linked list, there is a failure to properly update the head and tail pointers of the order being removed.

Vulnerability Detail

 function _removeOrderFromList(uint256 target, UniswapV3Pool pool, BatchOrder storage order) internal {
        // grab the centerHead and centerTail from mapping
        uint256 centerHead = poolToData[pool].centerHead;
        uint256 centerTail = poolToData[pool].centerTail;

        if (target == centerHead) {
            // if the target is the current center, set it to the center orders head
            uint256 newHead = orderBook[centerHead].head;
            // it is okay to be zero
            poolToData[pool].centerHead = newHead;
        } else if (target == centerTail) {
            // do the same check with tail
            uint256 newTail = orderBook[centerTail].tail;
            // it is okay to be zero
            poolToData[pool].centerTail = newTail;
        }

        // Remove order from linked list.
        orderBook[order.tail].head = order.head;
        orderBook[order.head].tail = order.tail;
        order.head = 0;
        order.tail = 0;
    }

the order's head and tail values are updated to remove the order from the linked list. However, it seems that the code fails to update the head and tail values of the order being removed.

// Remove order from linked list.
orderBook[order.tail].head = order.head;
orderBook[order.head].tail = order.tail;

Impact

  • leading to incorrect removal of orders from the linked list,

Code Snippet

Tool used

Manual Review

Recommendation

to fix the issues :

// Remove order from linked list.
if (order.head != 0) {
    orderBook[order.tail].head = order.head;
}
if (order.tail != 0) {
    orderBook[order.head].tail = order.tail;
}

pep7siup - LimitOrderRegistry.getGasPrice() might return incorrect results

pep7siup

medium

LimitOrderRegistry.getGasPrice() might return incorrect results

Summary

The function getGasPrice() in the LimitOrderRegistry contract has a vulnerability where the return value of ChainlinkAggregator(fastGasFeed).latestRoundData() is not adequately validated, potentially resulting in incorrect gas price calculations.

Vulnerability Detail

In the code snippet provided, the function getGasPrice() retrieves the latest round data from a Chainlink aggregator contract. However, it does not validate if the value of _answer is positive before casting it to a uint256 value. If _answer is negative, the casting operation can result in a large uint256 value, leading to an inflated gas price.

function getGasPrice() public view returns (uint256) {
        ...
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            ...
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);
                return answer;
            ...
    }

Impact

The incorrect gas price calculation can result in a high estimated gas fee for users, potentially causing them to pay more than necessary for their transactions.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/59a83c6423834b9173b9c063ec1e505c5d9f1bbb/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1447-L1465

Tool used

Manual Review

Recommendation

To mitigate this issue, it is recommended to add a validation check to ensure that _answer is greater than zero before casting it to a uint256 value. The following code snippet demonstrates the recommended change:

	(, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
+	require(_answer > 0, "Chainlink _answer <= 0");

0xGoodess - a user has no way to cancel/withdraw an order that stays between lower/upper tick for a prolonged period of time in LimitOrderRegistry

0xGoodess

high

a user has no way to cancel/withdraw an order that stays between lower/upper tick for a prolonged period of time in LimitOrderRegistry

Summary

a user has no way to cancel/withdraw an order that stays between lower/upper tick for a prolonged period of time in LimitOrderRegistry.

Vulnerability Detail

Currently there are two ways for user to retrieve fund in the order management flow.

  1. ClaimOrder
  2. CancelOrder

ClaimOrder would get back the fully executed order, once the order turns from OTM (both lower and upper tick is beyond the currentTick), to ITM (where both lower and upper tick are within the currentTick, namely input fully turned into the other side).
This is done by the bot which does DecreaseLiquidity against the pool, retrieving a fully executed order and mark the fund isReadyForClaim.

CancelOrder, on the other hand, would allow a user to get back his/her original sent-in fund, provided the input is not touched, or the order is still in OTM state.

However, if the order is in between, namely the currentTick is within the lower/upper tick, or the order is in a partially filled state, there is no way for the user to retrieve the fund, or get back the fund against it. This causes an issue since the user might have thought:

  1. he/she configured a range that he/she thinks is too wide now.
  2. The currentTick simply stays within the targeted range for a prolonged period of time.

At worst, there is also a chance that the user mis-configure, and send in a limit order that has maximal lower/upper range as possible, and he/she only realizes it after currentTick hits (the order becomes MIXED). In this case, he/she essentially lose access to the fund (imagine someone sends a limit order to buy ETH from $1 to $1949 (current price as of writing is $1950), or a limit order to sell ETH from $1951 to $100,000 for example.

Impact

No way to withdraw fund when the order is "partially filled", namely the currentTick stays in between the targeted lower/upper tick. A user risks fund being stuck if the lower/upper tick is configured to be too wide, or simply the market price stays there.

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L854-L862

Tool used

Manual Review

Recommendation

  1. Allow a MIXED order to be cancelled.
  2. Remove the bipolar amount0 / amount1 non-zero checks on cancelOrder, such that a partially filled order can also be cancelled.
...
---                if (status != OrderStatus.OTM) revert LimitOrderRegistry__OrderITM(tick, targetTick, direction);
+++               if (status != OrderStatus.OTM || status != OrderStatue.MIXED) revert LimitOrderRegistry__OrderITM(tick, targetTick, direction);
...
...
        if (order.direction) {
            if (amount0 > 0) poolToData[pool].token0.safeTransfer(sender, amount0);
---            else revert LimitOrderRegistry__NoLiquidityInOrder();
---            if (amount1 > 0) revert LimitOrderRegistry__AmountShouldBeZero();
+++         poolToData[pool].token1.safeTransfer(sender, amount1);
        } else {
            if (amount1 > 0) poolToData[pool].token1.safeTransfer(sender, amount1);
---            else revert LimitOrderRegistry__NoLiquidityInOrder();
---            if (amount0 > 0) revert LimitOrderRegistry__AmountShouldBeZero();
+++.         poolToData[pool].token0.safeTransfer(sender, amount0);
        }

Actually the snippet can be refactored to a transfer of both token0 and token1 for both directions, but leave as it is for easier diff.

mstpr-brainbot - If the token has 0-2 decimals user can get sandwitched

mstpr-brainbot

medium

If the token has 0-2 decimals user can get sandwitched

Summary

Vulnerability Detail

Since the contract should be adaptable with every univ3 tokens, there is an edge case with the 2 decimal tokens. When liquidity is added to the position the minimumOut is calculated as follows:

uint128 amount0Min = amount0 == 0 ? 0 : (amount0 * 0.9999e4) / 1e4;
uint128 amount1Min = amount1 == 0 ? 0 : (amount1 * 0.9999e4) / 1e4;

if the token0 has 2 decimals and users adds liquidity with that token with an amount0 of 1000 (10 tokens with 2 decimal precision), amount0Min will be calculated as 0 due to division truncates the result towards zero. If the amount0Min is 0 Sandwitch attack threads will be viable for this operation.

Impact

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1215-L1241

Tool used

Manual Review

Recommendation

p0wd3r - If the request for registerUpkeep is set to pending status, the protocol functionality will be disabled.

p0wd3r

high

If the request for registerUpkeep is set to pending status, the protocol functionality will be disabled.

Summary

If the request for registerUpkeep is set to pending status, the protocol functionality will be disabled.

Vulnerability Detail

In the registerUpkeep function of Chainlink, it will determine whether to set the request as pending based on the configuration of the registrar. Additionally, only the owner of the registrar can approve pending requests.

https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/automation/2_0/KeeperRegistrar2_0.sol#L371-L382

    uint256 upkeepId;
    RegistrarConfig memory config = s_config;
    if (_shouldAutoApprove(config, sender)) {
      s_config.approvedCount = config.approvedCount + 1;

      upkeepId = _approve(params, hash);
    } else {
      uint96 newBalance = s_pendingRequests[hash].balance + params.amount;
      s_pendingRequests[hash] = PendingRequest({admin: params.adminAddress, balance: newBalance});
    }

    return upkeepId;

https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/automation/2_0/KeeperRegistrar2_0.sol#L188-L219

  ) external onlyOwner {
    PendingRequest memory request = s_pendingRequests[hash];
    if (request.admin == address(0)) {
      revert RequestNotFound();
    }
    bytes32 expectedHash = keccak256(abi.encode(upkeepContract, gasLimit, adminAddress, checkData, offchainConfig));
    if (hash != expectedHash) {
      revert HashMismatch();
    }
    delete s_pendingRequests[hash];
    _approve(

In the setupLimitOrder of GFX, there is no check whether the return value of registerUpkeep is 0.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L417-L428

                ERC20(address(LINK)).safeApprove(address(registrar), initialUpkeepFunds);
                RegistrationParams memory params = RegistrationParams({
                    name: "Limit Order Registry",
                    encryptedEmail: abi.encode(0),
                    upkeepContract: address(this),
                    gasLimit: uint32(maxFillsPerUpkeep * upkeepGasLimit),
                    adminAddress: owner,
                    checkData: abi.encode(pool),
                    offchainConfig: abi.encode(0),
                    amount: uint96(initialUpkeepFunds)
                });
                registrar.registerUpkeep(params);

Therefore, even if the request enters the pending state, it can still be completed setup.
This may lead to the following scenarios:

  1. Owner executes setUp, putting the request into pending status.
  2. User creates an order.
  3. The price has reached the user's target price, but the upkeep is still in a pending state, resulting in the automation not triggering properly and the transaction unable to be executed. The user has suffered losses.

Impact

The order cannot be completed successfully, causing losses to the user.

Code Snippet

Tool used

Manual Review

Recommendation

Check the return value of registerUpkeep, if it is 0 then revert.

0xStalin - Silent overflow can affect the collection of fees from the NonFungiblePositionManager

0xStalin

medium

Silent overflow can affect the collection of fees from the NonFungiblePositionManager

Summary

Fees can get stuck in the NonFungiblePositionManager contract when calling the collect() because of how the fees to collect are computed when partial liquidity is removed

Vulnerability Detail

After liquidity has been decreased from the LP Position in the NonFungiblePositionManager contract it is computed the amount of fees to collect based on the amount of tokens that were received.

Impact

Fees can get stuck in the NonFungiblePositionManager contract when calling the collect() because of how the fees to collect are computed when partial liquidity is removed

Code Snippet

Tool used

Manual Review

Recommendation

// Use a safe downcast function
function _safeUint128(uint256 x) internal pure returns (uint128) {
    require(x <= uint256(type(uint128).max));
    return uint128(x);
}

mstpr-brainbot - Gas limit and gas price is not operable with L2's

mstpr-brainbot

high

Gas limit and gas price is not operable with L2's

Summary

Constant gas limit and gas price are not operable with L2's.

Vulnerability Detail

Max gas limit and gas price are hardcoded as constants in the contracts bytecode meaning that they can't change. However, these values are not operable with Arbitrum.

Here a random tx found in arbitrum
https://arbiscan.io/tx/0xad0670e42f29ca4a7457b970601df558b5e352c2fe4fba4de8ac8aa9047e3d71

as noticed, the gas limit higher than the contracts constant values.

Impact

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L212-L218

Code Snippet

Tool used

Manual Review

Recommendation

p0wd3r - fastGasFeed's answer should check if it is greater than MAX_GAS_PRICE.

p0wd3r

medium

fastGasFeed's answer should check if it is greater than MAX_GAS_PRICE.

Summary

fastGasFeed's answer should check if it is greater than MAX_GAS_PRICE.

Vulnerability Detail

First of all, the owner is restricted, which means that the actions of the owner need to be constrained.

Q: Is the admin/owner of the protocol/contracts TRUSTED or RESTRICTED?
restricted. the owner should not be able to steal funds.

Owner can set the address of fastGasFeed, which is used to obtain gas price.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L482-L484

    function setFastGasFeed(address feed) external onlyOwner {
        fastGasFeed = feed;
    }

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1447-L1465

    function getGasPrice() public view returns (uint256) {
        // If gas feed is set use it.
        if (fastGasFeed != address(0)) {
            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();
            uint256 timeSinceLastUpdate = block.timestamp - _timestamp;
            // Check answer is not stale.
            if (timeSinceLastUpdate > FAST_GAS_HEARTBEAT) {
                // If answer is stale use owner set value.
                // Multiply by 1e9 to convert gas price to gwei
                return uint256(upkeepGasPrice) * 1e9;
            } else {
                // Else use the datafeed value.
                uint256 answer = uint256(_answer);
                return answer;
            }
        }
        // Else use owner set value.
        return uint256(upkeepGasPrice) * 1e9; // Multiply by 1e9 to convert gas price to gwei
    }

As a fallback solution for gas price, upkeepGasPrice is restricted from exceeding MAX_GAS_PRICE.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L471-L475

    function setUpkeepGasPrice(uint32 gasPrice) external onlyOwner {
        // should revert if the gas price provided is greater than the provided max gas price.
        if (gasPrice > MAX_GAS_PRICE) revert LimitOrderRegistry__InvalidGasPrice();
        upkeepGasPrice = gasPrice;
    }

But as the owner, there are no restrictions on the arbitrarily controllable fastGasFeed answer.

Impact

Owner can set gas price arbitrarily, excessively high gas prices prevent users from claiming and cause a DoS on the protocol's functionality.

Code Snippet

Tool used

Manual Review

Recommendation

fastGasFeed's answer should check if it is greater than MAX_GAS_PRICE.

Duplicate of #54

XDZIBEC - ClaimOrder function incorrectly calculates owed amount

XDZIBEC

medium

ClaimOrder function incorrectly calculates owed amount

Summary

the claimOrder function where the owed is calculates, could result a potential for integer overflow or underflow in the claimOrder function

Vulnerability Detail

the claimOrder function where the owed is calculates, the calculation of owed is incorrect because it does not take into account the fact that the depositAmount parameter may be greater than the total amount of liquidity that was deposited for the order

// Calculate owed amount.
        uint256 totalTokenDeposited;
        uint256 totalTokenOut;
        ERC20 tokenOut;

        // again, remembering that direction == true means that the input token is token0.
        if (userClaim.direction) {
            totalTokenDeposited = userClaim.token0Amount;
            totalTokenOut = userClaim.token1Amount;
            tokenOut = poolToData[userClaim.pool].token1;
        } else {
            totalTokenDeposited = userClaim.token1Amount;
            totalTokenOut = userClaim.token0Amount;
            tokenOut = poolToData[userClaim.pool].token0;
        }

        uint256 owed = (totalTokenOut * depositAmount) / totalTokenDeposited;

ensure that the division operation does not result in any unintended truncation or rounding errors. Depending on the token amounts involved, precision loss could occur.

Impact

-could result a potential for integer overflow or underflow in the claimOrder function

Code Snippet

Tool used

Manual Review

Recommendation

  • use a precise arithmetic library or consider using fixed-point arithmetic to perform the calculation with the desired precision

Duplicate of #55

xiaoming90 - User's swapped LINK tokens might be lost

xiaoming90

high

User's swapped LINK tokens might be lost

Summary

Swapped LINK tokens stored in the LimitOrderRegistry contract that belong to the users and owner might be utilized or spent by Chainlink Automation, resulting in a loss of assets.

Vulnerability Detail

In Unswap V3, there are several pools where one of the token pair consist of the Chainlink Link token. The following is a non-exhaustive list of pools that consists of LINK tokens with significant TVL (Data extracted from Uniswap Info Page):

Note: I have verified that the LINK tokens in all the above-mentioned pools are the correct ERC677 LINK token that Chainlink could utilize/spent in Ethereum.

The owner can support any UniswapV3 pool in the LimitOrderRegistry contract.

Assume that the LINK/ETH pool is supported. In this case, users can create a limit order that deposits ETH into the contract and purchase LINK when it reaches a specific price point (Order is ITM). When the order is ITM, it will be fulfilled, and LINK tokens will be sent and stored in the LimitOrderRegistry contract. Users must then call the claimOrder function to claim their swapped LINK tokens.

On the other hand, LINK tokens are also being deposited into the LimitOrderRegistry contract by the owner for the purpose of Chainlink Automation. Whenever the performUpkeep function is triggered by Chainlink Node, Chainklink will reduce the LINK balance in the LimitOrderRegistry contract as a fee.

The issue is that the user's swapped LINK tokens, including the owner's swap fee collected denominated in LINK, might be spent away by Chainlink Automation.

Assume Bob's order is fulfilled, and the pool sends 50 LINK tokens to the contract. If Bob claims the order immediately after his order is fulfilled, he should receive 50 LINK tokens back. However, Bob only claimed the order 1 month later. When he claimed the order 1 month later, only 10 LINK tokens were left in the LimitOrderRegistry contract because Chainlink Automation had reduced the LINK balance of the contracts as an upkeep/maintenance fee. In addition, the owner's bot is configured only to replenish the LINK tokens if it falls below 10 LINK.

As a result, Bob's 40 LINK tokens are lost and have been unauthorized spent.

If even Bob decides to retrieve his remaining 10 LINK tokens by calling the claimOrder function, it is impossible because when the code at Line 724 below calls the safeTransfer function, it will attempt to transfer 50 LINK tokens that Bob is entitled to, which will revert due to insufficient assets.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L724

File: LimitOrderRegistry.sol
696:     function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
721:         uint256 owed = (totalTokenOut * depositAmount) / totalTokenDeposited;
722: 
723:         // Transfer tokens owed to user.
724:         tokenOut.safeTransfer(user, owed);

When the owner attempts to collect their earned LINK swap, it might revert due to insufficient assets when Line 499 below is executed.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L490

File: LimitOrderRegistry.sol
490:     function withdrawSwapFees(address tokenFeeIsIn) external onlyOwner {
491:         uint256 fee = tokenToSwapFees[tokenFeeIsIn];
492: 
493:         // Make sure there are actually fees to withdraw;
494:         if (fee == 0) revert LimitOrderRegistry__ZeroFeesToWithdraw(tokenFeeIsIn);
495: 
496:         // set fees to 0
497:         tokenToSwapFees[tokenFeeIsIn] = 0;
498:         // transfer fees to the user
499:         ERC20(tokenFeeIsIn).safeTransfer(owner, fee);
500:     }

Impact

Loss of assets (LINK) for the users and owners.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L724

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L490

Tool used

Manual Review

Recommendation

Consider implementing one of the following solutions to mitigate the issue:

Solution 1 - Disallow pool that consists of LINK token

Explicitly disallow owner from adding Uniswap V3 pool that consists of LINK token into the LimitOrderRegistry contract

Solution 2 - Store the user's LINK token and Chainlink Automation's LINK token in separate contracts

Consider storing the user's LINK token and Chainlink Automation's LINK token in separate contracts. This will prevent any overlap and ensure that the LINK token cannot be unauthorized access by another party.

mahyar - LimitOrderRegistry -> owenr can steal funds from user

mahyar

high

LimitOrderRegistry -> owenr can steal funds from user

Summary

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L505

In withdrawNative() function owner can withdraw all of the WETH token avaialable in contract

Vulnerability Detail

If users wants to create new order with newOrder() function based of any pool which contains WETH token and owner calls withdrawNative() function, owner drains all of the WETH available in contract this means users who created the order based of WETH as token out they cannot claim their order since there is nothing in the contract.

Impact

owner calls withdrawNative() function and drain all of the WETH and user can't withdraw thier token out

Code Snippet

    function withdrawNative() external onlyOwner {
        uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
        uint256 nativeBalance = address(this).balance;
        // Make sure there is something to withdraw.
        if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

        // transfer wrappedNativeBalance if it exists
        if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
        // transfer nativeBalance if it exists
        if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
    }

Tool used

Manual Review

Recommendation

Owners should store their fee in a storage variable and withdraw the token based of the allowed amount in the variable not withdrawing all of the tokens

Duplicate of #48

mstpr-brainbot - Protocol doesn't collect fees as intented

mstpr-brainbot

high

Protocol doesn't collect fees as intented

Summary

Protocol doesn't receive as they intent to when orders are fulfilled in performUpkeep.

Vulnerability Detail

When a users orders are in position, performUpkeep can be called to fulfill the ITM orders. When orders are fully fulfilled and the users are only receiving the capital that they provided. Example: If Alice deposited 1000USDC at some tick in OTM when the order fulfills Alice receives only the ETH that she bought. The fees will be claimable by the protocol owner.

Now, let's look at the following code snippet in the _takeFromPosition function:

function _takeFromPosition(
        uint256 target,
        UniswapV3Pool pool,
        uint256 liquidityPercent,
        uint256 deadline
    ) internal returns (uint128, uint128) {
        (, , , , , , , uint128 liquidity, , , , ) = POSITION_MANAGER.positions(target);
        liquidity = uint128(uint256(liquidity * liquidityPercent) / 1e18);

        // Create decrease liquidity params.
        // NB: because the amount0Min and amount1Min are 0 here, it is technically possible to front run this
        // that is probably okay, but it should be noted
        NonFungiblePositionManager.DecreaseLiquidityParams memory params = NonFungiblePositionManager
            .DecreaseLiquidityParams({
                tokenId: target,
                liquidity: liquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline: deadline
            });

        // Decrease liquidity in pool.
        uint128 amount0;
        uint128 amount1;
        {
            // @audit a0 and a1 are principal + fees !
            (uint256 a0, uint256 a1) = POSITION_MANAGER.decreaseLiquidity(params);
            // downcast to uint128 since those are the units we use
            amount0 = uint128(a0);
            amount1 = uint128(a1);
        }

        // If completely closing position, then collect fees as well.
        NonFungiblePositionManager.CollectParams memory collectParams;
        {
            uint128 amount0Max;
            uint128 amount1Max;
            if (liquidityPercent == 1e18) {
                amount0Max = type(uint128).max;
                amount1Max = type(uint128).max;
            } else {
                // Otherwise only collect principal.
                amount0Max = amount0;
                amount1Max = amount1;
            }
            // Create fee collection params.
            collectParams = NonFungiblePositionManager.CollectParams({
                tokenId: target,
                recipient: address(this),
                amount0Max: amount0Max,
                amount1Max: amount1Max
            });
        }

        // Save token balances.
        ERC20 token0 = poolToData[pool].token0;
        ERC20 token1 = poolToData[pool].token1;
        // @audit this balances are the idle balances
        uint256 token0Balance = token0.balanceOf(address(this));
        uint256 token1Balance = token1.balanceOf(address(this));

        // Collect fees.
        // @audit both collect fees + the principal
        POSITION_MANAGER.collect(collectParams);

        // Save fees earned, take the total token amount out - the amount removed from liquidity to get the fees earned.
        uint128 token0Fees = uint128(token0.balanceOf(address(this)) - token0Balance) - amount0;
        uint128 token1Fees = uint128(token1.balanceOf(address(this)) - token1Balance) - amount1;
        // Save any swap fees.
        if (token0Fees > 0) tokenToSwapFees[address(token0)] += token0Fees;
        if (token1Fees > 0) tokenToSwapFees[address(token1)] += token1Fees;

        return (amount0, amount1);
    }

The assumption here in this code is that when decreaseLiquidity called, the return variables are belong the initial capital without the fees. However, when entire liquidity is removed from v3 position, the removed liquidity will also include the fees aswell.

Example to demonstrate:

If a position has 1000USDC and 1000DAI in beginning and earns 10 DAI + 10 USDC as fee, when the full liquidity removed the return values will be (1010USDC,1010DAI). Fees included.

Protocol never receives the fees they intent to.

Impact

Since this is something that protocol team didn't know about, I'll label this as high because it is breaking the design.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1340-L1410

Tool used

Manual Review

Recommendation

MohammedRizwan - Unhandled chainlink revert would lock price oracle access

MohammedRizwan

medium

Unhandled chainlink revert would lock price oracle access

Summary

Chainlink's latestRoundData() is used which could potentially revert and make it impossible to query any prices. This could lead to permanent denial of service.

Vulnerability Detail

In LimitOrderRegistry.sol, getGasPrice()

File: src/LimitOrderRegistry.sol

1447    function getGasPrice() public view returns (uint256) {
1448        // If gas feed is set use it.
1449        if (fastGasFeed != address(0)) {
1450            (, int256 _answer, , uint256 _timestamp, ) = IChainlinkAggregator(fastGasFeed).latestRoundData();


              // Some code

The above functions makes use of Chainlink's latestRoundData() to get the latest price. However, there is no fallback logic to be executed when the access to the Chainlink data feed is denied by Chainlink's multisigs. Chainlink's multisigs can immediately block access to price feeds at will. Therefore, to prevent denial of service scenarios, it is recommended to query Chainlink price feeds using a defensive approach with Solidity’s try/catch structure. In this way, if the call to the price feed fails, the caller contract is still in control and can handle any errors safely and explicitly.

Impact

Call to latestRoundData could potentially revert and make it impossible to query any prices. This could lead to permanent denial of service.

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1450

Reference

Openzeppelin reference:
Refer to https://blog.openzeppelin.com/secure-smart-contract-guidelines-the-dangers-of-price-oracles/ for more information regarding potential risks to account for when relying on external price feed providers.

Tool used

Manual Review

Recommendation

Surround the call to latestRoundData() with try/catch instead of calling it directly. In a scenario where the call reverts, the catch block can be used to call a fallback oracle or handle the error in any other suitable way.

mstpr-brainbot - Malicious user can take advantage of unset minimumAssets values

mstpr-brainbot

medium

Malicious user can take advantage of unset minimumAssets values

Summary

Users can take advantage of the not setted minimumAssets by DOS'ing the queue or lowering the fee after a successful fulfilled order.

Vulnerability Detail

When a new pool added by the owner, the minimumAssets is not enforced to set. Minimum amount originally always blocks 0 amount deposits. However, if it's not set a user can take advantage of this by creating minimal orders. This would not only DOS the queue but the fee that the user pays will also be lower. A user can create multiple accounts and deposit the bear minimum which is usually 10001 due to the minAmountOut calculation in the _addPosition since 0 amount liquidity adding is forbidden in univ3.

This is how the fee is calculated for users when they claim their position:
newClaim.feePerUser = uint128(estimatedFee / totalUsers);

if user can increase the totalUsers to a big number, user can lower down the fee.

Scenario: Assume the minimumAmounts are not set for the token LUSD. Bob created an order and he is the only one in that order.
Bob assumes the estimatedFee which is uint256(upkeepGasLimit * getGasPrice()) will be 0.1 ETH.
Bob adds 10001 LUSD with 10 different accounts to the same tick position. 10001 LUSD is a very very low number since LUSD has 18 decimals so Bob can do this almost free. Now, when Bobs' order fulfills via a successful performUpkeep, Bob will pay

0.1 / 10 = 0.01 ETH. Which means Bob paid 10x lower fee than he should have.

Impact

Although this can easily prevented by the owner by setting the minimumAssets in the same tx with registering the token, I classified this as a medium finding since the owner is RESTRICTED. I think this should be enforced when a new pool is registered.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1215-L1248

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1256-L1261

Tool used

Manual Review

Recommendation

When registering the pool, also set the minimumAssets amounts for both of the tokens.

Duplicate of #49

xiaoming90 - Lack of segregation between users' assets and collected fees resulting in loss of funds for the users

xiaoming90

high

Lack of segregation between users' assets and collected fees resulting in loss of funds for the users

Summary

The users' assets are wrongly sent to the owner due to a lack of segregation between users' assets and collected fees, which might result in an irreversible loss of assets for the victims.

Vulnerability Detail

GLX uses the Chainlink Automation to execute the LimitOrderRegistry.performUpkeep function when there are orders that need to be fulfilled. The LimitOrderRegistry contract must be funded with LINK tokens to keep the operation running.

To ensure the LINK tokens are continuously replenished and funded, users must pay a fee denominated in Native ETH or ERC20 WETH tokens on orders claiming as shown below. The collected ETH fee will be stored within the LimitOrderRegistry contract.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L696

File: LimitOrderRegistry.sol
696:     function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
723:         // Transfer tokens owed to user.
724:         tokenOut.safeTransfer(user, owed);
725: 
726:         // Transfer fee in.
727:         address sender = _msgSender();
728:         if (msg.value >= userClaim.feePerUser) {
729:             // refund if necessary.
730:             uint256 refund = msg.value - userClaim.feePerUser;
731:             if (refund > 0) sender.safeTransferETH(refund);
732:         } else {
733:             WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
734:             // If value is non zero send it back to caller.
735:             if (msg.value > 0) sender.safeTransferETH(msg.value);
736:         }
..SNIP..

To retrieve the ETH fee collected, the owner will call the LimitOrderRegistry.withdrawNative function that will send all the Native ETH and ERC20 WETH tokens within the LimitOrderRegistry contract to the owner's address. After executing this function, the Native ETH and ERC20 WETH tokens on this contract will be zero and wiped out.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505

File: LimitOrderRegistry.sol
505:     function withdrawNative() external onlyOwner {
506:         uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
507:         uint256 nativeBalance = address(this).balance;
508:         // Make sure there is something to withdraw.
509:         if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();
510: 
511:         // transfer wrappedNativeBalance if it exists
512:         if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
513:         // transfer nativeBalance if it exists
514:         if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
515:     }

Most owners will automate replenishing the LimitOrderRegistry contract with LINK tokens to ensure its balance does not fall below zero and for ease of maintenance. For instance, a certain percentage of the collected ETH fee (e.g., 50%) will be swapped immediately to LINK tokens on a DEX upon collection and transferred the swapped LINK tokens back to the LimitOrderRegistry contract. The remaining will be spent to cover operation and maintenance costs.

However, the issue is that there are many Uniswap V3 pools where their token pair consists of ETH/WETH. In fact, most large pools in Uniswap V3 will consist of ETH/WETH. For instance, the following Uniswap pools consist of ETH/WETH as one of the pool tokens:

Assume that the owner has configured and setup the LimitOrderRegistry contract to work with the Uniswap DAI/ETH pool, and the current price of the DAI/ETH pool is 1,500 DAI/ETH.

Bob submit a new Buy Limit Order swapping DAI to ETH at the price of 1,000 DAI/ETH. Bob would deposit 1,000,000 DAI to the LimitOrderRegistry contract.

When Bob's Buy Limit Order is ITM and fulfilled, 1000 ETH/WETH will be sent to and stored within the LimitOrderRegistry contract.

The next step that Bob must do to claim the swapped 1000 ETH/WETH is to call the LimitOrderRegistry.claimOrder function, which will collect the fee and transfer the swapped 1000 ETH/WETH to Bob.

Unfortunately, before Bob could claim his swapped ETH/WETH, the LimitOrderRegistry.withdrawNative function is triggered by the owner or the owner's bots. As noted earlier, when the LimitOrderRegistry.withdrawNative function is triggered, all the Native ETH and ERC20 WETH tokens on this contract will be transferred to the owner's address. As a result, Bob's 1000 swapped ETH/WETH stored within the LimitOrderRegistry contract are sent to the owner's address, and the balance of ETH/WETH in the LimitOrderRegistry contract is zero.

When Bob calls the LimitOrderRegistry.claimOrder function, the transaction will revert because insufficient ETH/WETH is left in the LimitOrderRegistry contract.

Unfortunately for Bob, there is no way to recover back his ETH/WETH that is sent to the owner's address. Following outline some of the possible scenarios where this could happen:

  • The owners set up their infrastructure to automatically swap a portion or all the ETH/WETH received to LINK tokens and transfer them to the LimitOrderRegistry contract, and there is no way to retrieve the deposited LINK tokens from the LimitOrderRegistry contract even if the owner wishes to do so as there is no function within the contract to allow this action.
  • The owners set up their infrastructure to automatically swap a small portion of ETH/WETH received to LINK tokens and send the rest of the ETH/WETH to 100 investors/DAO members' addresses. So, it is no guarantee that the investors/DAO members will return the ETH/WETH to Bob.

Impact

Loss of assets for the users

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L505

Tool used

Manual Review

Recommendation

Consider implementing one of the following solutions to mitigate the issue:

Solution 1 - Only accept Native ETH as fee

Uniswap V3 pool stored ETH as Wrapped ETH (WETH) ERC20 token internally. When the collect function is called against the pool, WETH ERC20 tokens are returned to the caller. Thus, the most straightforward way to mitigate this issue is to update the contract to collect the fee in Native ETH only.

In this case, there will be a clear segregation between users' assets (WETH) and owner's fee (Native ETH)

function withdrawNative() external onlyOwner {
-    uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
    uint256 nativeBalance = address(this).balance;
    // Make sure there is something to withdraw.
-    if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();
+    if (nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

-    // transfer wrappedNativeBalance if it exists
-    if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
    // transfer nativeBalance if it exists
    if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
}
function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
    // Transfer tokens owed to user.
    tokenOut.safeTransfer(user, owed);

    // Transfer fee in.
    address sender = _msgSender();
    if (msg.value >= userClaim.feePerUser) {
        // refund if necessary.
        uint256 refund = msg.value - userClaim.feePerUser;
        if (refund > 0) sender.safeTransferETH(refund);    
    } else {
-       WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
-       // If value is non zero send it back to caller.
-       if (msg.value > 0) sender.safeTransferETH(msg.value);
+		revert LimitOrderRegistry__InsufficientFee;
    }
..SNIP..

Solution 2 - Define state variables to keep track of the collected fee

Consider defining state variables to keep track of the collected fee so that the fee will not mix up with users' assets.

function claimOrder(uint128 batchId, address user) external payable returns (ERC20, uint256) {
..SNIP..
    // Transfer fee in.
    address sender = _msgSender();
    if (msg.value >= userClaim.feePerUser) {
+    	collectedNativeETHFee += userClaim.feePerUser
        // refund if necessary.
        uint256 refund = msg.value - userClaim.feePerUser;
        if (refund > 0) sender.safeTransferETH(refund);
    } else {
+    	collectedWETHFee += userClaim.feePerUser
        WRAPPED_NATIVE.safeTransferFrom(sender, address(this), userClaim.feePerUser);
        // If value is non zero send it back to caller.
        if (msg.value > 0) sender.safeTransferETH(msg.value);
    }
..SNIP..
function withdrawNative() external onlyOwner {
-   uint256 wrappedNativeBalance = WRAPPED_NATIVE.balanceOf(address(this));
-   uint256 nativeBalance = address(this).balance;
+	uint256 wrappedNativeBalance = collectedWETHFee;
+	uint256 nativeBalance = collectedNativeETHFee;
+	collectedWETHFee = 0; // clear the fee
+	collectedNativeETHFee = 0; // clear the fee
    // Make sure there is something to withdraw.
    if (wrappedNativeBalance == 0 && nativeBalance == 0) revert LimitOrderRegistry__ZeroNativeBalance();

    // transfer wrappedNativeBalance if it exists
    if (wrappedNativeBalance > 0) WRAPPED_NATIVE.safeTransfer(owner, wrappedNativeBalance);
    // transfer nativeBalance if it exists
    if (nativeBalance > 0) owner.safeTransferETH(nativeBalance);
}

Vagner - `_takeFromPosition` doesn't use any slippage protection for the `decreaseLiquidity` which could cause the users getting less funds then intended

Vagner

high

_takeFromPosition doesn't use any slippage protection for the decreaseLiquidity which could cause the users getting less funds then intended

Summary

The protocol doesn't use any slippage protection in _takeFromPosition which could lead to loss of funds/user getting way less funds than expected.

Vulnerability Detail

The protocol states in the comments that because of the fact that amount0Min and amount1Min are 0 here the function can be possible front-run but that is probably ok https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1349-L1351
but I want to argue the fact that this is not ok giving two different exemples/cases.

  • firstly the slippage is an important factor for users that wants to mint/burn shares so they can protect themselves against sandwich attacks and high volatility price movements, consider the fact that the protocol intends to work with any ERC20 token and pool that is supported by UniswapV3 and there are pools that have lower liquidity and can be easily manipulated
  • secondly the protocol is intended to be deployed and worked also on L2 chains like Arbitrum or Optimism, chains that unlike mainnet ethereum, can be in a network downtime since stuff like The Sequencer on Arbitrum can be down. In that case, it is stated in the Uniswap support page that : No swaps will be executed , No fees will be earned and No transactions will be possible. In particular, LPs will be unable to add liquidity, remove liquidity, claim fees, adjust price range, etc. as can be seen here https://support.uniswap.org/hc/en-us/articles/7425219666061-How-to-provide-liquidity-on-Arbitrum. It is also stated that when the network goes live again, LPs may experience sudden price movements if L1 market prices shifted while Arbitrum was down. which means that if there is no slippage protection users may get way less assets than intended which would occur losses of funds for the user without them knowing

Impact

This is a high impact since the user can lose funds and can't really protect themselves against it since they can set any slippage.

Code Snippet

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L1352-L1359

Tool used

Manual Review

Recommendation

Let the user specify the amount0Min/amount1Min so they can protect themselves instead of hardcoding the slippage or setting it to 0.

MohammedRizwan - Missing check for active Arbitrum Sequencer

MohammedRizwan

medium

Missing check for active Arbitrum Sequencer

Summary

Chainlink recommends that all Optimistic L2 oracles consult the Sequencer Uptime Feed to ensure that the sequencer is live before trusting the data returned by the oracle. This check is is missing in LimitOrderRegistry.sol contract.

Vulnerability Detail

Since the smart contracts will also be deployed on Arbitrum. If the Arbitrum Sequencer goes down, oracle data will not be kept up to date, and thus could become stale. However, users are able to continue to interact with the protocol directly through the L1 optimistic rollup contract. You can review Chainlink docs on L2 Sequencer Uptime Feeds for more details on this.

As a result, users may be able to use the protocol while oracle feeds are stale. This could cause many problems, but as a simple example:

  • A user has an account with 100 tokens, valued at 1 ETH each, and no borrows
  • The Arbitrum sequencer goes down temporarily
  • While it’s down, the price of the token falls to 0.5 ETH each
  • The current value of the user’s account is 50 ETH, so they should be able to borrow a maximum of 200 ETH to keep account healthy ((200 + 50) / 200 = 1.2)

Because of the stale price, the protocol lets them borrow 400 ETH ((400 + 100) / 400 = 1.2) # Impact If the Arbitrum sequencer goes down, the protocol will allow users to continue to operate at the previous (stale) rates.

Impact

If the Arbitrum sequencer goes down, the protocol will allow users to continue to operate at the previous (stale) rates.

Code Snippet

https://github.com/crispymangoes/uniswap-v3-limit-orders/blob/753974a4ba5b07ec076c5e5bcbe7277e5921be4b/src/LimitOrderRegistry.sol#L1450

Tool used

Manual Review

Recommendation

Use sequencer oracle to determine whether the sequencer is offline or not, and don't allow orders to be executed while the sequencer is offline.

Check the Chainlink Documentation for a full example: https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Also check the queue system here: https://docs.chain.link/data-feeds/l2-sequencer-feeds/

Duplicate of #65

kutugu - feePerUser calculation has precision error resulting in low protocol income

kutugu

medium

feePerUser calculation has precision error resulting in low protocol income

Summary

When calculating the amount of feePerUser, you should round up instead of down, which will result in lower protocol income, and the larger the number of people, the larger the difference.

Vulnerability Detail

newClaim.feePerUser = uint128(estimatedFee / totalUsers);

There is a precision error in division here, and when rounding is taken into account, the protocol should take precedence over user experience.
For each person, the accuracy error is only 1 wei, but for the protocol, 1000 people is 1000 wei. Especially considering the maturity of the subsequent protocol and the increase of users and interactions, the greater the impact of the error on the income of the protocol.
In particular, this part of the cost may be for the maintenance of chainlink automation application, low income may lead to run out of funds, unable to maintain the normal operation of the protocol.

Impact

feePerUser calculation has precision error resulting in low protocol income

Code Snippet

Tool used

Manual Review

Recommendation

Round up here

Duplicate of #55

p0wd3r - In performUpkeep, currentTick should be reacquired after _fulfillOrder.

p0wd3r

high

In performUpkeep, currentTick should be reacquired after _fulfillOrder.

Summary

In performUpkeep, currentTick should be reacquired after _fulfillOrder.

Vulnerability Detail

In performUpkeep, currentTick is obtained outside the loop, meaning that multiple orderStatus checks are based on the same tick.

https://github.com/sherlock-audit/2023-06-gfx/blob/main/uniswap-v3-limit-orders/src/LimitOrderRegistry.sol#L927-L938

        (, int24 currentTick, , , , , ) = pool.slot0();
        bool orderFilled;

        // Fulfill orders.
        uint256 target = walkDirection ? data.centerHead : data.centerTail;
        for (uint256 i; i < maxFillsPerUpkeep; ++i) {
            if (target == 0) break;
            BatchOrder storage order = orderBook[target];
            OrderStatus status = _getOrderStatus(currentTick, order.tickLower, order.tickUpper, order.direction);
            if (status == OrderStatus.ITM) {
                _fulfillOrder(target, pool, order, estimatedFee, deadline);

In _fulfillOrder, the operations of removing liquidity and collecting fees are performed, which will affect the price of the current pool, that is, it will affect the value of slot0.

Therefore, currentTick may change after _fulfillOrder, thereby affecting orderStatus, but the same currentTick is always used in this function.

Impact

The order that does not meet the tick requirements was executed, which affected the core functionality of the protocol.

Code Snippet

Tool used

Manual Review

Recommendation

In performUpkeep, currentTick should be reacquired after _fulfillOrder.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.