LoopFi - oualidpro's results

A dedicated lending market for Ethereum carry trades. Users can supply a long tail of Liquid Restaking Tokens (LRT) and their derivatives as collateral to borrow ETH for increased yield exposure.

General Information

Platform: Code4rena

Start Date: 01/05/2024

Pot Size: $12,100 USDC

Total HM: 1

Participants: 47

Period: 7 days

Judge: Koolex

Id: 371

League: ETH

LoopFi

Findings Distribution

Researcher Performance

Rank: 45/47

Findings: 1

Award: $0.00

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

0 USDC - $0.00

Labels

bug
grade-b
QA (Quality Assurance)
sponsor acknowledged
sufficient quality report
Q-13

External Links

Summary

Low Risk Issues

IssueInstances
[L‑1]Missing checks for address(0) when assigning values to address state variables2
[L‑2]increase/decrease or forceApprove allowance should be used instead of approve2
[L‑3]Governance operations should be behind a timelock4
[L‑4]Setters should have initial value check3
[L‑5]Solidity version 0.8.20 may not work on other chains due to PUSH01
[L‑6]Int casting block.timestamp can reduce the lifespan of a contract2
[L‑7]Unused/empty receive()/fallback() function1
[L‑8]Consider implementing two-step procedure for updating protocol addresses1
[L‑9]Privileged functions can create points of failure5
[L‑10]Functions calling contracts/addresses with transfer hooks are missing reentrancy guards1
[L‑11]Code does not follow the best practice of check-effects-interaction3
[L‑12]prevent re-setting a state variable with the same value3
[L‑13]Empty receive functions can cause gas issues1

Total: 29 instances over 13 issues

Non-critical Issues

IssueInstances
[NC‑1]Assembly blocks should have extensive comments2
[NC‑2]Constants in comparisons should appear on the left side12
[NC‑3][NatSpec] Natspec description is missing from contract46
[NC‑4]Contract does not follow the Solidity style guide's suggested layout ordering1
[NC‑5]Custom error has no error details14
[NC‑6]Consider using delete rather than assigning zero to clear values2
[NC‑7]Empty bytes check is missing12
[NC‑8]Events are missing sender information6
[NC‑9]Events may be emitted out of order due to reentrancy4
[NC‑10]Defining All External/Public Functions in Contract Interfaces12
[NC‑11]Function ordering does not follow the Solidity style guide1
[NC‑12]addresss shouldn't be hard-coded1
[NC‑13]Imports could be organized more systematically1
[NC‑14]Import declarations should import specific identifiers, rather than the whole file2
[NC‑15]Inconsistent usage of require/error1
[NC‑16]Large numeric literals should use underscores for readability1
[NC‑17]Long lines of code3
[NC‑18]Missing event and or timelock for critical parameter change1
[NC‑19]Consider using named mappings3
[NC‑20]Events that mark critical parameter changes should contain both the old and the new value2
[NC‑21]require() / revert() statements should have descriptive reason strings1
[NC‑22]Setters should prevent re-setting of the same value2
[NC‑23]NatSpec @return argument is missing3
[NC‑24]Consider using SafeTransferLib.safeTransferETH() or Address.sendValue() for clearer semantic meaning2
[NC‑25]Empty receive()/payable fallback() function does not authorize requests1
[NC‑26]State variables should have Natspec comments16
[NC‑27]Contracts should have full test coverage1
[NC‑28]Top level pragma declarations should be separated by two blank lines1
[NC‑29]Critical functions should be a two step procedure4
[NC‑30]Event is missing indexed fields9
[NC‑31]Missing upgradability functionality1
[NC‑32]Constants should be defined rather than using magic numbers3
[NC‑33]Use a single file for system wide constants1
[NC‑34]Consider using SMTChecker1
[NC‑35]Complex function controle flow2
[NC‑36]Consider bounding input array length1
[NC‑37][NatSpec] Natspec @dev is missing from contract41
[NC‑38]Contract should expose an interface12
[NC‑39][NatSpec] Natspec @notice is missing from contract42
[NC‑40]Contract uses both require()/revert() as well as custom errors1
[NC‑41]immutable variable names don't follow the Solidity style guide1
[NC‑42]Use the latest Solidity version for better security1
[NC‑43]Consider adding formal verification proofs1
[NC‑44]Missing zero address check in functions with address parameters2
[NC‑45]Use a struct to encapsulate multiple function parameters1
[NC‑46]Multiple mappings with same keys can be combined into a single struct mapping for readability2
[NC‑47]constructor should emit an event1
[NC‑48]Complex functions should include comments2
[NC‑49]Make use of Solidiy's using keyword1
[NC‑50][Solidity]: All verbatim blocks are considered identical by deduplicator and can incorrectly be unified1
[NC‑51][NatSpec] Natspec @param is missing from error17
[NC‑52]Not using the latest version of prb-math from dependencies1
[NC‑53]Enforcing Lowercase and Underscores Only in the name Field of package.json1
[NC‑54]Using Low-Level Call for Transfers2
[NC‑55]Off-by-one timestamp error5
[NC‑56]Incorrect withdraw declaration1
[NC‑57]A event should be emitted if a non immutable state variable is set in a constructor5
[NC‑58]Returning a struct instead of returning many variables is better1
[NC‑59][Solidity]: Order of Argument Evaluation Disrupted in Non-Expression-Split Code by Optimizer Sequences with FullInliner1
[NC‑60]Consider adding validation of user inputs10
[NC‑61]Consider using named function calls8

Total: 337 instances over 61 issues

Low Risk Issues

[L‑1] Missing checks for address(0) when assigning values to address state variables

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

99:         exchangeProxy = _exchangeProxy;

337:         owner = _owner;

link , link

</details>

[L‑2] increase/decrease or forceApprove allowance should be used instead of approve

In order to prevent front running, increase/decrease or forceApprove allowance should be used in place of approve where possible

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

231:         lpETH.approve(address(lpETHVault), claimedAmount);

495:         require(_sellToken.approve(exchangeProxy, _amount));

link , link

</details>

[L‑3] Governance operations should be behind a timelock

All critical and governance operations should be protected by a timelock. For example from the point of view of a user, the changing of the owner of a contract is a high risk operation that may have outcomes ranging from an attacker gaining control over the protocol, to the function no longer functioning due to a typo in the destination address. To give users plenty of warning so that they can validate any ownership changes, changes of ownership should be behind a timelock.

There are 4 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

336:     function setOwner(address _owner) external onlyAuthorized {
337:         owner = _owner;
338: 
339:         emit OwnerUpdated(_owner);
340:     }

364:     function allowToken(address _token) external onlyAuthorized {
365:         isTokenAllowed[_token] = true;
366:     }

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {
373:         emergencyMode = _mode;
374:     }

379:     function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyAuthorized {
380:         if (tokenAddress == address(lpETH) || isTokenAllowed[tokenAddress]) {
381:             revert NotValidToken();
382:         }
383:         IERC20(tokenAddress).safeTransfer(owner, tokenAmount);
384: 
385:         emit Recovered(tokenAddress, tokenAmount);
386:     }

link , link, link, link

</details>

[L‑4] Setters should have initial value check

Setters should have initial value check to prevent assigning wrong value to the variable. Assginment of wrong value can lead to unexpected behavior of the contract.

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

