GithubHelp home page GithubHelp logo

2023-07-basin-findings's Introduction

Basin Audit

Unless otherwise discussed, this repo will be made public after audit completion, sponsor review, judging, and issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Audit findings are submitted to this repo

Sponsors have three critical tasks in the audit process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. You may also add questions for the judge in the comments.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue.

Note that when the repo is public, after all issues are mitigated, wardens will read these comments; they may also be included in your C4 audit report.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: wardens are required to submit these as bulk listings of issues and recommendations. They may only submit a single, compiled report in each category:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, sponsors are not required to weigh in on severity or risk level. We ask that sponsors:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the OK.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

If you are planning a Code4rena mitigation review:

  1. In your own Github repo, create a branch based off of the commit you used for your Code4rena audit, then
  2. Create a separate Pull Request for each High or Medium risk C4 audit finding (e.g. one PR for finding H-01, another for H-02, etc.)
  3. Link the PR to the issue that it resolves within your contest findings repo.

Most C4 mitigation reviews focus exclusively on reviewing mitigations of High and Medium risk findings. Therefore, QA and Gas mitigations should be done in a separate branch. If you want your mitigation review to include QA or Gas-related PRs, please reach out to C4 staff and let’s chat!

If several findings are inextricably related (e.g. two potential exploits of the same underlying issue, etc.), you may create a single PR for the related findings.

If you aren’t planning a mitigation review

  1. Within a repo in your own GitHub organization, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-07-basin-findings's People

Contributors

code423n4 avatar kartoonjoy avatar c4-judge avatar

Stargazers

guy avatar

Watchers

Ashok avatar  avatar

2023-07-basin-findings's Issues

Not checking whether iToken and jToken are the same token leads to unexpected situations.

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L730-L750

Vulnerability details

Impact

Not checking whether iToken and jToken are the same token leads to unexpected situations.

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L730-L750
function _getIJ(
IERC20[] memory _tokens,
IERC20 iToken,
IERC20 jToken
) internal pure returns (uint256 i, uint256 j) {
bool foundI = false;
bool foundJ = false;

    for (uint256 k; k < _tokens.length; ++k) {
        if (iToken == _tokens[k]) {
            i = k;
            foundI = true;
        } else if (jToken == _tokens[k]) {
            j = k;
            foundJ = true;
        }
    }

    if (!foundI) revert InvalidTokens();
    if (!foundJ) revert InvalidTokens();
}

Tools Used

vscode

Recommended Mitigation Steps

Add corresponding checks

Assessed type

Other

liquidity drain shifting wrong token

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L367

Vulnerability details

Impact

The Well.sol contract does not correctly validate the shifted token in the shift() function. This lack of validation allows any user to debalance the liquidity of one token of the AMM (via transfer) and request for a shift from the other token, draining one of the tokens.

The impact of this vulnerability depends on the prices of the tokens the AMM handles(i.e. WETH-USDC) as the attacker can debalance it with an amount of USDC and obtain pretty much the same amount of WETH, which has more value than the USDC used.

Proof of Concept

The foundry test provided simulates how an user can transfer an amount of token0 and retrieve a lower amount of token1 (which shouldnt be possible). these steps can be repeated as long as token1 balance > 0 in the AMM.

function test_shift_differentTokens() public prank(user) {

        address _user = users.getNextUserAddress();

        Balances memory userBalance = getBalances(_user, well);
        Balances memory wellBalance = getBalances(address(well), well);

        //initial balances
        console.log(userBalance.tokens[0]);  
        console.log(userBalance.tokens[1]);

        console.log(wellBalance.tokens[0]);
        console.log(wellBalance.tokens[1]);
        console.log("-----");

        //debalancing token0
        tokens[0].transfer(address(well), 100e18);

        // new balances of well
        wellBalance = getBalances(address(well), well);

        console.log(wellBalance.tokens[0]);
        console.log(wellBalance.tokens[1]);
        console.log("-----");
        
        //shifting token1
        well.shift(tokens[1], 0, _user);

        // balances after shifting
        userBalance = getBalances(_user, well);
        wellBalance = getBalances(address(well), well);

        console.log(userBalance.tokens[0]);
        console.log(userBalance.tokens[1]);

        console.log(wellBalance.tokens[0]);
        console.log(wellBalance.tokens[1]);
    }

The output of this test shows how many token1 the attacker has obtained.

[PASS] test_shift_differentTokens() (gas: 149126)
Logs:
  0
  0
  2000000000000000000000
  2000000000000000000000
  -----
  2100000000000000000000
  2000000000000000000000
  -----
  0
  95238095238095238095
  2100000000000000000000
  1904761904761904761905

Tools Used

manual testing

Recommended Mitigation Steps

It is recommended to validate and match the token used for shift, the balance of that token and the reserves of the token in order to obtain the correct amountOut value.

Assessed type

Invalid Validation

check missed in the `_swapFrom` to prevent sending tokens to the reserves directly and cause bad cases happen

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L186-L196
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L215-L239

Vulnerability details

Impact

the function swapFrom in Well.sol contract make a swap between from and to token and sending the amountOut to the recipient address. this work fine and there is no problem with that but if the user set the recipient to the Well address or let's say the receive address then the user can set token to the reserve address and manipulate the reserves of the Well contract without doing necessary calculation for adding liquidity (that is implemented in addLiquidty functions) in this case the user can manipulate the reserve address as well.

note that the Well contract is the liquidity pool that holds the reserves of both token(from[i] and to[j]) and that's mean this contract is the reserve contract for both tokens(if not the attacker still can send token to the reserve address and cause manipulate in balances)

Proof of Concept

in the function swapFrom we call the _swapFrom which make a swap and transferring the amountOut to the recipient address that we set it in the params:

function swapFrom(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant expire(deadline) returns (uint256 amountOut) {
        //audit-info send the amountIn to this contract and amountOut to our recipient address
        fromToken.safeTransferFrom(msg.sender, address(this), amountIn);
        amountOut = _swapFrom(fromToken, toToken, amountIn, minAmountOut, recipient);
    }

the _swapFrom function:

 function _swapFrom(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient
    ) internal returns (uint256 amountOut) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _updatePumps(_tokens.length);
        (uint256 i, uint256 j) = _getIJ(_tokens, fromToken, toToken);

        reserves[i] += amountIn;
        uint256 reserveJBefore = reserves[j];
        reserves[j] = _calcReserve(wellFunction(), reserves, j, totalSupply());

        // Note: The rounding approach of the Well function determines whether
        // slippage from imprecision goes to the Well or to the User.
        amountOut = reserveJBefore - reserves[j];
        if (amountOut < minAmountOut) {
            revert SlippageOut(amountOut, minAmountOut);
        }
       //@audit what if we set the reserve/tokens address here !?
        toToken.safeTransfer(recipient, amountOut);
        emit Swap(fromToken, toToken, amountIn, amountOut, recipient);
        _setReserves(_tokens, reserves);
    }

