Platform: Code4rena
Start Date: 03/07/2023
Pot Size: $40,000 USDC
Total HM: 14
Participants: 74
Period: 7 days
Judge: alcueca
Total Solo HM: 9
Id: 259
League: ETH
Rank: 7/74
Findings: 3
Award: $1,019.61
π Selected for report: 1
π Solo Findings: 0
706.0851 USDC - $706.09
https://github.com/code-423n4/2023-07-basin/blob/9403cf973e95ef7219622dbbe2a08396af90b64c/src/Well.sol#L352-L377 https://github.com/code-423n4/2023-07-basin/blob/9403cf973e95ef7219622dbbe2a08396af90b64c/src/Well.sol#L590-L598
The Wall
contract mandates that the Pumps
should be updated with the previous block's reserves
in case reserves
are changed in the current block to reflect the price change accurately.
However, this doesn't happen in the shift()
and sync()
functions, providing an opportunity for any user to manipulate the reserves
in the current block before updating the Pumps
with new manipulated reserves
values.
The Pumps
(oracles) can be manipulated. This can affect any contract/protocol that utilizes Pumps
as on-chain oracles.
shift()
operation to update reserve
s to desired amounts in the current block, thereby overriding the reserves
from the previous block.swapFrom()/swapTo()
operations to extract back the funds used in the shift()
function. As a result, the attacker is not affected by any arbitration as pool reserves
revert back to the original state.swapFrom()/swapTo()
operations trigger the Pumps
update with invalid reserves
, resulting in oracle manipulation.Note: The sync()
function can also manipulate reserves
in the current block, but it's less useful than shift()
from an attacker's perspective.
This test illustrates how to use shift()
to manipulate Pumps
data.
Create test/pumps/Pump.Manipulation.t.sol
and run forge test --match-test manipulatePump
.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {TestHelper, Call} from "../TestHelper.sol"; import {MultiFlowPump} from "src/pumps/MultiFlowPump.sol"; import {from18} from "test/pumps/PumpHelpers.sol"; contract PumpManipulationTest is TestHelper { MultiFlowPump pump; function setUp() public { pump = new MultiFlowPump( from18(0.5e18), // cap reserves if changed +/- 50% per block from18(0.5e18), // cap reserves if changed +/- 50% per block 12, // block time from18(0.9e18) // ema alpha ); Call[] memory _pumps = new Call[](1); _pumps[0].target = address(pump); _pumps[0].data = new bytes(0); setupWell(2,_pumps); } function test_manipulatePump() public prank(user) { uint256 amountIn = 1 * 1e18; // 1. equal swaps, reserves should be unchanged uint256 amountOut = well.swapFrom(tokens[0], tokens[1], amountIn, 0, user, type(uint256).max); well.swapFrom(tokens[1], tokens[0], amountOut, 0, user, type(uint256).max); uint256[] memory lastReserves = pump.readLastReserves(address(well)); assertApproxEqAbs(lastReserves[0], 1000 * 1e18, 1); assertApproxEqAbs(lastReserves[1], 1000 * 1e18, 1); // 2. equal shift + swap, reserves should be unchanged (but are different) increaseTime(120); tokens[0].transfer(address(well), amountIn); amountOut = well.shift(tokens[1], 0, user); well.swapFrom(tokens[1], tokens[0], amountOut, 0, user, type(uint256).max); lastReserves = pump.readLastReserves(address(well)); assertApproxEqAbs(lastReserves[0], 1000 * 1e18, 1); assertApproxEqAbs(lastReserves[1], 1000 * 1e18, 1); } }
Manual review, Foundry
Update Pumps
in the shift()
and sync()
function.
function shift( IERC20 tokenOut, uint256 minAmountOut, address recipient ) external nonReentrant returns (uint256 amountOut) { IERC20[] memory _tokens = tokens(); - uint256[] memory reserves = new uint256[](_tokens.length); + uint256[] memory reserves = _updatePumps(_tokens.length);
function sync() external nonReentrant { IERC20[] memory _tokens = tokens(); - uint256[] memory reserves = new uint256[](_tokens.length); + uint256[] memory reserves = _updatePumps(_tokens.length);
Oracle
#0 - c4-pre-sort
2023-07-11T13:37:01Z
141345 marked the issue as duplicate of #278
#1 - c4-pre-sort
2023-07-11T13:59:17Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-07-11T13:59:22Z
141345 marked the issue as primary issue
#3 - c4-sponsor
2023-07-17T17:07:37Z
publiuss marked the issue as sponsor confirmed
#4 - c4-judge
2023-08-03T08:05:04Z
alcueca marked the issue as selected for report
#5 - publiuss
2023-08-23T00:52:30Z
This issue has been fixed by updating the Pumps in shift(...)
and sync(...)
:
π Selected for report: 0xprinc
Also found by: 0x11singh99, 0xAnah, 0xWaitress, 0xkazim, 2997ms, 33audits, 404Notfound, 8olidity, CRIMSON-RAT-REACH, CyberPunks, DanielWang888, Deekshith99, Eeyore, Eurovickk, Inspecktor, JGcarv, John, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Qeew, QiuhaoLi, Rolezn, TheSavageTeddy, Topmark, Trust, Udsen, a3yip6, alexzoid, bigtone, codegpt, erebus, fatherOfBlocks, ginlee, glcanvas, hunter_w3b, josephdara, kaveyjoe, kutugu, mahdirostami, max10afternoon, oakcobalt, peanuts, pfapostol, ptsanev, qpzm, radev_sw, ravikiranweb3, sces60107, seth_lawson, te_aut, twcctop, zhaojie, ziyou-
17.5208 USDC - $17.52
Well
implementationEven though the Well
contract is not upgradable, it leverages the OpenZeppelin upgradable contracts implementations for cloning functionality.
It is recommended to call the _disableInitializers()
function in the Well
implementation constructor to prevent any user from calling the init()
function.
There is no direct risk associated with calling the init()
function on the Well
implementation by any user other than the possibility of setting implementation with stupid names for the name
and symbol
variables, which can affect the project's reputation.
Add a constructor with a call to the _disableInitializers()
function to the Well
contract.
bytes32 constant RESERVES_STORAGE_SLOT = bytes32(uint256(keccak256("reserves.storage.slot")) - 1); + + constructor() { + _disableInitializers(); + } + function init(string memory name, string memory symbol) public initializer { }
shl
function leads to unnecessary over-estimation of size in calldatacopy
within _getArgBytes()
functionIn the ClonePlus
contract, within the _getArgBytes()
function, the size parameter for calldatacopy
is incorrectly calculated due to the misuse of the shl(5, bytesLen)
operation. This operation results in shifting bytesLen
5 places to the left, effectively multiplying bytesLen
by 32 (2^5), leading to an over-estimation of the required size.
While this issue does not have a direct impact on the data
returned by _getArgBytes()
β since data
length is correctly allocated with bytesLen
, and any excess during the calldatacopy
call is omittedβit can potentially lead to confusion and unexpected behaviors.
/src/utils/ClonePlus.sol#L36-L38
File: ClonePlus.sol 36: assembly { 37: calldatacopy(add(data, ONE_WORD), offset, shl(5, bytesLen)) 38: }
Avoid unnecessary left-shifting by directly using bytesLen
as the size parameter in the calldatacopy
function, which accurately reflects the size of the bytes data to be copied.
function _getArgBytes(uint256 argOffset, uint256 bytesLen) internal pure returns (bytes memory data) { if (bytesLen == 0) return data; uint256 offset = _getImmutableArgsOffset() + argOffset; data = new bytes(bytesLen); // solhint-disable-next-line no-inline-assembly assembly { - calldatacopy(add(data, ONE_WORD), offset, shl(5, bytesLen)) + calldatacopy(add(data, ONE_WORD), offset, bytesLen) } }
SafeCast
usageThe SafeCast
library is imported, but its functionality is not used in the Aquifer
, Well
and MultiFlowPump
contracts.
/src/Well.sol#L8 /src/Well.sol#L23
File: Well.sol 8: import {SafeCast} from "oz/utils/math/SafeCast.sol"; 23: using SafeCast for uint256;
/src/Aquifer.sol#L6 /src/Aquifer.sol#L20
File: Aquifer.sol 6: import {SafeCast} from "oz/utils/math/SafeCast.sol"; 20: using SafeCast for uint256;
/src/pumps/MultiFlowPump.sol#L13 /src/pumps/MultiFlowPump.sol#L30
File: MultiFlowPump.sol 13: import {SafeCast} from "oz/utils/math/SafeCast.sol"; 30: using SafeCast for uint256;
Remove redundant SafeCast
library usage.
There are redundant import declarations across contracts.
In the Aquifer
contract the IWellFunction
, Well
, Call
and IERC20
imports are redundant.
/src/Aquifer.sol#L8 /src/Aquifer.sol#L10
File: Aquifer.sol 8: import {IWellFunction} from "src/interfaces/IWellFunction.sol"; 10: import {Well, IWell, Call, IERC20} from "src/Well.sol";
Remove redundant imports.
The code heavily uses storage and constant variables with implicit visibility; this does not directly affect security, but does compromise the code's readability.
File: Aquifer.sol 25: mapping(address => address) wellImplementations;
/src/Well.sol#L26-L29 /src/Well.sol#L77-L82
File: Well.sol 26: uint256 constant ONE_WORD = 32; 27: uint256 constant PACKED_ADDRESS = 20; 28: uint256 constant ONE_WORD_PLUS_PACKED_ADDRESS = 52; // For gas efficiency purposes 29: bytes32 constant RESERVES_STORAGE_SLOT = bytes32(uint256(keccak256("reserves.storage.slot")) - 1); 77: uint256 constant LOC_AQUIFER_ADDR = 0; 78: uint256 constant LOC_TOKENS_COUNT = LOC_AQUIFER_ADDR + PACKED_ADDRESS; 79: uint256 constant LOC_WELL_FUNCTION_ADDR = LOC_TOKENS_COUNT + ONE_WORD; 80: uint256 constant LOC_WELL_FUNCTION_DATA_LENGTH = LOC_WELL_FUNCTION_ADDR + PACKED_ADDRESS; 81: uint256 constant LOC_PUMPS_COUNT = LOC_WELL_FUNCTION_DATA_LENGTH + ONE_WORD; 82: uint256 constant LOC_VARIABLE = LOC_PUMPS_COUNT + ONE_WORD;
/src/pumps/MultiFlowPump.sol#L36-L39
File: MultiFlowPump.sol 36: bytes16 immutable LOG_MAX_INCREASE; 37: bytes16 immutable LOG_MAX_DECREASE; 38: bytes16 immutable ALPHA; 39: uint256 immutable BLOCK_TIME;
To improve the code readability, it is recommended to explicitly define the visibility for all storage and constant variables.
Aquifer
contractThe constructor declaration in the Aquifer
contract is redundant and decreases code readability.
File: Aquifer.sol 27: constructor() ReentrancyGuard() {}
Remove redundant constructor declaration.
draft-ERC20PermitUpgradeable.sol
draft contractThe ERC20PermitUpgradeable
is final as of 2022-11-01 and should be imported correctly.
File: Well.sol 6: import {ERC20Upgradeable, ERC20PermitUpgradeable} from "ozu/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol";
Import from "ozu/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
In the init()
function, parameters name
and symbol
are shadowing ERC20Upgradeable.name()
and ERC20Upgradeable.symbol()
view functions.
File: Well.sol 31: function init(string memory name, string memory symbol) public initializer { 32: __ERC20Permit_init(name); 33: __ERC20_init(name, symbol);
To avoid any potential confusion or error due to function shadowing, consider renaming the name
and symbol
parameters.
__ReentrancyGuard_init()
call missed in the Well.init()
functionThe __ReentrancyGuard_init()
is not called during the Well.init()
function execution.
In this case there is no direct issue with the missed __ReentrancyGuard_init()
call, only the first call to any function with nonReentrant
modifier will use more gas as the storage variable ReentrancyGuardUpgradeable._status
will be initialized for the first time.
File: Well.sol 31: function init(string memory name, string memory symbol) public initializer {
Add the missing __ReentrancyGuard_init()
call to the Well.init()
function to ensure the reentrancy guard is properly set up, reducing the gas cost for subsequent nonReentrant
function calls.
#0 - c4-pre-sort
2023-07-12T09:30:55Z
141345 marked the issue as high quality report
#1 - 141345
2023-07-13T14:54:10Z
#2 - c4-sponsor
2023-07-22T10:15:34Z
publiuss marked the issue as sponsor confirmed
#3 - c4-judge
2023-08-04T21:16:39Z
alcueca marked the issue as grade-a
296.0006 USDC - $296.00
The overall quality of the code is commendable, especially the innovative approach taken to separate the AMM into three distinct components.
However, there's an area that warrants further attention.
The testing strategy could be improved by incorporating more comprehensive integration tests, which include the real logic of all components interacting in a full protocol flow. This would ensure that the behavior of the whole system under various conditions is correctly validated.
I bring to this audit about 30 hours of in-depth manual code review and extensive experience as a lead smart contract auditor at a prominent auditing firm. My transition to Code4Rena contests has allowed me to take a fresh, competitive approach to auditing.
The codebase was evaluated through a comprehensive line-by-line manual review. This method ensures a detailed understanding of each component and its role within the system, as well as an appreciation of the overall system architecture.
The protocolβs architecture, with its clean and logical separation into three distinct components (Well, WellFunction, and Pumps), is well-structured. The innovative design promotes versatility and easy comprehension, and no modifications are recommended at this time.
The codebase is of good quality: well-organized, readable, and largely consistent. Some minor issues were noted: the occasional inconsistency (pragma version, the use of i++ over ++i), typos in the comments, unnecessary use of the override modifier in the aquifer() function, and the opportunity to use inherit doc comments in interfaces. Despite these small issues, the code is proficiently implemented and structured.
However, an enhanced integration test suite that covers the real logic of all components in a full protocol flow is recommended for a more rigorous quality check.
No centralization risks were found in the sample components. The "Guilty until proven innocent" approach mitigates potential centralization risks in the system architecture, but this depends heavily on each component's implementation. Each new component should be treated with caution and audited separately to ensure decentralization.
The sample components emulate a two-token AMM similar to Uniswap V2 protocol, and their implementation is sound.
However, it's crucial to individually audit any future different implementations to maintain the integrity of the system mechanisms.
One systemic risk tied to the Well contract has been identified. It relates to an erroneous updating of the Pumps contracts under certain conditions, which may arise due to the absence of comprehensive integration tests.
Scalability and performance risks could surface if an excessive number of Pumps are added to the Well, heavily contingent on the implementation of the Pumps.
There's also an economic and financial risk associated with incorrect implementation of the WellFunction contracts if they are not audited.
However, there were no identified dependencies, regulatory, or legal risks in the system.
In conclusion, this project manifests commendable code quality and innovative architectural design. While some minor issues were found in code consistency and commenting, the major recommendation is to bolster the testing suite to cover comprehensive integration tests. This will help assure the functionality of the system in various scenarios and minimize systemic risks.
30 hours
#0 - c4-pre-sort
2023-07-12T12:15:28Z
141345 marked the issue as high quality report
#1 - c4-sponsor
2023-08-03T20:33:37Z
publiuss marked the issue as sponsor acknowledged
#2 - c4-judge
2023-08-05T20:16:17Z
alcueca marked the issue as grade-a