Platform: Code4rena
Start Date: 03/07/2023
Pot Size: $40,000 USDC
Total HM: 14
Participants: 74
Period: 7 days
Judge: alcueca
Total Solo HM: 9
Id: 259
League: ETH
Rank: 12/74
Findings: 4
Award: $484.35
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: tonisives
Also found by: Inspecktor, MohammedRizwan, Qeew, peanuts, sces60107
162.9427 USDC - $162.94
DoS for the Aquifer.boreWell() function due to frontrunning.
From the video documentation, Anyone can call boreWell()
in Aquifer.sol after confirming an implementation contract. The address of the new Well depends solely upon the _salt parameter provided by the user. Once the user's create transaction is broadcasted, the _salt parameter can be viewed by anyone watching the public mempool.
As a result, if a user intends to permissionlessly create a well, his create transaction can be forcefully reverted by an attacker by deploying a well for himself using the user's salt. Here is how this can happen:
} else { if (salt != bytes32(0)) { @> well = implementation.cloneDeterministic(salt); } else { well = implementation.clone(); }
VSCode
Consider making the upcoming well address user specific by combining the salt value with user's address.
} else { if (salt != bytes32(0)) { + well = implementation.cloneDeterministic(msg.sender, salt); } else { well = implementation.clone(); }
Other
#0 - c4-pre-sort
2023-07-11T14:35:42Z
141345 marked the issue as low quality report
#1 - c4-pre-sort
2023-07-11T14:39:44Z
141345 marked the issue as primary issue
#2 - c4-pre-sort
2023-07-12T16:03:16Z
141345 marked the issue as duplicate of #221
#3 - c4-pre-sort
2023-07-12T16:12:31Z
141345 marked the issue as duplicate of #181
#4 - c4-judge
2023-08-04T06:01:35Z
alcueca marked the issue as satisfactory
🌟 Selected for report: 0xprinc
Also found by: 0x11singh99, 0xAnah, 0xWaitress, 0xkazim, 2997ms, 33audits, 404Notfound, 8olidity, CRIMSON-RAT-REACH, CyberPunks, DanielWang888, Deekshith99, Eeyore, Eurovickk, Inspecktor, JGcarv, John, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Qeew, QiuhaoLi, Rolezn, TheSavageTeddy, Topmark, Trust, Udsen, a3yip6, alexzoid, bigtone, codegpt, erebus, fatherOfBlocks, ginlee, glcanvas, hunter_w3b, josephdara, kaveyjoe, kutugu, mahdirostami, max10afternoon, oakcobalt, peanuts, pfapostol, ptsanev, qpzm, radev_sw, ravikiranweb3, sces60107, seth_lawson, te_aut, twcctop, zhaojie, ziyou-
17.5208 USDC - $17.52
Slippage may not be accounted for accurately.
Well.sol accepts fee-on-transfer tokens, as seen in the NATSPEC and the functions.
function swapFromFeeOnTransfer(
The feeOnTransfer token can be either the incoming token or the outgoing token or both. Let's focus on the outgoing token. When a user wants to swap tokenX to tokenY (take tokenX as fee-on-transfer), he calls swapFromFeeOnTransfer()
which calls _swapFrom()
. _swapFrom()
does the necessary checks and updates before calculating the amountOut
. The function then checks that amountOut
must be greater than minAmountOut
before transferring the amountOut
value to the user.
amountOut = reserveJBefore - reserves[j]; if (amountOut < minAmountOut) { revert SlippageOut(amountOut, minAmountOut); } toToken.safeTransfer(recipient, amountOut); emit Swap(fromToken, toToken, amountIn, amountOut, recipient);
However, the fee for transferring the amountOut
to the user is not accounted in the minAmountOut
variable, which means that the fee for transferring is not included in the slippage.
For example, if tokenY has a 10% transfer fee.
VSCode
When interacting with fee on transfer tokens, the slippage check should be done after the transfer to account for the transfer fee.
amountOut = reserveJBefore - reserves[j]; - if (amountOut < minAmountOut) { - revert SlippageOut(amountOut, minAmountOut); - } - toToken.safeTransfer(recipient, amountOut); + uint256 balanceBefore = toToken.balanceOf(recipient); + token.safeTransfer(recipient, amountOut); + amountTransferred = toToken.balanceOf(recipient) - balanceBefore; + if (amountTransferred < minAmountOut) { + revert SlippageOut(amountOut, minAmountOut); + } emit Swap(fromToken, toToken, amountIn, amountOut, recipient);
Token-Transfer
#0 - c4-pre-sort
2023-07-11T14:36:39Z
141345 marked the issue as primary issue
#1 - c4-sponsor
2023-07-19T15:07:27Z
publiuss marked the issue as sponsor confirmed
#2 - c4-judge
2023-08-04T20:01:51Z
alcueca marked issue #110 as primary and marked this issue as a duplicate of 110
#3 - c4-judge
2023-08-05T21:22:41Z
alcueca marked the issue as satisfactory
#4 - c4-judge
2023-08-15T20:12:05Z
alcueca changed the severity to QA (Quality Assurance)
#5 - c4-judge
2023-08-15T20:12:34Z
alcueca marked the issue as grade-a
🌟 Selected for report: SM3_SS
Also found by: 0x11singh99, 0xAnah, 0xSmartContract, 0xn006e7, 0xprinc, DavidGiladi, ElCid, JCN, K42, MIQUINHO, Raihan, Rolezn, SAAJ, SY_S, Strausses, TheSavageTeddy, bigtone, erebus, hunter_w3b, josephdara, lsaudit, mahdirostami, oakcobalt, peanuts, pfapostol, seth_lawson
7.8853 USDC - $7.89
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L22 https://github.com/code-423n4/2023-07-basin/blob/9403cf973e95ef7219622dbbe2a08396af90b64c/src/Well.sol#L31-L34
Reentrancy Guard will not work as intended.
Well.sol uses ReentrancyGuardUpgradeable.sol from OpenZeppelin.
import {ReentrancyGuardUpgradeable} from "ozu/security/ReentrancyGuardUpgradeable.sol";
However, it is not initialized.
function init(string memory name, string memory symbol) public initializer { __ERC20Permit_init(name); __ERC20_init(name, symbol);
In OpenZeppelin's ReentrancyGuardUpgradeable, __ReentrancyGuard_init()
should be called:
function __ReentrancyGuard_init() internal onlyInitializing { __ReentrancyGuard_init_unchained(); } function __ReentrancyGuard_init_unchained() internal onlyInitializing { _status = _NOT_ENTERED; }
If __ReentrancyGuard_init()
is not called, the modifier nonReentrant will not work as intended.
Manual Review
Call __ReentrancyGuard_init()
in init() in Well.sol.
function init(string memory name, string memory symbol) public initializer { __ERC20Permit_init(name); __ERC20_init(name, symbol); + __ReentrancyGuard_init()
Upgradable
#0 - c4-pre-sort
2023-07-11T13:03:39Z
141345 marked the issue as low quality report
#1 - c4-pre-sort
2023-07-11T13:04:13Z
141345 marked the issue as primary issue
#2 - 141345
2023-07-12T16:26:09Z
auto finding
#3 - c4-judge
2023-08-04T19:56:02Z
alcueca marked issue #201 as primary and marked this issue as a duplicate of 201
#4 - c4-judge
2023-08-05T21:23:32Z
alcueca marked the issue as satisfactory
#5 - c4-judge
2023-08-15T19:59:28Z
alcueca changed the severity to G (Gas Optimization)
#6 - c4-judge
2023-08-19T18:41:22Z
alcueca marked the issue as grade-b
296.0006 USDC - $296.00
Spent most of the time auditing the Well's code and briefly looked through the Math in the Pump contract.
The addition of both 'swapExactInputSingle' (swapFrom) and 'swapExactOutputSingle' (swapTo) style when swapping the tokens is appreciated and well created, especially since the latter is not very common.
SwapTo()
calculates the input amount before transferring the tokens in. This is unlike how Uniswap does it, which is transferring the maximum amount of tokens in and then refunding the remaining tokens back. The protocol does it better and saves some gas in the process as well by doing just one transfer.
Shift is an interesting concept as it opts away from using the router and let the Wells interact with each other instead. Saves some gas by not transferring tokens around many times.
Deadline, slippage is set properly in every swap and increase/decrease liquidity.
Getter functions are well written and plenty; users have access to many external view functions to get a sense of what they are doing before calling the actual function.
swapFrom()
and swapFromFeeOnTransfer()
) should have a bool feeOnTransfer
flag the same as addLiquidity()
in order to save gas in case the user accidentally calls the wrong function. This saves the user some gas in case he wants to use fee-on-transfer token but calls swapFrom()
instead, and then has to wait until _setReserves()
is calculated before the function reverts, which costs quite a bit of gas for the computation. Should check whether the swap uses fee-on-transfer tokens from the very start as such:function swapFrom( IERC20 fromToken, IERC20 toToken, uint256 amountIn, uint256 minAmountOut, address recipient, uint256 deadline, + bool isFeeOnTransfer ) external nonReentrant expire(deadline) returns (uint256 amountOut) { + if(isFeeOnTransfer) revert(); fromToken.safeTransferFrom(msg.sender, address(this), amountIn);
The swapTo function can also include the feeOnTransfer parameter (must always be false) to save some gas in case the user unknowningly uses the swapTo()
function with a fee-on-transfer token.
Since the Well function is awaiting a proper implementation, protocol could give an example of how to edit the Well contract. For example, when updating pump, the protocol can give a possible commented code block if an external shutoff mechanism were to be added:
try IPump(_pump.target).update(reserves, _pump.data) {} catch { // ignore reversion. If an external shutoff mechanism is added to a Pump, it could be called here. + // Example of how a shutoff mechanism would look like in code }
Since the creation of the Well clone is permissionless, the implementation contract has to be airtight to make sure that the creators of the well cannot exploit the well through inflation attacks or any sort of attacks when minting LPs, swapping or decreasing liquidity. This poses a huge systemic risk because if the implementation fails, every clone that clones through the foiled implementation will also be at risk.
Also, even though the Wells are permissionless, if any Well user is malicious (if there is an opportunity to be malicious), then it will affect the reputation of the protocol since the whole system relies on the Well contracts.
15 hours
#0 - c4-pre-sort
2023-07-12T12:13:43Z
141345 marked the issue as high quality report
#1 - c4-sponsor
2023-08-03T20:32:11Z
publiuss marked the issue as sponsor acknowledged
#2 - c4-judge
2023-08-05T20:17:43Z
alcueca marked the issue as grade-a