Basin - sces60107's results

A composable EVM-native decentralized exchange protocol.

General Information

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

Basin

Findings Distribution

Researcher Performance

Rank: 16/74

Findings: 2

Award: $180.46

QA:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: tonisives

Also found by: Inspecktor, MohammedRizwan, Qeew, peanuts, sces60107

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-181

Awards

162.9427 USDC - $162.94

External Links

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Aquifer.sol#L34

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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);
            }

Assessed type

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)

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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
            );
            …
    }

Tools Used

Manual Review

Well._updatePumps should ensure that an update call cannot be made with a reserve of 0

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter