QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: low
Invalid

UpliftOnlyExample contract has unsanitized params in constructor. An oversight could disrupt the contract's intended functionality.

Summary

The UpliftOnlyExample contract has unsanitized params in the constructor. As it can be seen in the code snippet below, the constructor of the contract accepts a _upliftFeeBps, _minWithdrawalFeeBps, _updateWeightRunnerParam, and Version, and so on. These four parameters are important parameters for the contract, and they are used in the contract's logic. However, the contract does not check the validity of these parameters. If the contract is deployed with invalid parameters, it could disrupt the contract's intended functionality. Where an empty string value for version could be a potential issue and the parameters _upliftFeeBps, and _minWithdrawalFeeBps are used to evaluate fee per LP (Liquidity Provider) and if these values are set to zero or to some other undesired hostile value, it could disrupt the contract's intended functionality. Moreover, same for _updateWeightRunnerParam which is used to update the weight of the pool.

Vulnerability Details

see the code snippet below:

UpliftOnlyExample.sol::constructor:

constructor(
IVault vault,
IWETH weth,
IPermit2 permit2,
uint64 _upliftFeeBps,
uint64 _minWithdrawalFeeBps,
address _updateWeightRunnerParam,
// @info: missing a modifier for version check
string memory version,
string memory name,
string memory symbol
) MinimalRouter(vault, weth, permit2, version) Ownable(msg.sender) {
require(bytes(name).length > 0 && bytes(symbol).length > 0, "NAMEREQ"); //Must provide a name / symbol
lpNFT = new LPNFT(name, symbol, address(this));
// @info: missing value validation for _upliftFeeBps and _minWithdrawalFeeBps
upliftFeeBps = _upliftFeeBps;
minWithdrawalFeeBps = _minWithdrawalFeeBps;
// @info: missing value validation for _updateWeightRunnerParam
_updateWeightRunner = _updateWeightRunnerParam;
}

Also, there should have a min and max value check for the _upliftFeeBps, _minWithdrawalFeeBps parameters.

PoC

The following code snippet demonstrates how the contract could be deployed with invalid parameters:

Steps to reproduce:

  1. Go to the test folder inside pool-hooks folder then -> foundry folder -> UpliftExample.t.sol

  2. Replace the setUp function with the following code snippet:

function setUp() public virtual override {
BaseTest.setUp();
(address ownerLocal, address addr1Local, address addr2Local) = (vm.addr(1), vm.addr(2), vm.addr(3));
owner = ownerLocal;
addr1 = addr1Local;
addr2 = addr2Local;
vault = deployVaultMock();
vm.label(address(vault), "vault");
vaultExtension = IVaultExtension(vault.getVaultExtension());
vm.label(address(vaultExtension), "vaultExtension");
vaultAdmin = IVaultAdmin(vault.getVaultAdmin());
vm.label(address(vaultAdmin), "vaultAdmin");
authorizer = BasicAuthorizerMock(address(vault.getAuthorizer()));
vm.label(address(authorizer), "authorizer");
factoryMock = PoolFactoryMock(address(vault.getPoolFactoryMock()));
vm.label(address(factoryMock), "factory");
router = deployRouterMock(IVault(address(vault)), weth, permit2);
vm.label(address(router), "router");
batchRouter = deployBatchRouterMock(IVault(address(vault)), weth, permit2);
vm.label(address(batchRouter), "batch router");
feeController = vault.getProtocolFeeController();
vm.label(address(feeController), "fee controller");
vm.startPrank(address(vaultAdmin));
updateWeightRunner = new MockUpdateWeightRunner(address(vaultAdmin), address(addr2), true);
vm.label(address(updateWeightRunner), "updateWeightRunner");
updateWeightRunner.setQuantAMMSwapFeeTake(0);
vm.stopPrank();
vm.startPrank(owner);
upliftOnlyRouter = new UpliftOnlyExample(
IVault(address(vault)),
weth,
permit2,
@> 0,
// 2000000, // upliftFeeBps
// 200, // upliftFeeBps
@> 0,
// 5, // minWithdrawalFeeBps
// 500000, // minWithdrawalFeeBps
// address(updateWeightRunner),
@> address(0),
// "Uplift LiquidityPosition v1",
@> "",
"Uplift LiquidityPosition v1",
"Uplift LiquidityPosition v1"
);
vm.stopPrank();
vm.label(address(upliftOnlyRouter), "upliftOnlyRouter");
// Here the Router is also the hook.
poolHooksContract = address(upliftOnlyRouter);
(pool,) = createPool();
// Approve vault allowances.
for (uint256 i = 0; i < users.length; ++i) {
address user = users[i];
vm.startPrank(user);
approveForSender();
vm.stopPrank();
}
if (pool != address(0)) {
approveForPool(IERC20(pool));
}
// Add initial liquidity.
initPool();
(daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc));
}