336:     function setOwner(address _owner) external onlyAuthorized {

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {

link , link, link

</details>

[L‑5] Solidity version 0.8.20 may not work on other chains due to PUSH0

The compiler for Solidity 0.8.20 switches the default target EVM version to Shanghai, which includes the new PUSH0 op code. This op code may not yet be implemented on all L2s, so deployment on these chains will fail. To work around this issue, use an earlier EVM version

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

2: pragma solidity 0.8.20;

link

</details>

[L‑6] Int casting block.timestamp can reduce the lifespan of a contract

Consider removing casting to ensure future functionality.

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

327:         startClaimDate = uint32(block.timestamp);

355:         loopActivation = uint32(block.timestamp);

link , link

</details>

[L‑7] Unused/empty receive()/fallback() function

If the intention is for the Ether to be used, the function should call another function or emit an event, otherwise it should revert.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

392:     receive() external payable {}
393: 

link

</details>

[L‑8] Consider implementing two-step procedure for updating protocol addresses

A copy-paste error or a typo may end up bricking protocol functionality, or sending tokens to an address with no known private key. Consider implementing a two-step procedure for updating protocol addresses, where the recipient is set as pending, and must 'accept' the assignment by making an affirmative call. A straight forward way of doing this would be to have the target contracts implement EIP-165, and to have the 'set' functions ensure that the recipient is of the right interface type.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

336:     function setOwner(address _owner) external onlyAuthorized {
337:         owner = _owner;
338: 
339:         emit OwnerUpdated(_owner);
340:     }

link

</details>

[L‑9] Privileged functions can create points of failure

Ensure such accounts are protected and consider implementing multi sig to prevent a single point of failure

There are 5 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

336:     function setOwner(address _owner) external onlyAuthorized {

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)
349:         external
350:         onlyAuthorized

364:     function allowToken(address _token) external onlyAuthorized {

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {

379:     function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyAuthorized {

link , link, link, link, link

</details>

[L‑10] Functions calling contracts/addresses with transfer hooks are missing reentrancy guards

Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks will open the users of this protocol up to read-only reentrancies with no way to protect against it, except by block-listing the whole protocol.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit function `withdraw()` is not protected against reentrancy
302:             IERC20(_token).safeTransfer(msg.sender, lockedAmount);

link

</details>

[L‑11] Code does not follow the best practice of check-effects-interaction

Code should follow the best-practice of check-effects-interaction, where state variables are updated before any external calls are made. Doing so prevents a large class of reentrancy bugs.

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit the function `withdraw()` is called before the following assignment
190:                 totalSupply = totalSupply + _amount;

//@audit the function `withdraw()` is called before the following assignment
191:                 balances[_receiver][ETH] += _amount;

//@audit the function `safeTransferFrom()` is called before the following assignment
193:                 balances[_receiver][_token] += _amount;

link , link, link

</details>

[L‑12] prevent re-setting a state variable with the same value

Not only is wasteful in terms of gas, but this is especially problematic when an event is emitted and the old and new values set are the same, as listeners might not expect this kind of scenario.

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

336:     function setOwner(address _owner) external onlyAuthorized {

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {

link , link, link

</details>

[L‑13] Empty receive functions can cause gas issues

In Solidity, functions that receive Ether without corresponding functionality to utilize or withdraw these funds can inadvertently lead to a permanent loss of value. This is because if someone sends Ether to the contract, they may be unable to retrieve it. To avoid this, functions receiving Ether should be accompanied by additional methods that process or allow the withdrawal of these funds. If the intent is to use the received Ether, it should trigger a separate function; if not, it should revert the transaction (for instance, via require(msg.sender == address(weth))). Access control checks can also prevent unintended Ether transfers, despite the slight gas cost they entail. If concerns over gas costs persist, at minimum, include a rescue function to recover unused Ether. Missteps in handling Ether in smart contracts can lead to irreversible financial losses, hence these precautions are crucial.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

392:     receive() external payable {}

link

</details>

Non-critical Issues

[NC‑1] Assembly blocks should have extensive comments

Assembly blocks are taking a lot more time to audit than normal Solidity code, and often have gotchas and side-effects that the Solidity versions of the same code do not. Consider adding more comments explaining what is being done in every step of the assembly code

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

454:         assembly {

475:         assembly {

link , link

</details>

[NC‑2] Constants in comparisons should appear on the left side

There are 12 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit `ETH`
144:         if (_token == ETH) {

//@audit `ETH`
158:         if (_token == ETH) {

//@audit `0`
176:         if (_amount == 0) {

//@audit `ETH`
179:         if (_token == ETH) {

//@audit `0`
245:         if (userStake == 0) {

//@audit `ETH`
248:         if (_token == ETH) {

//@audit `0`
287:         if (lockedAmount == 0) {

//@audit `ETH`
290:         if (_token == ETH) {

//@audit `TIMELOCK`
316:         if (block.timestamp - loopActivation <= TIMELOCK) {

//@audit `UNI_SELECTOR`
414:             if (selector != UNI_SELECTOR) {

//@audit `TRANSFORM_SELECTOR`
423:             if (selector != TRANSFORM_SELECTOR) {

//@audit `ETH`
426:             if (outputToken != ETH) {

link , link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑3] [NatSpec] Natspec description is missing from contract

It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.source

There are 46 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

25:     ILpETH public lpETH;

26:     ILpETHVault public lpETHVault;

27:     IWETH public immutable WETH;

28:     address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

29:     address public immutable exchangeProxy;

31:     address public owner;

33:     uint256 public totalSupply;

34:     uint256 public totalLpETH;

35:     mapping(address => bool) public isTokenAllowed;

42:     bytes4 public constant UNI_SELECTOR = 0x803ba26d;

43:     bytes4 public constant TRANSFORM_SELECTOR = 0x415565b0;

45:     uint32 public loopActivation;

46:     uint32 public startClaimDate;

47:     uint32 public constant TIMELOCK = 7 days;

48:     bool public emergencyMode;

50:     mapping(address => mapping(address => uint256)) public balances; // User -> Token -> Balance

56:     event Locked(address indexed user, uint256 amount, address token, bytes32 indexed referral);

57:     event StakedVault(address indexed user, uint256 amount);

58:     event Converted(uint256 amountETH, uint256 amountlpETH);

59:     event Withdrawn(address indexed user, address token, uint256 amount);

60:     event Claimed(address indexed user, address token, uint256 reward);

61:     event Recovered(address token, uint256 amount);

62:     event OwnerUpdated(address newOwner);

63:     event LoopAddressesUpdated(address loopAddress, address vaultAddress);

64:     event SwappedTokens(address sellToken, uint256 sellAmount, uint256 buyETHAmount);

70:     error InvalidToken();

71:     error NothingToClaim();

72:     error TokenNotAllowed();

73:     error CannotLockZero();

74:     error CannotWithdrawZero();

75:     error UseClaimInstead();

76:     error FailedToSendEther();

77:     error SwapCallFailed();

78:     error WrongSelector(bytes4 selector);

79:     error WrongDataTokens(address inputToken, address outputToken);

80:     error WrongDataAmount(uint256 inputTokenAmount);

81:     error WrongRecipient(address recipient);

82:     error WrongExchange();

83:     error LoopNotActivated();

84:     error NotValidToken();

85:     error NotAuthorized();

86:     error CurrentlyNotPossible();

87:     error NoLongerPossible();

511:     modifier onlyAuthorized() {

518:     modifier onlyAfterDate(uint256 limitDate) {

525:     modifier onlyBeforeDate(uint256 limitDate) {

link , link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑4] Contract does not follow the Solidity style guide's suggested layout ordering

The style guide says that, within a contract, the ordering should be 1) Type declarations, 2) State variables, 3) Events, 4) Modifiers, and 5) Functions, but the contract(s) below do not follow this ordering

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit the modifier definition is misplaced
511:     modifier onlyAuthorized() {

link

</details>

[NC‑5] Custom error has no error details

Consider adding parameters to the error to indicate which user or values caused the failure

There are 14 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

70:     error InvalidToken();

71:     error NothingToClaim();

72:     error TokenNotAllowed();

73:     error CannotLockZero();

74:     error CannotWithdrawZero();

75:     error UseClaimInstead();

76:     error FailedToSendEther();

77:     error SwapCallFailed();

82:     error WrongExchange();

83:     error LoopNotActivated();

84:     error NotValidToken();

85:     error NotAuthorized();

86:     error CurrentlyNotPossible();

87:     error NoLongerPossible();

link , link, link, link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑6] Consider using delete rather than assigning zero to clear values

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

285:         balances[msg.sender][_token] = 0;

250:             balances[msg.sender][_token] = 0;

link , link

</details>

[NC‑7] Empty bytes check is missing

When developing smart contracts in Solidity, it's crucial to validate the inputs of your functions. This includes ensuring that the bytes parameters are not empty, especially when they represent crucial data such as addresses, identifiers, or raw data that the contract needs to process. Missing empty bytes checks can lead to unexpected behaviour in your contract.For instance, certain operations might fail, produce incorrect results, or consume unnecessary gas when performed with empty bytes.Moreover, missing input validation can potentially expose your contract to malicious activity, including exploitation of unhandled edge cases. To mitigate these issues, always validate that bytes parameters are not empty when the logic of your contract requires it.

There are 12 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit  ,_referral are not checked
124:     function lockETH(bytes32 _referral) external payable {
125:         _processLock(ETH, msg.value, msg.sender, _referral);

//@audit  ,_referral are not checked
133:     function lockETHFor(address _for, bytes32 _referral) external payable {
134:         _processLock(ETH, msg.value, _for, _referral);

//@audit  ,_referral are not checked
143:     function lock(address _token, uint256 _amount, bytes32 _referral) external {
144:         if (_token == ETH) {

//@audit  ,_referral are not checked
157:     function lockFor(address _token, uint256 _amount, address _for, bytes32 _referral) external {
158:         if (_token == ETH) {

//@audit  ,_referral are not checked
172:     function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)
173:         internal
174:         onlyBeforeDate(loopActivation)
175:     {

//@audit  ,_data are not checked
211:     function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
212:         external
213:         onlyAfterDate(startClaimDate)
214:     {

//@audit  ,_data are not checked
226:     function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
227:         external
228:         onlyAfterDate(startClaimDate)
229:     {

//@audit  ,_data are not checked
240:     function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
241:         internal
242:         returns (uint256 claimedAmount)
243:     {

//@audit  ,_data are not checked
405:     function _validateData(address _token, uint256 _amount, Exchange _exchange, bytes calldata _data) internal view {
406:         address inputToken;

//@audit  ,_data are not checked
448:     function _decodeUniswapV3Data(bytes calldata _data)
449:         internal
450:         pure
451:         returns (address inputToken, address outputToken, uint256 inputTokenAmount, address recipient, bytes4 selector)
452:     {

//@audit  ,_data are not checked
470:     function _decodeTransformERC20Data(bytes calldata _data)
471:         internal
472:         pure
473:         returns (address inputToken, address outputToken, uint256 inputTokenAmount, bytes4 selector)
474:     {

//@audit  ,_swapCallData are not checked
491:     function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal {
492:         // Track our balance of the buyToken to determine how much we've bought.

link , link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑8] Events are missing sender information

When an action is triggered based on a user's action, not being able to filter based on who triggered the action makes event processing a lot more cumbersome. Including the msg.sender the events of these types of action will make events much more useful to end users, especially when msg.sender is not tx.origin.

There are 6 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

197:         emit Locked(_receiver, _amount, _token, _referral);

329:         emit Converted(totalBalance, totalLpETH);

339:         emit OwnerUpdated(_owner);

357:         emit LoopAddressesUpdated(_loopAddress, _vaultAddress);

385:         emit Recovered(tokenAddress, tokenAmount);

504:         emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);

link , link, link, link, link, link

</details>

[NC‑9] Events may be emitted out of order due to reentrancy

Ensure that events follow the best practice of check-effects-interaction, and are emitted before external calls

There are 4 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

234:         emit StakedVault(msg.sender, claimedAmount);

329:         emit Converted(totalBalance, totalLpETH);

385:         emit Recovered(tokenAddress, tokenAmount);

504:         emit SwappedTokens(address(_sellToken), _amount, boughtETHAmount);

link , link, link, link

</details>

[NC‑10] Defining All External/Public Functions in Contract Interfaces

It is preferable to have all the external and public function in an interface to make using them easier by developers. This helps ensure the whole API is extracted in a interface.

There are 12 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

124:     function lockETH(bytes32 _referral) external payable {
125:         _processLock(ETH, msg.value, msg.sender, _referral);

133:     function lockETHFor(address _for, bytes32 _referral) external payable {
134:         _processLock(ETH, msg.value, _for, _referral);

143:     function lock(address _token, uint256 _amount, bytes32 _referral) external {
144:         if (_token == ETH) {

157:     function lockFor(address _token, uint256 _amount, address _for, bytes32 _referral) external {
158:         if (_token == ETH) {

211:     function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
212:         external
213:         onlyAfterDate(startClaimDate)
214:     {

226:     function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)
227:         external
228:         onlyAfterDate(startClaimDate)
229:     {

315:     function convertAllETH() external onlyAuthorized onlyBeforeDate(startClaimDate) {
316:         if (block.timestamp - loopActivation <= TIMELOCK) {

336:     function setOwner(address _owner) external onlyAuthorized {
337:         owner = _owner;

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)
349:         external
350:         onlyAuthorized
351:         onlyBeforeDate(loopActivation)
352:     {

364:     function allowToken(address _token) external onlyAuthorized {
365:         isTokenAllowed[_token] = true;

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {
373:         emergencyMode = _mode;

379:     function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyAuthorized {
380:         if (tokenAddress == address(lpETH) || isTokenAllowed[tokenAddress]) {

link , link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑11] Function ordering does not follow the Solidity style guide

According to the Solidity style guide, functions should be laid out in the following order :constructor(), receive(), fallback(), external, public, internal, private, but the cases below do not follow this pattern

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

211:     function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)

link

</details>

[NC‑12] addresss shouldn't be hard-coded

It is often better to declare addresses as immutable, and assign them via constructor arguments. This allows the code to remain the same across deployments on different networks, and avoids recompilation when addresses need to change.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit hardcoded address : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
28:     address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

link

</details>

[NC‑13] Imports could be organized more systematically

The contract used interfaces should be imported first, followed by all other files. The examples below do not follow this layout.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

7: import {ILpETH, IERC20} from "./interfaces/ILpETH.sol";

link

</details>

[NC‑14] Import declarations should import specific identifiers, rather than the whole file

Using import declarations of the form import {<identifier_name>} from "some/ file.sol" avoids polluting the symbol namespace making flattened files smaller, and speeds up compilation

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

4: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

5: import "@openzeppelin/contracts/utils/math/Math.sol";

link , link

</details>

[NC‑15] Inconsistent usage of require/error

Some parts of the codebase use require statements, while others use custom errors. Consider refactoring the code to use the same approach: the following findings represent the minority of require vs error, and they show the first occurance in each file, for brevity.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

495:         require(_sellToken.approve(exchangeProxy, _amount));

link

</details>

[NC‑16] Large numeric literals should use underscores for readability

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

103:         startClaimDate = 4294967295; // Max uint32 ~ year 2107

link

</details>

[NC‑17] Long lines of code

Usually lines in source code are limited to 80 characters. Today's screens are much larger so it's reasonable to stretch this in some cases. The solidity style guide recommends a maximumum line length of 120 characters, so the lines below should be split when they reach that length.

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

417:             // UniswapV3Feature.sellTokenForEthToUniswapV3(encodedPath, sellAmount, minBuyAmount, recipient) requires `encodedPath` to be a Uniswap-encoded path, where the last token is WETH, and sends the NATIVE token to `recipient`

460:             encodedPathLength := calldataload(add(p, 96)) // Get length of encodedPath (obtained through abi.encodePacked)

461:             inputToken := shr(96, calldataload(add(p, 128))) // Shift to the Right with 24 zeroes (12 bytes = 96 bits) to get address

link , link, link

</details>

[NC‑18] Missing event and or timelock for critical parameter change

Events help non-contract tools to track changes, and events prevent users from being surprised by changes

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {
373:         emergencyMode = _mode;
374:     }

link

</details>

[NC‑19] Consider using named mappings

Consider using named mappings to make it easier to understand the purpose of each mapping

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

35:     mapping(address => bool) public isTokenAllowed;

50:     mapping(address => mapping(address => uint256)) public balances; // User -> Token -> Balance

50:     mapping(address => mapping(address => uint256)) public balances; // User -> Token -> Balance

link , link, link

</details>

[NC‑20] Events that mark critical parameter changes should contain both the old and the new value

This should especially be done if the new value is not required to be different from the old value

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

62:     event OwnerUpdated(address newOwner);

63:     event LoopAddressesUpdated(address loopAddress, address vaultAddress);

link , link

</details>

[NC‑21] require() / revert() statements should have descriptive reason strings

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

495:         require(_sellToken.approve(exchangeProxy, _amount));

link

</details>

[NC‑22] Setters should prevent re-setting of the same value

This especially problematic when the setter also emits the same value, which may be confusing to offline parsers

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit `owner` and `_owner` are never checked for the same value setting
336:     function setOwner(address _owner) external onlyAuthorized {
337:         owner = _owner;

//@audit `emergencyMode` and `_mode` are never checked for the same value setting
372:     function setEmergencyMode(bool _mode) external onlyAuthorized {
373:         emergencyMode = _mode;

link , link

</details>

[NC‑23] NatSpec @return argument is missing

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

// @audit the @return is missing
 @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.

// @audit the @return is missing
 @notice Decodes the data sent from 0x API when UniswapV3 is used
 @param _data      swap data from 0x API

// @audit the @return is missing
 @notice Decodes the data sent from 0x API when other exchanges are used via 0x TransformERC20 function
 @param _data      swap data from 0x API

link , link, link

</details>

[NC‑24] Consider using SafeTransferLib.safeTransferETH() or Address.sendValue() for clearer semantic meaning

These Functions indicate their purpose with their name more clearly than using low-level calls.

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

497:         (bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);

296:             (bool sent,) = msg.sender.call{value: lockedAmount}("");

link , link

</details>

[NC‑25] Empty receive()/payable fallback() function does not authorize requests

If the intention is for the Ether to be used, the function should call another function, otherwise it should revert (e.g. require(msg.sender == address(weth))). Having no access control on the function means that someone may send Ether to the contract, and have no way to get anything back out, which is a loss of funds. If the concern is having to spend a small amount of gas to check the sender against an immutable address, the code should at least have a function to rescue unused Ether.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

392:     receive() external payable {}
393: 

link

</details>

[NC‑26] State variables should have Natspec comments

Consider adding some Natspec comments on critical state variables to explain what they are supposed to do: this will help for future code reviews.

There are 16 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit lpETH need comments
25:     ILpETH public lpETH;

//@audit lpETHVault need comments
26:     ILpETHVault public lpETHVault;

//@audit WETH need comments
27:     IWETH public immutable WETH;

//@audit ETH need comments
28:     address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

//@audit exchangeProxy need comments
29:     address public immutable exchangeProxy;

//@audit owner need comments
31:     address public owner;

//@audit totalSupply need comments
33:     uint256 public totalSupply;

//@audit totalLpETH need comments
34:     uint256 public totalLpETH;

//@audit isTokenAllowed need comments
35:     mapping(address => bool) public isTokenAllowed;

//@audit UNI_SELECTOR need comments
42:     bytes4 public constant UNI_SELECTOR = 0x803ba26d;

//@audit TRANSFORM_SELECTOR need comments
43:     bytes4 public constant TRANSFORM_SELECTOR = 0x415565b0;

//@audit loopActivation need comments
45:     uint32 public loopActivation;

//@audit startClaimDate need comments
46:     uint32 public startClaimDate;

//@audit TIMELOCK need comments
47:     uint32 public constant TIMELOCK = 7 days;

//@audit emergencyMode need comments
48:     bool public emergencyMode;

//@audit balances need comments
50:     mapping(address => mapping(address => uint256)) public balances; // User -> Token -> Balance

link , link, link, link, link, link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑27] Contracts should have full test coverage

While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

@audit Multiple files
1: 

link

</details>

[NC‑28] Top level pragma declarations should be separated by two blank lines

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

2: pragma solidity 0.8.20;
3: 
4: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

link

</details>

[NC‑29] Critical functions should be a two step procedure

Critical functions in Solidity contracts should follow a two-step procedure to enhance security, minimize human error, and ensure proper access control. By dividing sensitive operations into distinct phases, such as initiation and confirmation, developers can introduce a safeguard against unintended actions or unauthorized access.

There are 4 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

336:     function setOwner(address _owner) external onlyAuthorized {

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {

link , link, link, link

</details>

[NC‑30] Event is missing indexed fields

Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.

There are 9 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

56:     event Locked(address indexed user, uint256 amount, address token, bytes32 indexed referral);

57:     event StakedVault(address indexed user, uint256 amount);

58:     event Converted(uint256 amountETH, uint256 amountlpETH);

59:     event Withdrawn(address indexed user, address token, uint256 amount);

60:     event Claimed(address indexed user, address token, uint256 reward);

61:     event Recovered(address token, uint256 amount);

62:     event OwnerUpdated(address newOwner);

63:     event LoopAddressesUpdated(address loopAddress, address vaultAddress);

64:     event SwappedTokens(address sellToken, uint256 sellAmount, uint256 buyETHAmount);

link , link, link, link, link, link, link, link, link

</details>

[NC‑31] Missing upgradability functionality

At the begining of a project, there is always the need to modify of add something to the source code especialy if any vulnerability is discovered. Therefore, having such system is crusial at least at the first stages of the project

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

16: contract PrelaunchPoints {

link

</details>

[NC‑32] Constants should be defined rather than using magic numbers

Even assembly can benefit from using readable constants instead of hex/numeric literals

There are 3 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit Try to make a `constant` with `4294967295` value
103:         startClaimDate = 4294967295; // Max uint32 ~ year 2107

//@audit Try to make a `constant` with `120` value
102:         loopActivation = uint32(block.timestamp + 120 days);

//@audit Try to make a `constant` with `100` value
253:             uint256 userClaim = userStake * _percentage / 100;

link , link, link

</details>

[NC‑33] Use a single file for system wide constants

Consider grouping all the system constants under a single file. This finding shows only the first constant for each file.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

28:     address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

link

</details>

[NC‑34] Consider using SMTChecker

The SMTChecker is a valuable tool for Solidity developers as it helps detect potential vulnerabilities and logical errors in the contract's code. By utilizing Satisfiability Modulo Theories (SMT) solvers, it can reason about the potential states a contract can be in, and therefore, identify conditions that could lead to undesirable behavior. This automatic formal verification can catch issues that might otherwise be missed in manual code reviews or standard testing, enhancing the overall contract's security and reliability.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

2: pragma solidity 0.8.20;

link

</details>

[NC‑35] Complex function controle flow

Due to multiple if, loop and conditions the following functions has a very complex controle flow that could make auditing very difficult to cover all possible path\nTherefore, consider breaking down these blocks into more manageable units, by splitting things into utility functions, by reducing nesting, and by using early returns

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

274:     function withdraw(address _token) external {
275:         if (!emergencyMode) {
276:             if (block.timestamp <= loopActivation) {
277:                 revert CurrentlyNotPossible();
278:             }
279:             if (block.timestamp >= startClaimDate) {
280:                 revert NoLongerPossible();
281:             }
282:         }
283: 
284:         uint256 lockedAmount = balances[msg.sender][_token];
285:         balances[msg.sender][_token] = 0;
286: 
287:         if (lockedAmount == 0) {
288:             revert CannotWithdrawZero();
289:         }
290:         if (_token == ETH) {
291:             if (block.timestamp >= startClaimDate){
292:                 revert UseClaimInstead();
293:             }
294:             totalSupply = totalSupply - lockedAmount;
295: 
296:             (bool sent,) = msg.sender.call{value: lockedAmount}("");
297: 
298:             if (!sent) {
299:                 revert FailedToSendEther();
300:             }
301:         } else {
302:             IERC20(_token).safeTransfer(msg.sender, lockedAmount);
303:         }
304: 
305:         emit Withdrawn(msg.sender, _token, lockedAmount);
306:     }

405:     function _validateData(address _token, uint256 _amount, Exchange _exchange, bytes calldata _data) internal view {
406:         address inputToken;
407:         address outputToken;
408:         uint256 inputTokenAmount;
409:         address recipient;
410:         bytes4 selector;
411: 
412:         if (_exchange == Exchange.UniswapV3) {
413:             (inputToken, outputToken, inputTokenAmount, recipient, selector) = _decodeUniswapV3Data(_data);
414:             if (selector != UNI_SELECTOR) {
415:                 revert WrongSelector(selector);
416:             }
417:             // UniswapV3Feature.sellTokenForEthToUniswapV3(encodedPath, sellAmount, minBuyAmount, recipient) requires `encodedPath` to be a Uniswap-encoded path, where the last token is WETH, and sends the NATIVE token to `recipient`
418:             if (outputToken != address(WETH)) {
419:                 revert WrongDataTokens(inputToken, outputToken);
420:             }
421:         } else if (_exchange == Exchange.TransformERC20) {
422:             (inputToken, outputToken, inputTokenAmount, selector) = _decodeTransformERC20Data(_data);
423:             if (selector != TRANSFORM_SELECTOR) {
424:                 revert WrongSelector(selector);
425:             }
426:             if (outputToken != ETH) {
427:                 revert WrongDataTokens(inputToken, outputToken);
428:             }
429:         } else {
430:             revert WrongExchange();
431:         }
432: 
433:         if (inputToken != _token) {
434:             revert WrongDataTokens(inputToken, outputToken);
435:         }
436:         if (inputTokenAmount != _amount) {
437:             revert WrongDataAmount(inputTokenAmount);
438:         }
439:         if (recipient != address(this) && recipient != address(0)) {
440:             revert WrongRecipient(recipient);
441:         }
442:     }

link , link

</details>

[NC‑36] Consider bounding input array length

The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require() that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit array name _allowedTokens
097:     constructor(address _exchangeProxy, address _wethAddress, address[] memory _allowedTokens) {
098:         owner = msg.sender;
099:         exchangeProxy = _exchangeProxy;
100:         WETH = IWETH(_wethAddress);
101: 
102:         loopActivation = uint32(block.timestamp + 120 days);
103:         startClaimDate = 4294967295; // Max uint32 ~ year 2107
104: 
105:         // Allow intital list of tokens
106:         uint256 length = _allowedTokens.length;
107:         for (uint256 i = 0; i < length;) {
108:             isTokenAllowed[_allowedTokens[i]] = true;
109:             unchecked {
110:                 i++;
111:             }
112:         }
113:         isTokenAllowed[_wethAddress] = true;
114:     }

link

</details>

[NC‑37] [NatSpec] Natspec @dev is missing from contract

There are 41 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

11: /**
12:  * @title   PrelaunchPoints
13:  * @author  Loop
14:  * @notice  Staking points contract for the prelaunch of Loop Protocol.
15:  */

56:     event Locked(address indexed user, uint256 amount, address token, bytes32 indexed referral);

57:     event StakedVault(address indexed user, uint256 amount);

58:     event Converted(uint256 amountETH, uint256 amountlpETH);

59:     event Withdrawn(address indexed user, address token, uint256 amount);

60:     event Claimed(address indexed user, address token, uint256 reward);

61:     event Recovered(address token, uint256 amount);

62:     event OwnerUpdated(address newOwner);

63:     event LoopAddressesUpdated(address loopAddress, address vaultAddress);

64:     event SwappedTokens(address sellToken, uint256 sellAmount, uint256 buyETHAmount);

70:     error InvalidToken();

71:     error NothingToClaim();

72:     error TokenNotAllowed();

73:     error CannotLockZero();

74:     error CannotWithdrawZero();

75:     error UseClaimInstead();

76:     error FailedToSendEther();

77:     error SwapCallFailed();

78:     error WrongSelector(bytes4 selector);

79:     error WrongDataTokens(address inputToken, address outputToken);

80:     error WrongDataAmount(uint256 inputTokenAmount);

81:     error WrongRecipient(address recipient);

82:     error WrongExchange();

83:     error LoopNotActivated();

84:     error NotValidToken();

85:     error NotAuthorized();

86:     error CurrentlyNotPossible();

87:     error NoLongerPossible();

92:     /**
93:      * @param _exchangeProxy address of the 0x protocol exchange proxy
94:      * @param _wethAddress   address of WETH
95:      * @param _allowedTokens list of token addresses to allow for locking
96:      */

120:     /**
121:      * @notice Locks ETH
122:      * @param _referral  info of the referral. This value will be processed in the backend.
123:      */

128:     /**
129:      * @notice Locks ETH for a given address
130:      * @param _for       address for which ETH is locked
131:      * @param _referral  info of the referral. This value will be processed in the backend.
132:      */

137:     /**
138:      * @notice Locks a valid token
139:      * @param _token     address of token to lock
140:      * @param _amount    amount of token to lock
141:      * @param _referral  info of the referral. This value will be processed in the backend.
142:      */

150:     /**
151:      * @notice Locks a valid token for a given address
152:      * @param _token     address of token to lock
153:      * @param _amount    amount of token to lock
154:      * @param _for       address for which ETH is locked
155:      * @param _referral  info of the referral. This value will be processed in the backend.
156:      */

332:     /**
333:      * @notice Sets a new owner
334:      * @param _owner address of the new owner
335:      */

398:     /**
399:      * @notice Validates the data sent from 0x API to match desired behaviour
400:      * @param _token     address of the token to sell
401:      * @param _amount    amount of token to sell
402:      * @param _exchange  exchange identifier where the swap takes place
403:      * @param _data      swap data from 0x API
404:      */

444:     /**
445:      * @notice Decodes the data sent from 0x API when UniswapV3 is used
446:      * @param _data      swap data from 0x API
447:      */

466:     /**
467:      * @notice Decodes the data sent from 0x API when other exchanges are used via 0x TransformERC20 function
468:      * @param _data      swap data from 0x API
469:      */

484:     /**
485:      *
486:      * @param _sellToken     The `sellTokenAddress` field from the API response.
487:      * @param _amount       The `sellAmount` field from the API response.
488:      * @param _swapCallData  The `data` field from the API response.
489:      */

511:     modifier onlyAuthorized() {

518:     modifier onlyAfterDate(uint256 limitDate) {

525:     modifier onlyBeforeDate(uint256 limitDate) {

link , link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑38] Contract should expose an interface

The contracts should expose an interface so that other projects can more easily integrate with it, without having to develop their own non-standard variants.

There are 12 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

124:     function lockETH(bytes32 _referral) external payable {

133:     function lockETHFor(address _for, bytes32 _referral) external payable {

143:     function lock(address _token, uint256 _amount, bytes32 _referral) external {

157:     function lockFor(address _token, uint256 _amount, address _for, bytes32 _referral) external {

211:     function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)

226:     function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)

315:     function convertAllETH() external onlyAuthorized onlyBeforeDate(startClaimDate) {

336:     function setOwner(address _owner) external onlyAuthorized {

348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)

364:     function allowToken(address _token) external onlyAuthorized {

372:     function setEmergencyMode(bool _mode) external onlyAuthorized {

379:     function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyAuthorized {

link , link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑39] [NatSpec] Natspec @notice is missing from contract

There are 42 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

56:     event Locked(address indexed user, uint256 amount, address token, bytes32 indexed referral);

57:     event StakedVault(address indexed user, uint256 amount);

58:     event Converted(uint256 amountETH, uint256 amountlpETH);

59:     event Withdrawn(address indexed user, address token, uint256 amount);

60:     event Claimed(address indexed user, address token, uint256 reward);

61:     event Recovered(address token, uint256 amount);

62:     event OwnerUpdated(address newOwner);

63:     event LoopAddressesUpdated(address loopAddress, address vaultAddress);

64:     event SwappedTokens(address sellToken, uint256 sellAmount, uint256 buyETHAmount);

70:     error InvalidToken();

71:     error NothingToClaim();

72:     error TokenNotAllowed();

73:     error CannotLockZero();

74:     error CannotWithdrawZero();

75:     error UseClaimInstead();

76:     error FailedToSendEther();

77:     error SwapCallFailed();

78:     error WrongSelector(bytes4 selector);

79:     error WrongDataTokens(address inputToken, address outputToken);

80:     error WrongDataAmount(uint256 inputTokenAmount);

81:     error WrongRecipient(address recipient);

82:     error WrongExchange();

83:     error LoopNotActivated();

84:     error NotValidToken();

85:     error NotAuthorized();

86:     error CurrentlyNotPossible();

87:     error NoLongerPossible();

92:     /**
93:      * @param _exchangeProxy address of the 0x protocol exchange proxy
94:      * @param _wethAddress   address of WETH
95:      * @param _allowedTokens list of token addresses to allow for locking
96:      */

164:     /**
165:      * @dev Generic internal locking function that updates rewards based on
166:      *      previous balances, then update balances.
167:      * @param _token       Address of the token to lock
168:      * @param _amount      Units of ETH or token to add to the users balance
169:      * @param _receiver    Address of user who will receive the stake
170:      * @param _referral    Address of the referral user
171:      */

204:     /**
205:      * @dev Called by a user to get their vested lpETH
206:      * @param _token      Address of the token to convert to lpETH
207:      * @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
208:      * @param _exchange   Exchange identifier where the swap takes place
209:      * @param _data       Swap data obtained from 0x API
210:      */

218:     /**
219:      * @dev Called by a user to get their vested lpETH and stake them in a
220:      *      Loop vault for extra rewards
221:      * @param _token      Address of the token to convert to lpETH
222:      * @param _percentage Proportion in % of tokens to withdraw. NOT useful for ETH
223:      * @param _exchange   Exchange identifier where the swap takes place
224:      * @param _data       Swap data obtained from 0x API
225:      */

237:     /**
238:      * @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
239:      */

268:     /**
269:      * @dev Called by a staker to withdraw all their ETH or LRT
270:      * Note Can only be called after the loop address is set and before claiming lpETH,
271:      * i.e. for at least TIMELOCK. In emergency mode can be called at any time.
272:      * @param _token      Address of the token to withdraw
273:      */

312:     /**
313:      * @dev Called by a owner to convert all the locked ETH to get lpETH
314:      */

360:     /**
361:      * @param _token address of a wrapped LRT token
362:      * @dev ONLY add wrapped LRT tokens. Contract not compatible with rebase tokens.
363:      */

368:     /**
369:      * @param _mode boolean to activate/deactivate the emergency mode
370:      * @dev On emergency mode all withdrawals are accepted at
371:      */

376:     /**
377:      * @dev Allows the owner to recover other ERC20s mistakingly sent to this contract
378:      */

388:     /**
389:      * Enable receive ETH
390:      * @dev ETH sent to this contract directly will be locked forever.
391:      */

484:     /**
485:      *
486:      * @param _sellToken     The `sellTokenAddress` field from the API response.
487:      * @param _amount       The `sellAmount` field from the API response.
488:      * @param _swapCallData  The `data` field from the API response.
489:      */

511:     modifier onlyAuthorized() {

518:     modifier onlyAfterDate(uint256 limitDate) {

525:     modifier onlyBeforeDate(uint256 limitDate) {

link , link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑40] Contract uses both require()/revert() as well as custom errors

Consider using just one method in a single file. The below instances represents the less used technique

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

495:         require(_sellToken.approve(exchangeProxy, _amount));

link

</details>

[NC‑41] immutable variable names don't follow the Solidity style guide

For immutable variable names, each word should use all capital letters, with underscores separating each word (CONSTANT_CASE)

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

29:     address public immutable exchangeProxy;

link

</details>

[NC‑42] Use the latest Solidity version for better security

Using the latest solidity version will help avoid old compiler related vulnerabilities

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

2: pragma solidity 0.8.20;

link

</details>

[NC‑43] Consider adding formal verification proofs

Consider using formal verification to mathematically prove that your code does what is intended, and does not have any edge cases with unexpected behavior. The solidity compiler itself has this functionality built in

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

@audit Should implement invariant tests
1: 

link

</details>

[NC‑44] Missing zero address check in functions with address parameters

Adding a zero address check for each address type parameter can prevent errors.

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit _exchangeProxy, _allowedTokens,  are not checked
97:     constructor(address _exchangeProxy, address _wethAddress, address[] memory _allowedTokens) {

//@audit _token,  are not checked
364:     function allowToken(address _token) external onlyAuthorized {

link , link

</details>

[NC‑45] Use a struct to encapsulate multiple function parameters

If a function has too many parameters, replacing them with a struct can improve code readability and maintainability, increase reusability, and reduce the likelihood of errors when passing the parameters.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

240:     function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)
241:         internal
242:         returns (uint256 claimedAmount)
243:     {
244:         uint256 userStake = balances[msg.sender][_token];
245:         if (userStake == 0) {
246:             revert NothingToClaim();
247:         }
248:         if (_token == ETH) {
249:             claimedAmount = userStake.mulDiv(totalLpETH, totalSupply);
250:             balances[msg.sender][_token] = 0;
251:             lpETH.safeTransfer(_receiver, claimedAmount);
252:         } else {
253:             uint256 userClaim = userStake * _percentage / 100;
254:             _validateData(_token, userClaim, _exchange, _data);
255:             balances[msg.sender][_token] = userStake - userClaim;
256: 
257:             // At this point there should not be any ETH in the contract
258:             // Swap token to ETH
259:             _fillQuote(IERC20(_token), userClaim, _data);
260: 
261:             // Convert swapped ETH to lpETH (1 to 1 conversion)
262:             claimedAmount = address(this).balance;
263:             lpETH.deposit{value: claimedAmount}(_receiver);
264:         }
265:         emit Claimed(msg.sender, _token, claimedAmount);
266:     }

link

</details>

[NC‑46] Multiple mappings with same keys can be combined into a single struct mapping for readability

Well-organized data structures make code reviews easier, which may lead to fewer bugs. Consider combining related mappings into mappings to structs, so it's clear what data is related.

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

35:     mapping(address => bool) public isTokenAllowed;

50:     mapping(address => mapping(address => uint256)) public balances; // User -> Token -> Balance

link , link

</details>

[NC‑47] constructor should emit an event

Use events to signal significant changes to off-chain monitoring tools.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

097:     constructor(address _exchangeProxy, address _wethAddress, address[] memory _allowedTokens) {
098:         owner = msg.sender;
099:         exchangeProxy = _exchangeProxy;
100:         WETH = IWETH(_wethAddress);
101: 
102:         loopActivation = uint32(block.timestamp + 120 days);
103:         startClaimDate = 4294967295; // Max uint32 ~ year 2107
104: 
105:         // Allow intital list of tokens
106:         uint256 length = _allowedTokens.length;
107:         for (uint256 i = 0; i < length;) {
108:             isTokenAllowed[_allowedTokens[i]] = true;
109:             unchecked {
110:                 i++;
111:             }
112:         }
113:         isTokenAllowed[_wethAddress] = true;
114:     }

link

</details>

[NC‑48] Complex functions should include comments

Large and/or complex functions should include comments to make them easier to understand and reduce margin for error.

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

172:     function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)

274:     function withdraw(address _token) external {

link , link

</details>

[NC‑49] Make use of Solidiy's using keyword

The directive using A for B can be used to attach functions (A) as operators to user-defined value types or as member functions to any type (B). The member functions receive the object they are called on as their first parameter (like the self variable in Python). The operator functions receive operands as parameters. Doing so improves readability, makes debugging easier, and promotes modularity and reusability in the code.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

189:                 WETH.withdraw(_amount);

link

</details>

[NC‑50] [Solidity]: All verbatim blocks are considered identical by deduplicator and can incorrectly be unified

The block deduplicator is a step of the opcode-based optimizer which identifies equivalent assembly blocks and merges them into a single one. However, when blocks contained verbatim, their comparison was performed incorrectly, leading to the collapse of assembly blocks which are identical except for the contents of the verbatim items. Since verbatim is only available in Yul, compilation of Solidity sources is not affected. For more details check the following link

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

2: pragma solidity 0.8.20;

link

</details>

[NC‑51] [NatSpec] Natspec @param is missing from error

There are 17 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

56:     event Locked(address indexed user, uint256 amount, address token, bytes32 indexed referral);

57:     event StakedVault(address indexed user, uint256 amount);

58:     event Converted(uint256 amountETH, uint256 amountlpETH);

59:     event Withdrawn(address indexed user, address token, uint256 amount);

60:     event Claimed(address indexed user, address token, uint256 reward);

61:     event Recovered(address token, uint256 amount);

62:     event OwnerUpdated(address newOwner);

63:     event LoopAddressesUpdated(address loopAddress, address vaultAddress);

64:     event SwappedTokens(address sellToken, uint256 sellAmount, uint256 buyETHAmount);

78:     error WrongSelector(bytes4 selector);

79:     error WrongDataTokens(address inputToken, address outputToken);

80:     error WrongDataAmount(uint256 inputTokenAmount);

81:     error WrongRecipient(address recipient);

237:     /**
238:      * @dev Claim logic. If necessary converts token to ETH before depositing into lpETH contract.
239:      */

376:     /**
377:      * @dev Allows the owner to recover other ERC20s mistakingly sent to this contract
378:      */

518:     modifier onlyAfterDate(uint256 limitDate) {

525:     modifier onlyBeforeDate(uint256 limitDate) {

link , link, link, link, link, link, link, link, link, link, link, link, link, link, link, link, link

</details>

[NC‑52] Not using the latest version of prb-math from dependencies

prb-math is an important mathematical library The package.json configuration file says that the project is using an old version of prb-math.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: package.json

//@audit `prb-math` version is 
1: 

link

</details>

[NC‑53] Enforcing Lowercase and Underscores Only in the name Field of package.json

package.json name variable should only consist of lowercase letters and underscores

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: package.json

//@audit package.json name is loop-prelaunch-contracts
1: {

link

</details>

[NC‑54] Using Low-Level Call for Transfers

Utilizing low-level calls like .call{value: value} for Ether transfers in Ethereum can be risky, as it can inadvertently allow malicious contract executions through fallback functions. To mitigate these risks and ensure safer Ether transfers, it is recommended to adopt more secure and explicit methods provided by reputable libraries such as OpenZeppelin. Functions like Address.sendValue() from OpenZeppelin provide a clearer and safer alternative for sending Ether, as they encapsulate necessary checks and error handling, ensuring that Ether is transferred securely and any errors are appropriately dealt with. This not only enhances the security of your smart contract but also improves code readability and maintainability, aligning with modern Solidity development practices.

There are 2 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

497:         (bool success,) = payable(exchangeProxy).call{value: 0}(_swapCallData);

296:             (bool sent,) = msg.sender.call{value: lockedAmount}("");

link , link

</details>

[NC‑55] Off-by-one timestamp error

In Solidity, using >= or <= to compare against block.timestamp (alias now) may introduce off-by-one errors due to the fact that block.timestamp is only updated once per block and its value remains constant throughout the block's execution. If an operation happens at the exact second when block.timestamp changes, it could result in unexpected behavior. To avoid this, it's safer to use strict inequality operators (> or <). For instance, if a condition should only be met after a certain time, use block.timestamp > time rather than block.timestamp >= time. This way, potential off-by-one errors due to the exact timing of block mining are mitigated, leading to safer, more predictable contract behavior.

There are 5 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

519:         if (block.timestamp <= limitDate) {

526:         if (block.timestamp >= limitDate) {

276:             if (block.timestamp <= loopActivation) {

279:             if (block.timestamp >= startClaimDate) {

291:             if (block.timestamp >= startClaimDate){

link , link, link, link, link

</details>

[NC‑56] Incorrect withdraw declaration

In Solidity, it's essential for clarity and interoperability to correctly specify return types in function declarations. If the withdraw function is expected to return a bool to indicate success or failure, its omission could lead to ambiguity or unexpected behavior when interacting with or calling this function from other contracts or off-chain systems. Missing return values can mislead developers and potentially lead to contract integrations built on incorrect assumptions. To resolve this, the function declaration for withdraw should be modified to explicitly include the bool return type, ensuring clarity and correctness in contract interactions.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

274:     function withdraw(address _token) external {
275:         if (!emergencyMode) {
276:             if (block.timestamp <= loopActivation) {
277:                 revert CurrentlyNotPossible();
278:             }
279:             if (block.timestamp >= startClaimDate) {
280:                 revert NoLongerPossible();
281:             }
282:         }
283: 
284:         uint256 lockedAmount = balances[msg.sender][_token];
285:         balances[msg.sender][_token] = 0;
286: 
287:         if (lockedAmount == 0) {
288:             revert CannotWithdrawZero();
289:         }
290:         if (_token == ETH) {
291:             if (block.timestamp >= startClaimDate){
292:                 revert UseClaimInstead();
293:             }
294:             totalSupply = totalSupply - lockedAmount;
295: 
296:             (bool sent,) = msg.sender.call{value: lockedAmount}("");
297: 
298:             if (!sent) {
299:                 revert FailedToSendEther();
300:             }
301:         } else {
302:             IERC20(_token).safeTransfer(msg.sender, lockedAmount);
303:         }
304: 
305:         emit Withdrawn(msg.sender, _token, lockedAmount);
306:     }

link

</details>

[NC‑57] A event should be emitted if a non immutable state variable is set in a constructor

There are 5 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

98:         owner = msg.sender;

99:         exchangeProxy = _exchangeProxy;

100:         WETH = IWETH(_wethAddress);

102:         loopActivation = uint32(block.timestamp + 120 days);

103:         startClaimDate = 4294967295; // Max uint32 ~ year 2107

link , link, link, link, link

</details>

[NC‑58] Returning a struct instead of returning many variables is better

Returning a struct from a Solidity function instead of multiple variables offers several benefits, enhancing code clarity and efficiency. Structs allow for the grouping of related data into a single entity, making the function's return values more organized and easier to manage. This approach significantly improves readability, as it encapsulates the data logically, helping developers quickly understand the returned information's structure. Additionally, it simplifies function interfaces, reducing the potential for errors when handling multiple return values. By using structs, you can also easily extend or modify the returned data without altering the function signature, facilitating smoother updates and maintenance of your smart contract code.

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

448:     function _decodeUniswapV3Data(bytes calldata _data)
449:         internal
450:         pure
451:         returns (address inputToken, address outputToken, uint256 inputTokenAmount, address recipient, bytes4 selector)
452:     {
453:         uint256 encodedPathLength;
454:         assembly {
455:             let p := _data.offset
456:             selector := calldataload(p)
457:             p := add(p, 36) // Data: selector 4 + lenght data 32
458:             inputTokenAmount := calldataload(p)
459:             recipient := calldataload(add(p, 64))
460:             encodedPathLength := calldataload(add(p, 96)) // Get length of encodedPath (obtained through abi.encodePacked)
461:             inputToken := shr(96, calldataload(add(p, 128))) // Shift to the Right with 24 zeroes (12 bytes = 96 bits) to get address
462:             outputToken := shr(96, calldataload(add(p, add(encodedPathLength, 108)))) // Get last address of the hop
463:         }
464:     }

link

</details>

[NC‑59] [Solidity]: Order of Argument Evaluation Disrupted in Non-Expression-Split Code by Optimizer Sequences with FullInliner

Function call arguments in Yul are evaluated right to left. This order matters when the argument expressions have side-effects, and changing it may change contract behavior. FullInliner is an optimizer step that can replace a function call with the body of that function. The transformation involves assigning argument expressions to temporary variables, which imposes an explicit evaluation order. FullInliner was written with the assumption that this order does not necessarily have to match usual argument evaluation order because the argument expressions have no side-effects. In most circumstances this assumption is true because the default optimization step sequence contains the ExpressionSplitter step. ExpressionSplitter ensures that the code is in expression-split form, which means that function calls cannot appear nested inside expressions, and all function call arguments have to be variables. The assumption is, however, not guaranteed to be true in general. Version 0.6.7 introduced a setting allowing users to specify an arbitrary optimization step sequence, making it possible for the FullInliner to actually encounter argument expressions with side-effects, which can result in behavior differences between optimized and unoptimized bytecode. Contracts compiled without optimization or with the default optimization sequence are not affected. To trigger the bug the user has to explicitly choose compiler settings that contain a sequence with FullInliner step not preceded by ExpressionSplitter. Ref

There are 1 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

2: pragma solidity 0.8.20;

link

</details>

[NC‑60] Consider adding validation of user inputs

There are 10 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

//@audit these parameters `_for`, are not checked
133:     function lockETHFor(address _for, bytes32 _referral) external payable {

//@audit these parameters `_for`, are not checked
157:     function lockFor(address _token, uint256 _amount, address _for, bytes32 _referral) external {

//@audit these parameters `_receiver`, are not checked
172:     function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral)

//@audit these parameters `_token`, are not checked
211:     function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)

//@audit these parameters `_token`, are not checked
226:     function claimAndStake(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data)

//@audit these parameters `_receiver`, are not checked
240:     function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data)

//@audit these parameters `_owner`, are not checked
336:     function setOwner(address _owner) external onlyAuthorized {

//@audit these parameters `_loopAddress`,`_vaultAddress`, are not checked
348:     function setLoopAddresses(address _loopAddress, address _vaultAddress)

//@audit these parameters `_token`, are not checked
364:     function allowToken(address _token) external onlyAuthorized {

//@audit these parameters `_sellToken`, are not checked
491:     function _fillQuote(IERC20 _sellToken, uint256 _amount, bytes calldata _swapCallData) internal {

link , link, link, link, link, link, link, link, link, link

</details>

[NC‑61] Consider using named function calls

Named function calls in Solidity greatly improve code readability by explicitly mapping arguments to their respective parameter names. This clarity becomes critical when dealing with functions that have numerous or complex parameters, reducing potential errors due to misordered arguments. Therefore, adopting named function calls contributes to more maintainable and less error-prone code.

There are 8 instances of this issue:

<details> <summary>see instances</summary>
File: ./src/PrelaunchPoints.sol

125:         _processLock(ETH, msg.value, msg.sender, _referral);

134:         _processLock(ETH, msg.value, _for, _referral);

147:         _processLock(_token, _amount, msg.sender, _referral);

161:         _processLock(_token, _amount, _for, _referral);

215:         _claim(_token, msg.sender, _percentage, _exchange, _data);

230:         uint256 claimedAmount = _claim(_token, address(this), _percentage, _exchange, _data);

254:             _validateData(_token, userClaim, _exchange, _data);

259:             _fillQuote(IERC20(_token), userClaim, _data);

link , link, link, link, link, link, link, link

</details>

#0 - CloudEllie

2024-05-11T17:25:51Z

#1 - 0xd4n1el

2024-05-13T17:11:09Z

It has some valid findings, but also some of them are not relevant given our assumptions

#2 - c4-judge

2024-06-03T10:46:09Z

koolexcrypto 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