Basin - 0xAnah'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: 36/74

Findings: 2

Award: $25.41

QA:
grade-a
Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Non-Critical Issues

[N-01] Unnecessary Code

The is no need to cache the token array since all that is needed in the function is just the length of the token array.

3 Instances

function getAddLiquidityOut(uint256[] memory tokenAmountsIn) external view returns (uint256 lpAmountOut) { IERC20[] memory _tokens = tokens(); uint256[] memory reserves = _getReserves(_tokens.length); for (uint256 i; i < _tokens.length; ++i) { reserves[i] = reserves[i] + tokenAmountsIn[i]; } lpAmountOut = _calcLpTokenSupply(wellFunction(), reserves) - totalSupply(); }

The above code be refactored to:

function getAddLiquidityOut(uint256[] memory tokenAmountsIn) external view returns (uint256 lpAmountOut) { uint256 _tokensLength = tokens().length; uint256[] memory reserves = _getReserves(_tokensLength); for (uint256 i; i < _tokensLength; ++i) { reserves[i] = reserves[i] + tokenAmountsIn[i]; } lpAmountOut = _calcLpTokenSupply(wellFunction(), reserves) - totalSupply(); }
function getRemoveLiquidityOut(uint256 lpAmountIn) external view returns (uint256[] memory tokenAmountsOut) { IERC20[] memory _tokens = tokens(); uint256[] memory reserves = _getReserves(_tokens.length); uint256 lpTokenSupply = totalSupply(); tokenAmountsOut = _calcLPTokenUnderlying(wellFunction(), lpAmountIn, reserves, lpTokenSupply); }

The above code could be refactored to:

function getRemoveLiquidityOut(uint256 lpAmountIn) external view returns (uint256[] memory tokenAmountsOut) { uint256 _tokensLength = tokens().length; uint256[] memory reserves = _getReserves(_tokensLength); uint256 lpTokenSupply = totalSupply(); tokenAmountsOut = _calcLPTokenUnderlying(wellFunction(), lpAmountIn, reserves, lpTokenSupply); }
function getRemoveLiquidityImbalancedIn(uint256[] calldata tokenAmountsOut) external view returns (uint256 lpAmountIn) { IERC20[] memory _tokens = tokens(); uint256[] memory reserves = _getReserves(_tokens.length); for (uint256 i; i < _tokens.length; ++i) { reserves[i] = reserves[i] - tokenAmountsOut[i]; } lpAmountIn = totalSupply() - _calcLpTokenSupply(wellFunction(), reserves); }

The above code be refactored to:

function getRemoveLiquidityImbalancedIn(uint256[] calldata tokenAmountsOut) external view returns (uint256 lpAmountIn) { uint256 _tokensLength = tokens().length; uint256[] memory reserves = _getReserves(_tokensLength); for (uint256 i; i < _tokensLength; ++i) { reserves[i] = reserves[i] - tokenAmountsOut[i]; } lpAmountIn = totalSupply() - _calcLpTokenSupply(wellFunction(), reserves); }

[N-02] Visibility should be set explicitly

The visibility of the variables should be set explicitly rather than defaulting to internal for clarity.

3 Instances

26: uint256 constant ONE_WORD = 32; 27: uint256 constant PACKED_ADDRESS = 20; 28: uint256 constant ONE_WORD_PLUS_PACKED_ADDRESS = 52; 77: uint256 constant LOC_AQUIFER_ADDR = 0;

The visibility of these constants could be set to private since they are not to be inherited.

The code could be refactored to:

26: uint256 private constant ONE_WORD = 32; 27: uint256 private constant PACKED_ADDRESS = 20; 28: uint256 private constant ONE_WORD_PLUS_PACKED_ADDRESS = 52; 77: uint256 private constant LOC_AQUIFER_ADDR = 0;
25: mapping(address => address) wellImplementations;

The wellImplementations mapping could be made private since the contract is not inherited and there is a getter function for the mapping defined in the contract.