the function _swapFrom will send the amountOut to the recipient address which can be the fromToken or toToken and sending token to the reserve directly without calling addReserve and cause many bad cases to happen(manipulate reserve balances for example)

Tools Used

manual review / uniswap v2 codebase

Recommended Mitigation Steps

recommend to add check to prevent the recipient to be the reserves address, check like these can be set in _swapFrom:

require(recipient != fromToken && recipient != toToken, "invalid recipient addresss")

or if the reserves for the both token is Well contract, this check will help:

require(recpient != address(this))

Assessed type

Other

precision error/rounding issue in readUint128() of LibBytes.sol

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibBytes.sol#L96

Vulnerability details

Impact

iByte = (i - 1) / 2 * 32;

In line 96 of LibBytes.sol, there is a division before multiplication. This can lead to precision/rounding errors

Proof of Concept

The logic with the issue is here --> https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibBytes.sol#L96

iByte = (i - 1) / 2 * 32;

We can make a demo function that outputs the results for when i = 4 and division is done before multiplication and another case when multiplication is done before multiplication (the right way).

   function demo()
       public
       view
       returns ( uint divBeforeMul, uint mulBeforeDiv )
   {
       uint i = 4;

       divBeforeMul =  (i - 1) / 2 * 32;
       mulBeforeDiv =  (i - 1) * 32 / 2;
   }

Running this gives divBeforeMul as 32 and mulBeforeDiv as 48. divBeforeMul is 32 because (4 - 1) / 2 = 1.5 but since solidity uints have no decimal points its reduced to 1. 1 * 32 = 32.

However doing it the other way (multiplication before division) gives 48. This is because (4 - 1) * 32 = 96 and 96 / 2 = 48. 48 is the more exact or correct value.

Note: this is a very similar issue to the rounding issue in LibLastReserveBytes.sol, but i am reporting it differently because it also occurs in another contract.

Tools Used

VS CODE

Recommended Mitigation Steps

do the multiplication first

 iByte = (i - 1) * 32 / 2;

Assessed type

Math

returns of bytes32 as name not handling in `getName` function

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibContractInfo.sol#L34-L44
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibWellConstructor.sol#L77

Vulnerability details

Impact

The getName() function, found in the LibContractInfo.sol contract and can cause a revert when it called in any functions. This is because the getName This could make the the contract or function that calling this function not compliant with the ERC20 standard for certain asset pairs, because the getName() function will return string if the call to the contract is true(the contract have name() function) and this call can run the if(success = true) but then revert in returning the name in string.

note that these function are only called in LibWellConstructor function but still valid because the LibContractInfo is in scope and this libs could be used in future because the team want to audit it.

Proof of Concept

The root cause of the issue is that the getName() function assumes the return type of any ERC20 token to be a string. If the return value is not a string, abi.decode() will revert, and this could happen because getName make a call to the _contract and if this contract contain a function called name() then the if (success) { will be run but the next line will revert because the name() function in the _contract did not return name as a string

the getName() function :

function getName(address _contract) internal view returns (string memory name) {
        (bool success, bytes memory data) = _contract.staticcall(abi.encodeWithSignature("name()"));
        name = new string(8);
        //@audit this case will be run if the _contract contains name(0) function and then revert
        //because the name() function returns bytes not string !
        if (success) {
            name = abi.decode(data, (string));
        } else {
            assembly {
                mstore(add(name, 0x20), shl(224, shr(128, _contract)))
            }
        }
    }

as you can see the function will run the success = true when the contract have name() function without handle the return of name if its string or bytes.

and There are some tokens that aren't compliant, such as Sai from Maker, which returns a bytes32 value:
https://kauri.io/#single/dai-token-guide-for-developers/#token-info

Because this is known to cause issues with tokens that don't fully follow the ERC20 spec, the safeName() function in the BoringCrypto library has a fix for this. The BoringCrypto safeName() function is similar to the one in basin LibContractInfo but it has a returnDataToString() function that handles the case of a bytes32 return value for a token name:
https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L47

if this function called in any of the basin contracts in future may lead to dos because of unhandeled returns of bytes rather than string

Tools Used

manual review

Recommended Mitigation Steps

Use the BoringCrypto safeName() function code to handle the case of a bytes32 return value:

https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L47

Assessed type

Other

Potential array length mismatch leads to index out-of-bounds error

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L632-L637

Vulnerability details

Impact

in the _setReserves function, the user submits two arrays (IERC20[] memory _tokens, uint256[] memory reserves) with the expectation that the indexes of the arrays correspond to the correct values in the other array, and that the lengths will be the same. However, this may not always be the case and if the lengths of the arrays are not the same, the transaction will revert due to index out of bounds errors

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L632-L637

Tools Used

Manual Review

Recommended Mitigation Steps

To prevent this vulnerability, it's important to verify that the lengths of the arrays are equal before executing the function. This is a common practice that can help catch errors before they result in failed transactions.

require(_tokens.length == reserves.length, "Array length not equal")

Assessed type

Error

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

_getArgBytes in ClonePlus.sol (used by Well.sol) overflows & corrupts memory

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/7e51c025d32aff3f2456842c83cda66cda274d11/src/utils/ClonePlus.sol#L37 https://github.com/code-423n4/2023-07-basin/blob/7e51c025d32aff3f2456842c83cda66cda274d11/src/Well.sol#L91 https://github.com/code-423n4/2023-07-basin/blob/7e51c025d32aff3f2456842c83cda66cda274d11/src/Well.sol#L106 https://github.com/code-423n4/2023-07-basin/blob/7e51c025d32aff3f2456842c83cda66cda274d11/src/Well.sol#L177

Vulnerability details

_getArgBytes in ClonePlus.sol initializes bytesLen bytes in memory; then, possibly due to an incorrect copy/paste from getArgIERC20Array, it populates them by copying over 32 * bytesLen bytes (because of the 5-bits shift), therefore overflowing in the operation and corrupting the memory adjacent to the destination.

Impact

Since in Solidity memory is not cleared within internal calls, code executed after the faulty inline assembly may have unpredictable behavior. Without a target bytecode, it's difficult to assess precisely what the impact can be or provide a PoC, but at the same time, it's also hard to assess what the impact cannot be. I would call this a low-probability-but-high-risk finding.

Examples of code that can misbehave are the pieces of logic following _getArgBytes calls, i.e.:

  • pumps(), in updatePumps(), called when reserves are already in memory and yet to be used for swap/liquidity operations
  • wellFunction(), also called by token-moving logic with critical data like reserves already in memory and not yet used

Proof of Concept

I hope you excuse me if I don't go down the rabbit hole here. A strong indication is however that by applying the below mitigation, no tests are broken.

Tools Used

Visual inspection

Recommended Mitigation Steps

Remove the 5-bits shift operation in the _getArgBytes inline assembly to copy only the intended bytesLen bytes:

            calldatacopy(add(data, ONE_WORD), offset, bytesLen)

Assessed type

Library

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Not checking values after performing swap

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L237

Vulnerability details

Impact

Since the call from _swapFrom is going to an external contract, that can have modified safeTransfer and safeTransferFrom functions that can be used to drain the pool

Proof of Concept

For instance, will look up on swapFrom function in Well.sol

function swapFrom(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant expire(deadline) returns (uint256 amountOut) {
        fromToken.safeTransferFrom(msg.sender, address(this), amountIn);
        amountOut = _swapFrom(fromToken, toToken, amountIn, minAmountOut, recipient);
    }

function _swapFrom(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient
    ) internal returns (uint256 amountOut) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _updatePumps(_tokens.length);
        (uint256 i, uint256 j) = _getIJ(_tokens, fromToken, toToken);

        reserves[i] += amountIn;
        uint256 reserveJBefore = reserves[j];
        reserves[j] = _calcReserve(wellFunction(), reserves, j, totalSupply());

        // Note: The rounding approach of the Well function determines whether
        // slippage from imprecision goes to the Well or 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);
        _setReserves(_tokens, reserves);
    }

There is no check for the true amount of tokens that have been sent and no check for the health(balances) of the pool(pair) after the swap.

It would help to make sure that the pair contract received and "sent" the correct amount of tokens.

Tools Used

Manual checking

Recommended Mitigation Steps

It would be better to check all balances after performing a swap, for instance as it is done in UniswapV2
https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L159

The same issue:
https://github.com/code-423n4/2023-07-basin/blob/9403cf973e95ef7219622dbbe2a08396af90b64c/src/Well.sol#L215
https://github.com/code-423n4/2023-07-basin/blob/9403cf973e95ef7219622dbbe2a08396af90b64c/src/Well.sol#L296

Assessed type

Token-Transfer

QA Report

See the markdown file with the details of this report here.

possible precision/rounding error in calcReserve() of ConstantProduct2.sol

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/functions/ConstantProduct2.sol#L58
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/functions/ConstantProduct2.sol#L66

Vulnerability details

Impact

in calcReserve() of ConstantProduct2.sol, there is a divison before multiplication when calculating the reserve amount value. This can cause inaccurate calculations/ precision/rounding errors

Proof of Concept

    function calcReserve(
        uint256[] calldata reserves,
        uint256 j,
        uint256 lpTokenSupply,
        bytes calldata
    ) external pure override returns (uint256 reserve) {
        // Note: potential optimization is to use unchecked math here
        reserve = lpTokenSupply ** 2;
        reserve = LibMath.roundUpDiv(reserve, reserves[j == 1 ? 0 : 1] * EXP_PRECISION);
    }

Now snippet for code in LibMath.sol, the liibrary from which roundUpDiv() logic is used.

    function roundUpDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) return 0;
        return (a - 1) / b + 1;
    }

