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: 16/74
Findings: 2
Award: $180.46
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: tonisives
Also found by: Inspecktor, MohammedRizwan, Qeew, peanuts, sces60107
162.9427 USDC - $162.94
https://github.com/code-423n4/2023-07-basin/blob/main/src/Aquifer.sol#L34
A benign user can call Aquifer.boreWell
with salt
to deploy a new Well with create2
. However the implementation of Aquifer.boreWell
is vulnerable to front-running attacks. A malicious user can front-run the transaction with malicious initFunctionCall
.
Aquifer.boreWell
uses create2
to deploy a new Well if salt > 0
. And then it performs well.call(initFunctionCall)
if initFunctionCall.length > 0
.
https://github.com/code-423n4/2023-07-basin/blob/main/src/Aquifer.sol#L34
function boreWell( address implementation, bytes calldata immutableData, bytes calldata initFunctionCall, bytes32 salt ) external nonReentrant returns (address well) { if (immutableData.length > 0) { if (salt != bytes32(0)) { well = implementation.cloneDeterministic(immutableData, salt); } else { well = implementation.clone(immutableData); } } else { if (salt != bytes32(0)) { well = implementation.cloneDeterministic(salt); } else { well = implementation.clone(); } } if (initFunctionCall.length > 0) { (bool success, bytes memory returnData) = well.call(initFunctionCall); if (!success) { // Next 5 lines are based on https://ethereum.stackexchange.com/a/83577 if (returnData.length < 68) revert InitFailed(""); assembly { returnData := add(returnData, 0x04) } revert InitFailed(abi.decode(returnData, (string))); } } … }
Since the salt
is unrelated to the initFunctionCall
. A malicious user can front-run the transaction with malicious initFunctionCall
. Since the well implementation is user-defined, the impact of this attack could be significant.
Manual Review
There are two methods to mitigate this issue:
Add msg.sender
to the salt
function boreWell( address implementation, bytes calldata immutableData, bytes calldata initFunctionCall, bytes32 salt ) external nonReentrant returns (address well) { if (immutableData.length > 0) { if (salt != bytes32(0)) { - well = implementation.cloneDeterministic(immutableData, salt); + well = implementation.cloneDeterministic(immutableData, keccak256(abi.encodePacked(salt, msg.sender))); } else { well = implementation.clone(immutableData); }
Or add initFunctionCall
to the salt
function boreWell( address implementation, bytes calldata immutableData, bytes calldata initFunctionCall, bytes32 salt ) external nonReentrant returns (address well) { if (immutableData.length > 0) { if (salt != bytes32(0)) { - well = implementation.cloneDeterministic(immutableData, salt); + well = implementation.cloneDeterministic(immutableData, keccak256(abi.encodePacked(salt, initFunctionCall))); } else { well = implementation.clone(immutableData); }
Other
#0 - c4-pre-sort
2023-07-11T15:06:58Z
141345 marked the issue as duplicate of #217
#1 - c4-pre-sort
2023-07-12T16:03:14Z
141345 marked the issue as duplicate of #221
#2 - c4-pre-sort
2023-07-12T16:12:30Z
141345 marked the issue as duplicate of #181
#3 - c4-judge
2023-08-04T06:01:21Z
alcueca marked the issue as satisfactory
#4 - c4-judge
2023-08-05T21:38:56Z
alcueca changed the severity to 2 (Med Risk)
🌟 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
https://github.com/code-423n4/2023-07-basin/blob/main/src/pumps/MultiFlowPump.sol#L27 https://github.com/code-423n4/2023-07-basin/blob/main/src/pumps/MultiFlowPump.sol#L119 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L645
The comment in MultiFlowPump.sol
says:
https://github.com/code-423n4/2023-07-basin/blob/main/src/pumps/MultiFlowPump.sol#L27
* Note: If an `update` call is made with a reserve of 0, the Geometric mean oracles will be set to 0. * Each Well is responsible for ensuring that an `update` call cannot be made with a reserve of 0. */
However, the current implementation of Well
doesn’t ensure that an update
call cannot be made with a reserve of 0.
The update
call is made in Well._updatePumps
. It is obvious that it doesn’t ensure that the reserves are not zero.
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L645
function _updatePumps(uint256 _numberOfTokens) internal returns (uint256[] memory reserves) { reserves = _getReserves(_numberOfTokens); if (numberOfPumps() == 0) { return reserves; } // gas optimization: avoid looping if there is only one pump if (numberOfPumps() == 1) { Call memory _pump = firstPump(); // Don't revert if the update call fails. 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. } } else { Call[] memory _pumps = pumps(); for (uint256 i; i < _pumps.length; ++i) { // Don't revert if the update call fails. try IPump(_pumps[i].target).update(reserves, _pumps[i].data) {} catch { // ignore reversion. If an external shutoff mechanism is added to a Pump, it could be called here. } } } }
And MultiFlowPump
uses a minimum of 1 for reserve if a reserve is 0. It could lead to the wrong result of other view functions.
https://github.com/code-423n4/2023-07-basin/blob/main/src/pumps/MultiFlowPump.sol#L119
function update(uint256[] calldata reserves, bytes calldata) external { … for (uint256 i; i < numberOfReserves; ++i) { // Use a minimum of 1 for reserve. Geometric means will be set to 0 if a reserve is 0. pumpState.lastReserves[i] = _capReserve( pumpState.lastReserves[i], (reserves[i] > 0 ? reserves[i] : 1).fromUIntToLog2(), blocksPassed ); … }
Manual Review
Well._updatePumps
should ensure that an update
call cannot be made with a reserve of 0
Other
#0 - c4-pre-sort
2023-07-11T15:16:32Z
141345 marked the issue as low quality report
#1 - 141345
2023-07-13T06:12:59Z
lack details on the impact and potential loss
maybe QA is more appropriate
#2 - c4-pre-sort
2023-07-13T06:52:50Z
141345 marked the issue as primary issue
#3 - alcueca
2023-08-04T12:35:45Z
Without a description of an impact, this should be classified as QA
#4 - c4-judge
2023-08-04T12:35:55Z
alcueca changed the severity to QA (Quality Assurance)
#5 - c4-judge
2023-08-04T21:40:41Z
alcueca marked the issue as grade-a