The code could be refactored to:

25: mapping(address => address) private wellImplementations;
23: uint256 constant EXP_PRECISION = 1e12;

The visibility of these constants could be set to private since they are not to be inherited.

The code could be refactored to:

23: uint256 private constant EXP_PRECISION = 1e12;

[N-03] The if statement can be refactored to the ternary

The normal if / else statement can be written in a shorthand way using the ternary operator. It increases readability and reduces the number of lines of code.

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

In the code above the nested if-else statements could be re-structured to ternary therefore the refactored code would be:

if (immutableData.length > 0) { well = salt != bytes32(0) ? implementation.cloneDeterministic(immutableData, salt) : implementation.clone(immutableData); } else { well = salt != bytes32(0) ? implementation.cloneDeterministic(salt) : implementation.clone(); }

#0 - c4-pre-sort

2023-07-13T14:45:26Z

141345 marked the issue as high quality report

#1 - c4-pre-sort

2023-07-14T05:50:11Z

141345 marked the issue as low quality report

#2 - c4-judge

2023-08-04T21:26:10Z

alcueca marked the issue as grade-a

Awards

7.8853 USDC - $7.89

Labels

bug
G (Gas Optimization)
grade-b
high quality report
sponsor confirmed
G-18

External Links

GAS OPTIMIZATIONS

[G-01] Unused Imports

Some files were imported and were not used these files costs gas during deployment and generally this is bad coding practice. You should remove these statements or use the imported files.

3 Instances

[G-02] Move lesser gas costing require checks to the top

Revert() statements that check input arguments or cost lesser gas should be at the top of the function. Checks that involve constants should come before checks that involve state variables, function calls, and calculations. By doing these checks first, the function is able to revert before wasting alot of gas in a function that may ultimately revert in the unhappy case.