Tools Used

vs code

Recommended Mitigation Steps

do the multiplication first

    function calcReserve(
        uint256[] calldata reserves,
        uint256 j,
        uint256 lpTokenSupply,
        bytes calldata
    ) external pure override returns (uint256 reserve) {
        // Note: potential optimization is to use unchecked math here
        reserve = lpTokenSupply ** 2;
        reserve = EXP_PRECISION * LibMath.roundUpDiv(reserve, reserves[j == 1 ? 0 : 1]);
    }

Assessed type

Math

Skim() is susceptible to MEV bot attack.

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L603

Vulnerability details

Impact

In the well.sol contract, the external function skim() can be called by anyone to transfer excess tokens in the contract to a recipient. This call is vulnerable to frontrunning attack as the caller can be frontrun to skim the funds before the callers transaction gets approved by miners.
This can lead to a serious loss of funds by the users.

  function skim(address recipient) external nonReentrant returns (uint256[] memory skimAmounts) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _getReserves(_tokens.length);
        skimAmounts = new uint256[](_tokens.length);
        for (uint256 i; i < _tokens.length; ++i) {
            skimAmounts[i] = _tokens[i].balanceOf(address(this)) - reserves[i];
            if (skimAmounts[i] > 0) {
                _tokens[i].safeTransfer(recipient, skimAmounts[i]);
            }
        }
    }

Proof of Concept

Consider a situation, whereby Alice instead of calling the addLiquidity function, mistakenly transfers her tokens directly into the well. Now, Alice wants to get her tokens back which is possible by calling the skim() function which calculates the skimamount by subtracting reserves of the token from the total balance of the token in the contract, which gives the excess token amount. Alice then calls the skim() function but while the transaction is still in the memory pool, if a mev bot sees this transaction, it can frrontrun the transaction, passing in a higher gas than Alice thereby effectively stealing the funds.

Tools Used

Manaul review

Recommended Mitigation Steps

Assessed type

MEV

The parameters 'reserve' are calculated incorrectly in the functions calcReserveAtRatioSwap and calcReserveAtRatioLiquidity.

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/functions/ConstantProduct.sol#L64-L77
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/functions/ConstantProduct.sol#L80-L93

Vulnerability details

Impact

The parameters 'reserve' are calculated incorrectly in the functions calcReserveAtRatioSwap and calcReserveAtRatioLiquidity.When j is greater than reserves.length, the ratio at index j may not exist, and the value of reserve /= reserves.length - 1 may differ from the expected value.

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/functions/ConstantProduct.sol#L64-L77
function calcReserveAtRatioSwap(
uint256[] calldata reserves,
uint256 j,
uint256[] calldata ratios,
bytes calldata
) external pure override returns (uint256 reserve) {
uint256 sumRatio = 0;
for (uint256 i; i < reserves.length; ++i) {
if (i != j) sumRatio += ratios[i];
}
@ sumRatio /= reserves.length - 1;
@ reserve = _prodX(reserves) * ratios[j] / sumRatio;
reserve = reserve.nthRoot(reserves.length);
}

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/functions/ConstantProduct.sol#L80-L93
function calcReserveAtRatioLiquidity(
uint256[] calldata reserves,
uint256 j,
uint256[] calldata ratios,
bytes calldata
) external pure override returns (uint256 reserve) {
for (uint256 i; i < reserves.length; ++i) {
if (i != j) {
@ reserve += ratios[j] * reserves[i] / ratios[i];
}
}
@ reserve /= reserves.length - 1;
}
}

Tools Used

vscode

Recommended Mitigation Steps

Limit the range of the parameter j.

Assessed type

Other

division before multiplication in readLastReserves() of LibLastReserveBytes.sol

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibLastReserveBytes.sol#L97

Vulnerability details

Impact

 iByte = (i - 1) / 2 * 32;

In line 97 of LibLastReserveBytes.sol, there is a division before multiplication. This can lead to precision/rounding errors

Proof of Concept