@> is used to show the changes in the code snippet.

  1. The _updateWeightRunnerParam parameter has no gettter in the contract, so we performed some mutational testing to check the contract's behavior with invalid parameter. Go to UpliftOnlyExample.sol and make the _updateWeightRunner variable public.

address public immutable _updateWeightRunner;
  1. Now add the following test snippet just below the setUp function in the UpliftExample.t.sol file:

function testUpliftExampleConstructorValues() public {
console.log("version: ", upliftOnlyRouter.version());
console.log("upliftFeeBps: ", upliftOnlyRouter.upliftFeeBps());
console.log("minWithdrawalFeeBps: ", upliftOnlyRouter.minWithdrawalFeeBps());
console.log("updateWeightRunner: ", upliftOnlyRouter._updateWeightRunner());
assertEq(bytes(upliftOnlyRouter.version()).length, 0, "version should be empty");
assertEq(upliftOnlyRouter.upliftFeeBps(), 0, "upliftFeeBps should be 0");
assertEq(upliftOnlyRouter.minWithdrawalFeeBps(), 0, "minWithdrawalFeeBps should be 0");
assertEq(upliftOnlyRouter._updateWeightRunner(), address(0), "updateWeightRunner should be 0");
}
  1. Now run the test cases using the following command: (Make sure you are in the pool-hooks directory)

forge test --mt testUpliftExampleConstructorValues -vv
  1. As we can see in the output, the contract is deployed with invalid parameters and the contract's intended functionality is disrupted.

[⠢] Compiling...
No files changed, compilation skipped
Ran 1 test for test/foundry/UpliftExample.t.sol:UpliftOnlyExampleTest
[PASS] testUpliftExampleConstructorValues() (gas: 28500)
Logs:
version:
upliftFeeBps: 0
minWithdrawalFeeBps: 0
updateWeightRunner: 0x0000000000000000000000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 25.40ms (244.00µs CPU time)
Ran 1 test suite in 27.21ms (25.40ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

  1. The contract could be deployed with invalid parameters.

  2. The contract's intended functionality could be disrupted.

  3. An empty string value for version could be a potential issue.

  4. The parameters _upliftFeeBps, and _minWithdrawalFeeBps are used to evaluate fee per LP (Liquidity Provider) and if these values are set to zero or to some other undesired hostile value, it could disrupt the contract's intended functionality.

  5. Same for _updateWeightRunner which is used to update the weight of the pool.

  6. Fee per LP (Liquidity Provider) could be set to zero or to some other undesired hostile value.

Tools Used

  1. Foundry (Framework)

  2. Chisel

  3. Forge test

  4. Mutational Testing

Recommendation

  1. Add a modifier to check the validity of the version parameter.

  2. Add value validation for _upliftFeeBps and _minWithdrawalFeeBps parameters.

  3. Add address validation for _updateWeightRunner parameter.

One possible solution could be the following:

+ uint256 constant MAX_FEE_BPS = 10000; // 100%
+ uint256 constant MIN_FEE_BPS = 10; // 10%
+ uint256 constant MAX_WITHDRAWAL_FEE_BPS = 10000; // 100%
+ uint256 constant MIN_WITHDRAWAL_FEE_BPS = 10; // 10%
+ modifier validVersion(string memory _version) {
+ require(bytes(_version).length > 0, "VERSIONREQ");
+ _;
+ }
constructor(
IVault vault,
IWETH weth,
IPermit2 permit2,
uint64 _upliftFeeBps,
uint64 _minWithdrawalFeeBps,
address _updateWeightRunnerParam,
string memory version,
string memory name,
string memory symbol
- ) MinimalRouter(vault, weth, permit2, version) Ownable(msg.sender) {
+ ) validVersion(version) MinimalRouter(vault, weth, permit2, version) Ownable(msg.sender) {
require(bytes(name).length > 0 && bytes(symbol).length > 0, "NAMEREQ"); //Must provide a name / symbol
+ require(_updateWeightRunnerParam != address(0), "INVALID_UPDATE_WEIGHT_RUNNER");
+ require(_upliftFeeBps >= MIN_FEE_BPS && _upliftFeeBps <= MAX_FEE_BPS, "INVALID_FEE_BPS");
+ require(_minWithdrawalFeeBps >= MIN_WITHDRAWAL_FEE_BPS && _minWithdrawalFeeBps <= MAX_WITHDRAWAL_FEE_BPS, "INVALID_WITHDRAWAL_FEE_BPS");
lpNFT = new LPNFT(name, symbol, address(this));
upliftFeeBps = _upliftFeeBps;
minWithdrawalFeeBps = _minWithdrawalFeeBps;
_updateWeightRunner = _updateWeightRunnerParam;
}

The above solution adds a modifier validVersion to check the validity of the version parameter. It also adds value validation for _upliftFeeBps and _minWithdrawalFeeBps parameters. Moreover, it adds address validation for _updateWeightRunner parameter. Therefore, enhancing and reducing the security and common pitfalls of the contract.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!