function readTwaReserves( address well, bytes calldata startCumulativeReserves, uint256 startTimestamp, bytes memory ) public view returns (uint256[] memory twaReserves, bytes memory cumulativeReserves) { bytes16[] memory byteCumulativeReserves = _readCumulativeReserves(well); bytes16[] memory byteStartCumulativeReserves = abi.decode(startCumulativeReserves, (bytes16[])); twaReserves = new uint256[](byteCumulativeReserves.length); // Overflow is desired on `startTimestamp`, so SafeCast is not used. bytes16 deltaTimestamp = _getDeltaTimestamp(uint40(startTimestamp)).fromUInt(); if (deltaTimestamp == bytes16(0)) { revert NoTimePassed(); } for (uint256 i; i < byteCumulativeReserves.length; ++i) { // Currently, there is no support for overflow. twaReserves[i] = (byteCumulativeReserves[i].sub(byteStartCumulativeReserves[i])).div(deltaTimestamp).pow_2ToUInt(); } cumulativeReserves = abi.encode(byteCumulativeReserves); }

In the function above the statements bytes16 deltaTimestamp = _getDeltaTimestamp(uint40(startTimestamp)).fromUInt(); and if (deltaTimestamp == bytes16(0)) {revert NoTimePassed();} should be moved to the top of the function so that in scenarios where the if condition results to true the function can fail early and cheaply rather than executing gas consuming statements before failing.

The function could be refactored to:

function readTwaReserves( address well, bytes calldata startCumulativeReserves, uint256 startTimestamp, bytes memory ) public view returns (uint256[] memory twaReserves, bytes memory cumulativeReserves) { // Overflow is desired on `startTimestamp`, so SafeCast is not used. bytes16 deltaTimestamp = _getDeltaTimestamp(uint40(startTimestamp)).fromUInt(); if (deltaTimestamp == bytes16(0)) { revert NoTimePassed(); } bytes16[] memory byteCumulativeReserves = _readCumulativeReserves(well); bytes16[] memory byteStartCumulativeReserves = abi.decode(startCumulativeReserves, (bytes16[])); twaReserves = new uint256[](byteCumulativeReserves.length); for (uint256 i; i < byteCumulativeReserves.length; ++i) { // Currently, there is no support for overflow. twaReserves[i] = (byteCumulativeReserves[i].sub(byteStartCumulativeReserves[i])).div(deltaTimestamp).pow_2ToUInt(); } cumulativeReserves = abi.encode(byteCumulativeReserves); }
function _readCumulativeReserves(address well) internal view returns (bytes16[] memory cumulativeReserves) { bytes32 slot = _getSlotForAddress(well); uint256[] memory reserves = IWell(well).getReserves(); (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } uint256 offset = _getSlotsOffset(numberOfReserves) << 1; assembly { slot := add(slot, offset) } cumulativeReserves = slot.readBytes16(numberOfReserves); uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); bytes16 deltaTimestampBytes = deltaTimestamp.fromUInt(); bytes16 blocksPassed = (deltaTimestamp / BLOCK_TIME).fromUInt(); // Currently, there is so support for overflow. for (uint256 i; i < cumulativeReserves.length; ++i) { lastReserves[i] = _capReserve(lastReserves[i], reserves[i].fromUIntToLog2(), blocksPassed); cumulativeReserves[i] = cumulativeReserves[i].add(lastReserves[i].mul(deltaTimestampBytes)); } }

In the function above it would be better if the uint256[] memory reserves = IWell(well).getReserves(); comes after the if statement block so that in scenarios where the if condition results to true the EVM would not consume gas for executing the uint256[] memory reserves = IWell(well).getReserves(); statement.

The code could be refactored to:

function _readCumulativeReserves(address well) internal view returns (bytes16[] memory cumulativeReserves) { bytes32 slot = _getSlotForAddress(well); (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } uint256[] memory reserves = IWell(well).getReserves(); uint256 offset = _getSlotsOffset(numberOfReserves) << 1; assembly { slot := add(slot, offset) } cumulativeReserves = slot.readBytes16(numberOfReserves); uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); bytes16 deltaTimestampBytes = deltaTimestamp.fromUInt(); bytes16 blocksPassed = (deltaTimestamp / BLOCK_TIME).fromUInt(); // Currently, there is so support for overflow. for (uint256 i; i < cumulativeReserves.length; ++i) { lastReserves[i] = _capReserve(lastReserves[i], reserves[i].fromUIntToLog2(), blocksPassed); cumulativeReserves[i] = cumulativeReserves[i].add(lastReserves[i].mul(deltaTimestampBytes)); } }
function readInstantaneousReserves(address well, bytes memory) public view returns (uint256[] memory emaReserves) { bytes32 slot = _getSlotForAddress(well); uint256[] memory reserves = IWell(well).getReserves(); (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } uint256 offset = _getSlotsOffset(numberOfReserves); assembly { slot := add(slot, offset) } bytes16[] memory lastEmaReserves = slot.readBytes16(numberOfReserves); uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); bytes16 blocksPassed = (deltaTimestamp / BLOCK_TIME).fromUInt(); bytes16 alphaN = ALPHA.powu(deltaTimestamp); emaReserves = new uint256[](numberOfReserves); for (uint256 i; i < numberOfReserves; ++i) { lastReserves[i] = _capReserve(lastReserves[i], reserves[i].fromUIntToLog2(), blocksPassed); emaReserves[i] = lastReserves[i].mul((ABDKMathQuad.ONE.sub(alphaN))).add(lastEmaReserves[i].mul(alphaN)).pow_2ToUInt(); } }

In the function above it would be better if the uint256[] memory reserves = IWell(well).getReserves(); comes after the if statement block so that in scenarios where the if condition results to true the EVM would not consume gas for executing the uint256[] memory reserves = IWell(well).getReserves(); statement.

The code could be refactored to:

function readInstantaneousReserves(address well, bytes memory) public view returns (uint256[] memory emaReserves) { bytes32 slot = _getSlotForAddress(well); (uint8 numberOfReserves, uint40 lastTimestamp, bytes16[] memory lastReserves) = slot.readLastReserves(); if (numberOfReserves == 0) { revert NotInitialized(); } uint256[] memory reserves = IWell(well).getReserves(); uint256 offset = _getSlotsOffset(numberOfReserves); assembly { slot := add(slot, offset) } bytes16[] memory lastEmaReserves = slot.readBytes16(numberOfReserves); uint256 deltaTimestamp = _getDeltaTimestamp(lastTimestamp); bytes16 blocksPassed = (deltaTimestamp / BLOCK_TIME).fromUInt(); bytes16 alphaN = ALPHA.powu(deltaTimestamp); emaReserves = new uint256[](numberOfReserves); for (uint256 i; i < numberOfReserves; ++i) { lastReserves[i] = _capReserve(lastReserves[i], reserves[i].fromUIntToLog2(), blocksPassed); emaReserves[i] = lastReserves[i].mul((ABDKMathQuad.ONE.sub(alphaN))).add(lastEmaReserves[i].mul(alphaN)).pow_2ToUInt(); } }

[G-03] Declaring redundant variables

Some varibles were defined even though they are used once. Not defining variables can reduce gas cost and contract size

2 Instances (12 gas saved)

function getRemoveLiquidityOneTokenOut( uint256 lpAmountIn, IERC20 tokenOut ) external view returns (uint256 tokenAmountOut) { IERC20[] memory _tokens = tokens(); uint256[] memory reserves = _getReserves(_tokens.length); uint256 j = _getJ(_tokens, tokenOut); tokenAmountOut = _getRemoveLiquidityOneTokenOut(lpAmountIn, j, reserves); }

In the function above the reserves variable is declared and is only used once. You could invoke the _getReserves(token.length) function directly in the _getRemoveLiquidityOneTokenOut() function rather than assigning its value to a memory variable which would cost mstore (3 gas) and mload (3 gas) operations.

The code could be refactored to:

function getRemoveLiquidityOneTokenOut( uint256 lpAmountIn, IERC20 tokenOut ) external view returns (uint256 tokenAmountOut) { IERC20[] memory _tokens = tokens(); uint256 j = _getJ(_tokens, tokenOut); tokenAmountOut = _getRemoveLiquidityOneTokenOut(lpAmountIn, j, _getReserves(_tokens.length)); }
function getRemoveLiquidityOut(uint256 lpAmountIn) external view returns (uint256[] memory tokenAmountsOut) { IERC20[] memory _tokens = tokens(); uint256[] memory reserves = _getReserves(_tokens.length); uint256 lpTokenSupply = totalSupply(); tokenAmountsOut = _calcLPTokenUnderlying(wellFunction(), lpAmountIn, reserves, lpTokenSupply); }

In the function above the reserves variable is declared and is only used once. You could invoke the _getReserves(token.length) function directly in the _calcLPTokenUnderlying() function rather than assigning its value to a memory variable which would cost mstore (3 gas) and mload (3 gas) operations.

The code could be refactored to:

function getRemoveLiquidityOut(uint256 lpAmountIn) external view returns (uint256[] memory tokenAmountsOut) { IERC20[] memory _tokens = tokens(); uint256 lpTokenSupply = totalSupply(); tokenAmountsOut = _calcLPTokenUnderlying(wellFunction(), lpAmountIn, _getReserves(_tokens.length), lpTokenSupply); }

[G-04] Empty blocks should be removed or emit something

The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting.

#0 - c4-pre-sort

2023-07-13T12:55:18Z

141345 marked the issue as high quality report

#1 - publiuss

2023-07-24T13:57:42Z

[4] is not valid as you cannot have a catch statement without a try. [1], [2] and [3] seem to be valid.

#2 - c4-sponsor

2023-07-24T13:57:46Z

publiuss marked the issue as sponsor confirmed

#3 - c4-judge

2023-08-05T11:13:23Z

alcueca marked the issue as grade-b

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