The logic with the issue is here --> https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibLastReserveBytes.sol#L97

iByte = (i - 1) / 2 * 32;

We can make a demo function that outputs the results for when i = 4 and division is done before multiplication and another case when multiplication is done before multiplication (the right way).

    function demo()
        public
        view
        returns ( uint divBeforeMul, uint mulBeforeDiv )
    {
        uint i = 4;

        divBeforeMul =  (i - 1) / 2 * 32;
        mulBeforeDiv =  (i - 1) * 32 / 2;
    }

Running this gives divBeforeMul as 32 and mulBeforeDiv as 48. divBeforeMul is 32 because (4 - 1) / 2 = 1.5 but since solidity uints have no decimal points its reduced to 1. 1 * 32 = 32.
However doing it the other way (multiplication before division) gives 48. This is because (4 - 1) * 32 = 96 and 96 / 2 = 48. 48 is the more exact or correct value.

Tools Used

VS CODE

Recommended Mitigation Steps

do the multiplication first

  iByte = (i - 1) * 32 / 2;

Assessed type

Math

A malicious user can call boreWell() function with wrong input and skip the If-s

Lines of code

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

Vulnerability details

Impact

A malicious user can call boreWell() function in Aquifer.sol with wrong inputs. This will lead to unknown behave for well reserve.

Proof of Concept

Alice (the malicious user) calls boreWell() with only implementation input, this will create a well of implementation.clone(), because it will skip the first 2 if-s, and after that will skip all of the next If-s
As for result, it will save the implementation wellImplementations[well] = implementation and will emit the event.

The other Users will see that and can deposit in this Well reserve and will lose all of their tokens.

Tools Used

manual

Recommended Mitigation Steps

check the inputs

Assessed type

Other

Tokens can stuck in the contract if user will supply only one token type for initial call "addLiquidity"

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L413
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L436
https://github.com/code-423n4/2023-07-basin/blob/main/src/functions/ConstantProduct2.sol#L49
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L471

Vulnerability details

Impact

Tokens can stuck in the Well contract if a user will call addLiquidity with only one token type for the first ever deposit.

Proof of Concept

After a Well contract is created we can call addLiquidity to deposit tokens and get lp tokens in exchange. Also there is a possibility in the contract to make a depost only for one token type for each addLiquidity call.

So if a user is new to the protocol and might not know what is minLpAmountOut, he could leave 0 value for it. I believe on the website initial value for the javascript field will be 0 as well, so the situation is quite real.

And there is no check for the minLpAmountOut to be more than zero.

function _addLiquidity(
        uint256[] memory tokenAmountsIn,
        uint256 minLpAmountOut,
        address recipient,
        bool feeOnTransfer
    ) internal returns (uint256 lpAmountOut) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _updatePumps(_tokens.length);

        if (feeOnTransfer) {
            for (uint256 i; i < _tokens.length; ++i) {
                if (tokenAmountsIn[i] == 0) continue;
                tokenAmountsIn[i] = _safeTransferFromFeeOnTransfer(_tokens[i], msg.sender, tokenAmountsIn[i]);
                reserves[i] = reserves[i] + tokenAmountsIn[i];
            }
        } else {
            for (uint256 i; i < _tokens.length; ++i) {
                if (tokenAmountsIn[i] == 0) continue;
                _tokens[i].safeTransferFrom(msg.sender, address(this), tokenAmountsIn[i]);
                reserves[i] = reserves[i] + tokenAmountsIn[i];
            }
        }

        lpAmountOut = _calcLpTokenSupply(wellFunction(), reserves) - totalSupply();
        if (lpAmountOut < minLpAmountOut) {
            revert SlippageOut(lpAmountOut, minLpAmountOut);
        }

        _mint(recipient, lpAmountOut);
        _setReserves(_tokens, reserves);
        emit AddLiquidity(tokenAmountsIn, lpAmountOut, recipient);
    }

So for the initial deposit, reserve[0] and reserve[1] for both tokens will be 0 as well.

A user depost an amount for one token. After token transfer and _updatePumps() call, reserve[token0] will be equal for the deposit amount, and the second will still be 0. Due to calculations in the ConstantProduct2 contract a user receives 0 lp tokens.

    function calcLpTokenSupply(
        uint256[] calldata reserves,
        bytes calldata
    ) external pure override returns (uint256 lpTokenSupply) {
        lpTokenSupply = (reserves[0] * reserves[1] * EXP_PRECISION).sqrt();
    }

And with 0 for received lp tokens and same value for minLpAmountOut the check if (lpAmountOut < minLpAmountOut) {} will be passed.

Later on when a user will want to get his tokens back he will need to call removeLiquidity, but it will fail because he has no lp tokens to burn:

...
 tokenAmountsOut = new uint256[](_tokens.length);
 _burn(msg.sender, lpAmountIn);
...

So the first deposited token will stuck in the contract.

If you slightly modify the test in the WellAddLiquidityTest you can see the proof:

    function test_addLiquidity_oneSided() public prank(user) {
        uint256[] memory amounts = new uint256[](2);
        amounts[0] = 10 * 1e18;
        amounts[1] = 0;

        Snapshot memory before;
        AddLiquidityAction memory action;
        action.amounts = amounts;
        action.lpAmountOut = 0;
        action.recipient = user;
        action.fees = new uint256[](2);
        console.log(well.balanceOf(user));
        well.addLiquidity(amounts, 0, user, type(uint256).max);
        console.log(well.balanceOf(user));
    }

Tools Used

Manual review, Foundry test

Recommended Mitigation Steps

For the first depost you should provide a check to restrict minLpAmountOut be equal to 0, like this:
require(minLpAmountOut > 0, "fail lp");

Assessed type

Token-Transfer

User balance was not updated during minting and burning

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L413
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L548

Vulnerability details

Impact

In the function calls to add and remove liquidity, the user balance was not updated. The addLiquidity() and removeLiquidity() functions are used to add and remove liquidity to the well. Whereby, the lpTokens is minted to or burnt from user. This can lead to possible loss of funds or DOSas user balances are not incremented with the lpTokens minted to them or decremented when they are burnt.

   function _addLiquidity(
        uint256[] memory tokenAmountsIn,
        uint256 minLpAmountOut,
        address recipient,
        bool feeOnTransfer
    ) internal returns (uint256 lpAmountOut) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _updatePumps(_tokens.length);

        if (feeOnTransfer) {
            for (uint256 i; i < _tokens.length; ++i) {
                if (tokenAmountsIn[i] == 0) continue;
                tokenAmountsIn[i] = _safeTransferFromFeeOnTransfer(_tokens[i], msg.sender, tokenAmountsIn[i]);
                reserves[i] = reserves[i] + tokenAmountsIn[i];
            }
        } else {
            for (uint256 i; i < _tokens.length; ++i) {
                if (tokenAmountsIn[i] == 0) continue;
                _tokens[i].safeTransferFrom(msg.sender, address(this), tokenAmountsIn[i]);
                reserves[i] = reserves[i] + tokenAmountsIn[i];
            }
        }

        lpAmountOut = _calcLpTokenSupply(wellFunction(), reserves) - totalSupply();
        if (lpAmountOut < minLpAmountOut) {
            revert SlippageOut(lpAmountOut, minLpAmountOut);
        }

        _mint(recipient, lpAmountOut);
        _setReserves(_tokens, reserves);
        emit AddLiquidity(tokenAmountsIn, lpAmountOut, recipient);
    }

Proof of Concept

User calls addLiquidity() inwhich user is minted 2 lpTokens but his balance is not increased by mintamount, this leads to loss of funds by user. Again user then calls removeliquidity() passing the number of lptokens to burn,as there is no check that validates the burning, the lptoken is burnt and tokens are transfered to user but still this does not reflect on user balance also leading to loss of funds by the protocol.

Tools Used

Manual review

Recommended Mitigation Steps

Increment the user's balance after minting and decrement before burning of lptokens.

Assessed type

Token-Transfer

`getSymbol()` cannot get contract information

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/libraries/LibContractInfo.sol#L16-L26

Vulnerability details

Impact

This case is described in the comments of the getSymbol() function

if the contract does not have a symbol function, the first 4 bytes of the address are returned

But in fact, if the contract does not have the symbol() function, the first 4 bytes of the address of the contract will not be obtained

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract poc1{
    function getSymbol(address _contract) public view returns (string memory symbol) {
        (bool success, bytes memory data) = _contract.staticcall(abi.encodeWithSignature("symbol()"));
        symbol = new string(4);
        if (success) {
            symbol = abi.decode(data, (string));
        } else {
            assembly {
                mstore(add(symbol, 0x20), shl(224, shr(128, _contract)))
            }
        }
    }
}


contract poc2{
    function symbol1() public returns (string memory symbol) {
        return "namename";
    }
}

// getSymbol(address(poc2)) => error": "Failed to decode output: null: invalid codepoint at offset 3; unexpected continuation byte (argument=\"bytes\", value=Uint8Array(0x33283581), code=INVALID_ARGUMENT, version=strings/5.7.0)"

Tools Used

vscode

Recommended Mitigation Steps

Directly truncate the first 4 bytes of the address

Assessed type

DoS

QA Report

See the markdown file with the details of this report here.

No any access control for some external functions in Well.sol

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L186-L196 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L203-L213 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L264-L290 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L352-L377 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L392-L399 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L401-L408 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L460-L483 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L495-L517 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L548-L570 https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L603-L613

Vulnerability details

Impact

No any access control for some external functions in Well.sol and the tokens can be transfered to any address by attacker.

In Well.sol file, the following functions can be called by external without any access control.
And the input recipient address is not verified.

  • Well::skim
  • Well::swapFrom
  • Well::swapFromFeeOnTransfer
  • Well::swapTo
  • Well::shift
  • Well::addLiquidity
  • Well::addLiquidityFeeOnTransfer
  • Well::removeLiquidity
  • Well::removeLiquidityOneToken
  • Well::removeLiquidityImbalanced

So token's safeTransfer function is called arbitrarily by attacker.

Attacker can transfer all tokens of reserves by calling this functions.
So, the tokens of reserved can be transfered to any address by attacker.

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L186-L196
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L203-L213
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L264-L290
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L352-L377
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L392-L399
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L401-L408
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L460-L483
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L495-L517
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L548-L570
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L603-L613

Tool used

Manual Review

Recommended Mitigation Steps

Pleae consider to use "@openzeppelin/contracts/ownership/Ownable.sol" and restricts caller by adding modifier.

Assessed type

Invalid Validation

staticall success value not checked

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibContractInfo.sol#L53

Vulnerability details

Impact

Any error on the call could make the function enter in undefined behavior given the fact that it will return a decimals value from the variable data, whose value is undefined upon a staticall error.

Proof of Concept

function getDecimals(address _contract) internal view returns (uint8 decimals) {
        (bool success, bytes memory data) = _contract.staticcall(abi.encodeWithSignature("decimals()"));
        // there is no check for success == true

        decimals = success ? abi.decode(data, (uint8)) : 18; // default to 18 decimals
                                          ^-------------------------------------------------- HERE
}

Tools Used

Manual analysis

Recommended Mitigation Steps

Check the call to be made correctly with the success variable

Assessed type

Invalid Validation

The `swapTo` function in `Well.sol` is susceptible to underflow occurrences.

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L264-L290

Vulnerability details

Impact

The parameter amountOut in the swapTo function has the potential to experience underflow.

Proof of Concept

The swapTo function is utilized to exchange tokens with a specific amountOut and maxAmountIn.

Within this function, the calculation for reserve[j] is performed as follows:
reserves[j] -= amountOut;
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L264-L290

    function swapTo(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 maxAmountIn,
        uint256 amountOut,
        address recipient,
        uint256 deadline
    ) external nonReentrant expire(deadline) returns (uint256 amountIn) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _updatePumps(_tokens.length);
        (uint256 i, uint256 j) = _getIJ(_tokens, fromToken, toToken);

        reserves[j] -= amountOut;
        uint256 reserveIBefore = reserves[i];
        reserves[i] = _calcReserve(wellFunction(), reserves, i, totalSupply());

        // Note: The rounding approach of the Well function determines whether
        // slippage from imprecision goes to the Well or to the User.
        amountIn = reserves[i] - reserveIBefore;

        if (amountIn > maxAmountIn) {
            revert SlippageIn(amountIn, maxAmountIn);
        }

        _swapTo(fromToken, toToken, amountIn, amountOut, recipient);
        _setReserves(_tokens, reserves);
    }

In certain cases, the balance of the j-th token can exceed the corresponding reserves[j] and tokens can be transferred from the pool to the user.

If the value of amountOut exceeds the current value of reserve[j], an underflow occurs, leading to unpredictable errors within the wellFunction().

In the event of an underflow, reserve[j] will contain a significantly large amount, thereby resulting in potential errors.

Consequently, this could lead to a modification in the value of reserves, which may result in critical issues.

Tools Used

Recommended Mitigation Steps

To prevent underflow issues, it is advisable to add a require statement to verify that amountOut is not greater than reserves[j].

require(amountOut <= reserves[j], "Amount out exceeds reserves");

By including this require statement, the function will revert if the condition amountOut <= reserves[j] is not met, preventing any underflow errors.

Assessed type

Under/Overflow

ERC-20 other than the well's supported ones can't be rescued

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L603

Vulnerability details

The skim function in Well.sol loops through the reserve coins to rescue the surplus. However, ERC-20s accidentally transferred, other than the reserve coins, will remain frozen in the contract and lost for good.

Impact

Permanent freezing of ERC-20 funds accidentally transferred

Proof of Concept

  • send any non-reserve ERC-20 to a bored well
  • call the skim function

Tools Used

Manual review

Recommended Mitigation Steps

Add another rescue function that:

  • accepts a token contract address in argument
  • requires the token not to be one of the reserve tokens,
  • transfers the full balance to the given recipient

Assessed type

ERC20

Critical slots being overwritten by bad designed _getSlotForAddress function

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/pumps/MultiFlowPump.sol#L336

Vulnerability details

Impact

A call to the update function (see my prev submission) with custom parameters and being called by a contract deployed to a custom address (see CREATE2 and deterministic deployments) could overwrite critical slots in storage by manipulating the msg.sender address

Proof of Concept

I am not gonna expand on the lack of access control in the update function (prev subm). The issue here is given by the _getSlotForAddress function (seriously, why not use storage the standard way)

function _getSlotForAddress(address addressValue) internal pure returns (bytes32) {
        return bytes32(bytes20(addressValue)); // Because right padded, no collision on adjacent
}

which is called with a "user-controlled" parameter in some places BUT the only place which writes to storage is the update function (the other are views which can return bad results, I may report that as QA, IDK). Anyway, if we call the update function from a contract deployed by deterministic methods (CREATE2 opcode), say, for the shake of the example, to very low level addresses 0x000000000...2 and so on, we could trick the slot variable and start overwriting variables from there with the values on reserves

Tools Used

Manual analysis

Recommended Mitigation Steps

Do not use msg.sender as a value to calculate slots, use storage like everyone else (through state variable in the contract, storage references, sstores and all of that) and add access control to the update function (prev subm)

Assessed type

Other

Unbounded number of _tokens can cause DOS

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L557-L560

Vulnerability details

Impact

There is no limit to the number of _tokens. It is therefore possible to set a large number of tokenssuch that safeTransfer() will run out of gas when transferring tokens. This will cause denial of service to all removeLiquidityImbalanced functions

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L557-L560

Tools Used

Manual Review

Recommended Mitigation Steps

It would be best to set a sanity maximum number of tokens that can be added.

Assessed type

DoS

`getSymbol` could revert if the _contract return symbol as bytes32

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibWellConstructor.sol#L73-L79
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibContractInfo.sol#L16-L27

Vulnerability details

Impact

The getSymbol() function, found in the LibContractInfo.sol contract and can cause a revert when it called in any functions. This is because the getSymbol This could make the contract or function that calling this function reverts, because the getSymbol() function will return string if the call to the contract is true(the contract have symbol() function) and this call can run the if(success = true) but then revert in returning the symbol in string.

note that these function are only called in LibWellConstructor function but still valid because the LibContractInfo is in scope and this libs could be used in future because the team want to audit it.

Proof of Concept

The root cause of the issue is that the getSymbol() function assumes the return type of any ERC20 token to be a string. If the return value is not a string, abi.decode() will revert, and this could happen because getSymbol make a call to the _contract and if this contract contain a function called symbol() then the if (success) will be run but the next line will revert because the Symbol() function in the _contract did not return symbol as a string but it returns it as bytes32

the getSymbol() function:

function getSymbol(address _contract) internal view returns (string memory symbol) {
        (bool success, bytes memory data) = _contract.staticcall(abi.encodeWithSignature("symbol()"));
        symbol = new string(4);
        if (success) {
            //@audit
            symbol = abi.decode(data, (string));
        } else {
            assembly {
                mstore(add(symbol, 0x20), shl(224, shr(128, _contract)))
            }
        }
    }

this function only handle if the function symbol not exist in the _contract and did not handle the case of returning symbol as bytes32

as you can see the function will run the success = true when the contract have symbol() function without handle the return of symbol if its string or bytes.

Because this is known to cause issues with tokens that don't fully follow the ERC20 spec, the safeSymbol() function in the BoringCrypto library has a fix for this. The BoringCrypto safeSymbol() function is similar to the one in basin LibContractInfo but it has a returnDataToString() function that handles the case of a bytes32 return value for a token symbol:

https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L39

if this function called in any of the basin contracts in future may lead to dos because of unhandled returns of bytes rather than string

Tools Used

manual review

Recommended Mitigation Steps

Use the BoringCrypto safeSymbol() function code to handle the case of a bytes32 return value:

https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L47

Assessed type

Other

Immutable values are not maintained on upgrade

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/utils/Clone.sol#L7-L99

Vulnerability details

Impact

Immutable values are not maintained on upgrade

Proof of Concept

Immutable variables are stored in the actual contract code, in the bytecode.
If you are upgrading you cannot take the values, because they are not in the storage.

Tools Used

mannual

Recommended Mitigation Steps

Store the values that will be upgraded later in storage

Assessed type

Upgradable

precision/rounding error in readBytes16() of LibBytes16.sol

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibBytes16.sol#L73

Vulnerability details

Impact

iByte = (i - 1) / 2 * 32;

In line 73 of LibBytes16.sol, there is a division before multiplication. This can lead to precision/rounding errors.

Proof of Concept

The logic with the issue is here --> https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibBytes16.sol#L73

iByte = (i - 1) / 2 * 32;

We can make a demo function that outputs the results for when i = 4 and division is done before multiplication and another case when multiplication is done before multiplication (the right way).

  function demo()
      public
      view
      returns ( uint divBeforeMul, uint mulBeforeDiv )
  {
      uint i = 4;

      divBeforeMul =  (i - 1) / 2 * 32;
      mulBeforeDiv =  (i - 1) * 32 / 2;
  }

Running this gives divBeforeMul as 32 and mulBeforeDiv as 48. divBeforeMul is 32 because (4 - 1) / 2 = 1.5 but since solidity uints have no decimal points its reduced to 1. 1 * 32 = 32.

However doing it the other way (multiplication before division) gives 48. This is because (4 - 1) * 32 = 96 and 96 / 2 = 48. 48 is the more exact or correct value.

Note: this is a very similar issue to the rounding issue in LibLastReserveBytes.sol and LibBytes.sol, but i am reporting it differently because it also occurs in another contract.

Tools Used

VS CODE

Recommended Mitigation Steps

do the multiplication first

iByte = (i - 1) * 32 / 2;

Assessed type

Math

Lack of user input validation in many places

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibWellConstructor.sol#L13-L21
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibWellConstructor.sol#L36-L62
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L186-L196

Vulnerability details

Impact

There is no or very little validation of functions' parameters

Proof of Concept

Just see the files in scope, the links above is to highlight some examples

Tools Used

Manual analysis

Recommended Mitigation Steps

Check functions parameters (some require(addr != address(this) or address(0)..., you know what I mean by that)

Assessed type

Invalid Validation

Anyone can update reserves with arbitrary amounts

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/pumps/MultiFlowPump.sol#L72

Vulnerability details

Impact

Anyone can call the updatefunction inside MultiFlowPump.sol with arbitrary reserves parameter, thus modifying state variables in slots through low level storage functions.

Proof of Concept

There no check to ensure the caller is a trusted one

function update(uint256[] calldata reserves, bytes calldata) external {
                                                            ^-------------- NO MOD NOR REQUIRES BELOW

Tools Used

Manual analysis

Recommended Mitigation Steps

Ensure the caller is a trusted one or validate correctly the reserves parameters

Assessed type

Access Control

Bored wells can receive Ether despite lack of any rescue functionality

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/libraries/LibClone.sol#L16

Vulnerability details

In line with the fact that wells handle ERC-20s and not Ether, Well.sol does not implement any payable function. However, the chosen LibClone.sol implementation adds a receive() function that accepts Ether, also with as little gas as a transfer allows for.

Impact

Any Ether accidentally transferred to a bored well will be accepted by the contract and remain frozen for good.

Proof of Concept

The following test added to Well.Bore.t.sol passes, while it should fail:

    function test_receives_ether() public {
        vm.deal(user, 1 ether);
        vm.prank(user);
        (payable(address(well))).transfer(1 ether);
    }

Tools Used

Manual review

Recommended Mitigation Steps

One of the following:

  • change the Clone implementation to one that does not expose a receive() function
  • add a rescue function for stuck Ether

Assessed type

ETH-Transfer

QA Report

See the markdown file with the details of this report here.

Maliciuos user can get free tokens from Well contract

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L392
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L460
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L352
https://github.com/code-423n4/2023-07-basin/blob/main/test/Well.Shift.t.sol#L19

Vulnerability details

Impact

A malicious user can steal other users tokens form the Well contract by playing with addLiquidity(), shift() and removeLiquidity() functions.

Proof of Concept

After creating a Well contract users are able to add or remove liquidity from it. Also there is an unprotected shift() function that is suppose to be used during multi-step swaps as it is stated in the comments:

When using Wells for a multi-step swap, gas costs can be reduced by "shifting" tokens from one Well to another rather than returning them to a router (like Pipeline).

However the function can be called by any user anytime to get free tokens.

    function shift(
        IERC20 tokenOut,
        uint256 minAmountOut,
        address recipient
    ) external nonReentrant returns (uint256 amountOut) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = new uint256[](_tokens.length);

        for (uint256 i; i < _tokens.length; ++i) {
            reserves[i] = _tokens[i].balanceOf(address(this));
        }
        uint256 j = _getJ(_tokens, tokenOut);
        amountOut = reserves[j] - _calcReserve(wellFunction(), reserves, j, totalSupply());

        if (amountOut >= minAmountOut) {
            tokenOut.safeTransfer(recipient, amountOut);
            reserves[j] -= amountOut;
            _setReserves(_tokens, reserves);
            emit Shift(reserves, tokenOut, amountOut, recipient);
        } else {
            revert SlippageOut(amountOut, minAmountOut);
        }
    }

Moreover shift() function relies on contract token balance, but on on the reserves as it required in other functions. Here is a comment in the code section about it:

        // Use the balances of the pool instead of the stored reserves.
        // If there is a change in token balances relative to the currently
        // stored reserves, the extra tokens can be shifted into `tokenOut`.

Also there is a Foundry test provided by the team in the Well.Shift.t.sol#testFuzz_shift() that proves that any user can get tokens from the unbalances pools.

    function testFuzz_shift(uint256 amount) public prank(user) {
        amount = bound(amount, 1, 1000e18);

        // Transfer `amount` of token0 to the Well
        tokens[0].transfer(address(well), amount);
        Balances memory wellBalanceBeforeShift = getBalances(address(well), well);
        assertEq(wellBalanceBeforeShift.tokens[0], 1000e18 + amount, "Well should have received token0");
        assertEq(wellBalanceBeforeShift.tokens[1], 1000e18, "Well should have NOT have received token1");

        // Get a user with a fresh address (no ERC20 tokens)
        address _user = users.getNextUserAddress();
        Balances memory userBalanceBeforeShift = getBalances(_user, well);

        // Verify that `_user` has no tokens
        assertEq(userBalanceBeforeShift.tokens[0], 0, "User should start with 0 of token0");
        assertEq(userBalanceBeforeShift.tokens[1], 0, "User should start with 0 of token1");

        well.sync();
        uint256 minAmountOut = well.getShiftOut(tokens[1]);
        uint256[] memory calcReservesAfter = new uint256[](2);
        calcReservesAfter[0] = well.getReserves()[0];
        calcReservesAfter[1] = well.getReserves()[1] - minAmountOut;

        vm.expectEmit(true, true, true, true);
        emit Shift(calcReservesAfter, tokens[1], minAmountOut, _user);
        uint256 amtOut = well.shift(tokens[1], minAmountOut, _user);

        uint256[] memory reserves = well.getReserves();
        Balances memory userBalanceAfterShift = getBalances(_user, well);
        Balances memory wellBalanceAfterShift = getBalances(address(well), well);

        // User should have gained token1
        assertEq(userBalanceAfterShift.tokens[0], 0, "User should NOT have gained token0");
        assertEq(userBalanceAfterShift.tokens[1], amtOut, "User should have gained token1");
        assertTrue(userBalanceAfterShift.tokens[1] >= userBalanceBeforeShift.tokens[1], "User should have more token1");

        // Reserves should now match balances
        assertEq(wellBalanceAfterShift.tokens[0], reserves[0], "Well should have correct token0 balance");
        assertEq(wellBalanceAfterShift.tokens[1], reserves[1], "Well should have correct token1 balance");

        // The difference has been sent to _user.
        assertEq(
            userBalanceAfterShift.tokens[1],
            wellBalanceBeforeShift.tokens[1] - wellBalanceAfterShift.tokens[1],
            "User should have correct token1 balance"
        );
        assertEq(
            userBalanceAfterShift.tokens[1],
            userBalanceBeforeShift.tokens[1] + amtOut,
            "User should have correct token1 balance"
        );
        checkInvariant(address(well));
    }

So imagine a situation with the next steps:

  1. Maliciuos user adds liquidity to the well for the one specific token, for example. token0 as it shown in the tests;
  2. Later he calls a shift() function to get token1 from the well contract based on contract token balance. He also doesn't need to burn his lp token to do it.
  3. At the end he calls removeLiquidity to get his token0 back.

He can do it several times in a row and get as many free token1 as he can.

Tools Used

Manual review, Foundry test

Recommended Mitigation Steps

It's better to allow only swap or other system functions use shift option, but not for regular users.

Assessed type

Access Control

Analysis

See the markdown file with the details of this report here.

Lack of Validation in Aquifer Contract's boreWell Function for Implementation Address

Lines of code

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

Vulnerability details

Impact

This vulnerability could allow an attacker to deploy a malicious Well that could steal funds or otherwise harm users.

Proof of Concept

The boreWell function in the Aquifer contract allows anyone to deploy a new Well by cloning a pre-deployed Well implementation. The function takes in an implementation address as a parameter, which is the address of the Well implementation that will be used to deploy the new Well. However, the boreWell function does not check if the implementation address is valid or if it is a pre-deployed one. This means that an attacker could provide a fake implementation address which could then be used to deploy a malicious Well.
For example, an attacker could create a contract that looks like a Well implementation but that actually contains malicious code. They could then provide the address of this contract to the boreWell function. The Aquifer contract would then deploy the malicious contract which could then be used to steal funds or otherwise harm users.
The severity of this vulnerability is increased by the fact that the Aquifer contract is a permissionless contract. This means that anyone can deploy a new Well, which makes it more likely that an attacker will be able to deploy a malicious Well.

Tools Used

Manual analysis

Recommended Mitigation Steps

To mitigate this vulnerability, the boreWell function should be modified to check if the implementation address is valid. This could be done by checking if the contract at the specified address has the IAquiferWell interface implemented.
The function should check if the implementation address is a pre-deployed one

Assessed type

Access Control

Memory overwrite in init arguments

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/f15fe66d57c2f226c232685d16f297e54bcc0939/src/libraries/LibWellConstructor.sol#L71-L81

Vulnerability details

Impact

Writing this

string memory name = LibContractInfo.getSymbol(address(_tokens[0]));
string memory symbol = name;

does not copy the content of name in symbol, what it does is copy the POINTER to the place where the name is stored. Thus, one change in one of them leads to the other pointing to the "updated" variable, breaking the working process of the function and passing wrong arguments to the init function (so the high severity`

Proof of Concept

From Jean Cvllr awesome tutorials about data locations we have

In reality, what happened under the hood is that we create two pointers to memory, named by the variables data and greetings.

When we do data = greetings , we think we are assigning the value cafecafe to the variable data. But we are not assigning anything at all here! We are giving the following instruction to the EVM:

“variable data, I order you to point to the same location in memory that the variable greetings point to!”

(you can read the Yellow paper too but the above is more user-friendly). That means the next code does the same to the two variables because both point to the same place, so one change will affect the other the same way

for (uint256 i = 1; i < _tokens.length; ++i) {
        name = string.concat(name, ":", LibContractInfo.getSymbol(address(_tokens[i])));
        symbol = string.concat(symbol, LibContractInfo.getSymbol(address(_tokens[i])));
}
name = string.concat(name, " ", LibContractInfo.getName(_wellFunction.target), " Well");
symbol = string.concat(symbol, LibContractInfo.getSymbol(_wellFunction.target), "w");

Because of that, the initFunctionCall = abi.encodeWithSignature("init(string,string)", name, symbol); is broken given the fact that name and symbol are the same.

Tools Used

Manual analysis

Recommended Mitigation Steps

Use local variables instead of memory references/pointers

Assessed type

Error

QA Report

See the markdown file with the details of this report here.

Users have the ability to transfer excess tokens to their own account.

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L603-L613
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L352-L377

Vulnerability details

Impact

Users have the freedom to transfer excess tokens to any destination of their choice.

Proof of Concept

The purpose of the skim and shift functions in the Well contract is to transfer any excess tokens held by the Well to a designated recipient address.

It is important to note that there are no modifiers implemented to restrict access to the skim and shift functions within the Well contract.
https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L603-L613

    function skim(address recipient) external nonReentrant returns (uint256[] memory skimAmounts) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = _getReserves(_tokens.length);
        skimAmounts = new uint256[](_tokens.length);
        for (uint256 i; i < _tokens.length; ++i) {
            skimAmounts[i] = _tokens[i].balanceOf(address(this)) - reserves[i];
            if (skimAmounts[i] > 0) {
                _tokens[i].safeTransfer(recipient, skimAmounts[i]);
            }
        }
    }

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L352-L377

    function shift(
        IERC20 tokenOut,
        uint256 minAmountOut,
        address recipient
    ) external nonReentrant returns (uint256 amountOut) {
        IERC20[] memory _tokens = tokens();
        uint256[] memory reserves = new uint256[](_tokens.length);

        // Use the balances of the pool instead of the stored reserves.
        // If there is a change in token balances relative to the currently
        // stored reserves, the extra tokens can be shifted into `tokenOut`.
        for (uint256 i; i < _tokens.length; ++i) {
            reserves[i] = _tokens[i].balanceOf(address(this));
        }
        uint256 j = _getJ(_tokens, tokenOut);
        amountOut = reserves[j] - _calcReserve(wellFunction(), reserves, j, totalSupply());

        if (amountOut >= minAmountOut) {
            tokenOut.safeTransfer(recipient, amountOut);
            reserves[j] -= amountOut;
            _setReserves(_tokens, reserves);
            emit Shift(reserves, tokenOut, amountOut, recipient);
        } else {
            revert SlippageOut(amountOut, minAmountOut);
        }
    }

As a result, if there are no access restrictions or modifiers implemented in the skim and shift functions of the Well contract, it means that anyone can indeed call these functions and specify the recipient address as a parameter.
Consequently, anyone would be able to obtain the excess tokens from the Well by calling the skim and shift functions.

From the this code, anyone can call this function and set the recipient as a parameter, so anyone can get the excess token.

Tools Used

Recommended Mitigation Steps

Absolutely, by utilizing a modifier, we can effectively limit access to the skim and shift functions within the Well.sol.

Modifiers serve as a tool to impose certain conditions or permissions before executing a function.

By implementing a modifier, we can carefully control who is granted permission to call the skim and shift functions and consequently restrict unauthorized access to the transfer of excess tokens.

Assessed type

Access Control

Potential Precision Loss in Calculations

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/pumps/MultiFlowPump.sol#L113

Vulnerability details

Impact

If deltaTimestamp is less than BLOCK_TIME, blocksPassed will round down to zero which lead to wrong calculation in many places

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/pumps/MultiFlowPump.sol#L113

Tools Used

Manual Review

Recommended Mitigation Steps

Always consider if your computation may round down to zero, especially when using small numbers, and if so whether your code should revert

if( blocksPassed == 0 ) { revert("Round down to zero"); }

Assessed type

Math

Lack of _tokens.length check in Well::_setReserves

Lines of code

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L632-L637

Vulnerability details

Impact

_tokens.length is not checked whether it is small than reserves.length or not.
If tokens.length is small than reserves length, it will lead to revert on valid data.

Proof of Concept

https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L632-L637

Tools Used

Manual Review

Recommended Mitigation Steps

add _tokens.length check require statement on start of this function.

function _setReserves(IERC20[] memory _tokens, uint256[] memory reserves) internal {
  + require( _tokens.length == reserves.length, "Invalid Reserves Length");
	for (uint256 i; i < reserves.length; ++i) {
 		...
	}
}

Assessed type

Invalid Validation

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.