General Information

Platform: Code4rena

Start Date: 30/11/2021

Pot Size: $100,000 USDC

Total HM: 15

Participants: 36

Period: 7 days

Judge: 0xean

Total Solo HM: 4

Id: 62

League: ETH

Streaming Protocol

Findings Distribution

Researcher Performance

Rank: 5/36

Findings: 6

Award: $5,433.20

🌟 Selected for report: 4

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x0x0x, ScopeLift, gpersoon, gzeon, harleythedog, hyh, jonah1005, kenzo

Labels

bug
duplicate
3 (High Risk)
sponsor confirmed

Awards

385.4189 USDC - $385.42

External Links

Handle

hyh

Vulnerability details

Impact

depositTokenFlashloanFeeAmount can be stolen with side effect of either rewardTokenFeeAmount or depositTokenAmount to be frozen within the contract.

If there are deposit token flash loan fees an attacker can wait for endDepositLock timestamp to pass and immediately run recoverTokens which doesn't account for depositTokenFlashloanFeeAmount and will allow attacker to withdraw all the flash loan fees accumulated for deposit tokens flash loans.

Side effect of this is that either

  1. rewardTokenFeeAmount will be frozen within the contract as claimFees require both fee amounts, rewardTokenFeeAmount and depositTokenFlashloanFeeAmount to be transferred successfully, while the latter will be already gone, so transfer will fail on any run: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L625

or

  1. creatorClaimSoldTokens will fail in the similar manner on transfer attempt if claimFees was run first, retrieving depositTokenFlashloanFeeAmount at the expense of depositTokenAmount, which will be frozen in the contract this way: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L597

Both (1) and (2) will lead to fund freeze because recoverTokens do account for depositTokenAmount and will not allow fund retrieval when the contract balance is less than that. Which variant will happen depends on what function be called first, correspondingly creatorClaimSoldTokens or claimFees.

Proof of Concept

depositTokenFlashloanFeeAmount isn't accounted in recoverTokens: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L654

This way after endDepositLock timestamp pass anyone will be able to retrieve depositTokenFlashloanFeeAmount, which recoverTokens will treat as extra unaccounted funds.

Knowing this, an attacker will track the timestamp and run the transaction with highest economically viable gas price in the closest time slot, retrieving depositTokenFlashloanFeeAmount and causing either (1) or (2) scenario of deposit token funds freeze as a side effect (i.e. this is not grieving attack per se, but fund freeze comes a side effect as some part of the funds become missing, disabling the retrieval functionality).

Add deposit flash loans fee to the recoverTokens logic (also including here the Sale case logic described in another issue posted earlier as this is an unrelated improvement to the same lines of code, i.e. 'to be' here is a merged version):

Now:

if (token == depositToken) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens);

To be:

if (token == depositToken) { ... uint256 accountedDepositTotal = depositTokenAmount; uint256 redeemedDepositTotal = isSale ? (claimedDepositTokens ? accountedDepositTotal : 0) : redeemedDepositTokens; uint256 excess = ERC20(token).balanceOf(address(this)) - (accountedDepositTotal - redeemedDepositTotal + depositTokenFlashloanFeeAmount);

#0 - 0xean

2022-01-14T20:51:50Z

duplicate of #241

Findings Information

🌟 Selected for report: cmichel

Also found by: GeekyLumberjack, hyh, kenzo, pedroais

Labels

bug
duplicate
3 (High Risk)

Awards

1057.3907 USDC - $1,057.39

External Links

Handle

hyh

Vulnerability details

Impact

Reward tokens accidently sent to the Stream contract cannot be recovered with recoverTokens if some reward tokens were already claimed with claimReward. As recoverTokens is the only recovering functionality in the contract the corresponding reward tokens will be frozen.

Proof of Concept

claimReward doesn't change rewardTokenAmount as other user's rewards are calculated based off it. But reward token balance of the Stream contract does change with each reward payoff: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L575

This way reward tokens already claimed aren't accounted for in recoverTokens as only rewardTokenAmount is used there: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L672

It looks like no fund stealing is possible here, the vulnerability only allows for freezing of the reward tokens accidently sent to the contract.

Add claimedRewardTokens variable tracking cumulative reward tokens claimed

Now:

uint112 private rewardTokenAmount; ... function claimReward() public lock { ... uint256 rewardAmt = ts.rewards; ... ERC20(rewardToken).safeTransfer(msg.sender, rewardAmt); emit RewardsClaimed(msg.sender, rewardAmt); } ... function recoverTokens(address token, address recipient) public lock { ... if (token == rewardToken) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - (rewardTokenAmount + rewardTokenFeeAmount); ERC20(token).safeTransfer(recipient, excess); ...

To be:

uint112 private rewardTokenAmount; uint112 private claimedRewardTokens; ... function claimReward() public lock { ... uint256 rewardAmt = ts.rewards; ... claimedRewardTokens += rewardAmt; ERC20(rewardToken).safeTransfer(msg.sender, rewardAmt); emit RewardsClaimed(msg.sender, rewardAmt); } ... function recoverTokens(address token, address recipient) public lock { ... if (token == rewardToken) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - (rewardTokenAmount - claimedRewardTokens + rewardTokenFeeAmount); ERC20(token).safeTransfer(recipient, excess); ...

#0 - 0xean

2022-01-14T21:14:02Z

dupe of #214

Findings Information

🌟 Selected for report: harleythedog

Also found by: hyh, kenzo, pauliax, pedroais

Labels

bug
duplicate
3 (High Risk)

Awards

1057.3907 USDC - $1,057.39

External Links

Handle

hyh

Vulnerability details

Impact

Stream's recoverTokens will not allow recover any deposit tokens after Stream creator has claimed the depositTokenAmount of deposit tokens via creatorClaimSoldTokens in the sale Stream type case.

As there are no other token recovery mechanics the remaining funds, if any, will be effectively frozen within the contract.

Proof of Concept

When the Stream is a Sale after creatorClaimSoldTokens was successfully called the depositTokenAmount of depositToken will be transferred away: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L583

Since this the recoverTokens logic will not work for depositToken as it will not allow withdrawing up to depositTokenAmount: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L646

While there shouldn't be any deposit tokens left on the balance and so all extra deposit token should be withdrawable. This way any deposit tokens left will be frozen within the contract.

Update the logic to account for creator's ability to claim deposit token stash when Stream type is Sale.

Now:

if (token == depositToken) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens);

To be (adding extra memory variable to save on storage reads):

if (token == depositToken) { ... uint256 accountedDepositTotal = depositTokenAmount; uint256 redeemedDepositTotal = isSale ? (claimedDepositTokens ? accountedDepositTotal : 0) : redeemedDepositTokens; uint256 excess = ERC20(token).balanceOf(address(this)) - (accountedDepositTotal - redeemedDepositTotal);

#0 - brockelmore

2022-01-05T17:26:46Z

duplicate #121

Findings Information

🌟 Selected for report: hyh

Labels

bug
2 (Med Risk)
sponsor disputed

Awards

2417.4456 USDC - $2,417.45

External Links

Handle

hyh

Vulnerability details

Impact

Any airdrop gathered with arbitraryCall will be immediately lost as an attacker can track arbitraryCall transactions and back run them with calls to recoverTokens, which doesn't track any tokens besides reward, deposit and incentive tokens, and will give the airdrop away.

Proof of Concept

arbitraryCall requires that tokens to be gathered shouldn't be reward, deposit or incentive tokens: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L735

Also, the function doesn't mark gathered tokens in any way. Thus, the airdrop is freely accessible for anyone to be withdrawn with recoverTokens: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L687

Add airdrop tokens balance mapping, record what is gathered in arbitraryCall and prohibit their free withdrawal in recoverTokens similarly to incentives[].

Now:

mapping (address => uint112) public incentives; ... function recoverTokens(address token, address recipient) public lock { ... if (incentives[token] > 0) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - incentives[token]; ... }

To be:

mapping (address => uint112) public incentives; mapping (address => uint112) public airdrops; ... function recoverTokens(address token, address recipient) public lock { ... if (incentives[token] > 0) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - incentives[token]; ... } if (airdrops[token] > 0) { ... uint256 excess = ERC20(token).balanceOf(address(this)) - airdrops[token]; ... } ... // we do know what airdrop token will be gathered function arbitraryCall(address who, bytes memory data, address token) public lock externallyGoverned { ... // get token balances uint256 preDepositTokenBalance = ERC20(depositToken).balanceOf(address(this)); uint256 preRewardTokenBalance = ERC20(rewardToken).balanceOf(address(this)); uint256 preAirdropBalance = ERC20(token).balanceOf(address(this)); (bool success, bytes memory _ret) = who.call(data); require(success); uint256 postAirdropBalance = ERC20(token).balanceOf(address(this)); require(postAirdropBalance <= type(uint112).max, "air_112"); uint112 amt = uint112(postAirdropBalance - preAirdropBalance); require(amt > 0, "air"); airdrops[token] += amt;

#0 - brockelmore

2021-12-06T18:28:12Z

The intention is that the claim airdrop + transfer is done atomically. Compound-style governance contracts come with this ability out of the box.

#1 - 0xean

2022-01-15T01:17:29Z

Going to agree with the warden that as the code is written this is an appropriate risk to call out and be aware of. Downgrading in severity because it relies on external factors but there is no on chain enforcement that this call will be operated correctly and therefore believe it represent a valid concern even if the Sponsor has a mitigation plan in place.

Findings Information

🌟 Selected for report: cyberboy

Also found by: Jujic, defsec, hyh, mtz, robee

Labels

bug
duplicate
1 (Low Risk)

Awards

79.3043 USDC - $79.30

External Links

Handle

hyh

Vulnerability details

Impact

As different compiler versions have critical behavior specifics if the contract gets accidentally deployed using another compiler version compared to one they tested with, various types of undesired behavior can be introduced.

Proof of Concept

LockeERC20 contract use pragma solidity >=0.8.0: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/LockeERC20.sol#L2

Fix the compiler version. ^ modifier is viable as it's done in Locke: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L2

#0 - brockelmore

2021-12-06T16:58:13Z

duplicate #19

Findings Information

🌟 Selected for report: hyh

Also found by: 0x1f8b, Meta0xNull

Labels

bug
1 (Low Risk)

Awards

217.5701 USDC - $217.57

External Links

Handle

hyh

Vulnerability details

Impact

Being instantiated with wrong configuration, the contract is inoperable and deploy gas costs will be lost. If misconfiguration is noticed too late the various types of malfunctions become possible.

Proof of Concept

The checks for zero addresses during contract construction and initialization are best-practices.

Now LockeERC20 and Stream contracts do not check for correctness of constructor arguments:

LockeERC20 https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/LockeERC20.sol#L56

Stream https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L263

Add zero-address checks and key non-address variables checks in all contract constructors. Small increase of gas costs are far out weighted by wrong deploy costs savings and additional coverage against misconfiguration.

Findings Information

🌟 Selected for report: WatchPug

Also found by: defsec, hyh, pauliax

Labels

bug
duplicate
G (Gas Optimization)

Awards

18.2633 USDC - $18.26

External Links

Handle

hyh

Vulnerability details

Impact

Gas is overspent on check (compiler is 0.8.0). As withdraw is a core user facing function even small optimizations are recommended.

Proof of Concept

withdraw function: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L455

Also, depositTokenAmount variable always changes along with ts.tokens one, with the only difference that ts.tokens can be reduced by updateStreamInternal logic: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L229

This way depositTokenAmount is always not less than ts.tokens and can be decreased unchecked.

Now:

require(ts.tokens >= amount, "amt"); ts.tokens -= amount; ... depositTokenAmount -= amount;

To be:

require(ts.tokens >= amount, "amt"); unchecked { ts.tokens -= amount; depositTokenAmount -= amount; }

#0 - 0xean

2022-01-17T13:42:51Z

dupe of #238

Findings Information

🌟 Selected for report: hyh

Labels

bug
G (Gas Optimization)

Awards

100.2104 USDC - $100.21

External Links

Handle

hyh

Vulnerability details

Impact

Gas is overspent on storage access.

Proof of Concept

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L203

Save repeatedly accessed storage variables (lastUpdate, ts.lastUpdate, ts.tokens) to memory and use them. As each storage read is costly this will reduce function gas consumption.

Now:

if (block.timestamp >= startTime) { // set lastUpdates if need be if (ts.lastUpdate == 0) { ts.lastUpdate = uint32(block.timestamp); } if (lastUpdate == 0) { lastUpdate = uint32(block.timestamp); } ... // update users unstreamed balance uint32 acctTimeDelta = uint32(block.timestamp) - ts.lastUpdate; if (acctTimeDelta > 0 && ts.tokens > 0) { // some time has passed since this user last interacted // update ts not yet streamed ts.tokens -= uint112(acctTimeDelta * ts.tokens / (endStream - ts.lastUpdate));

To be:

if (block.timestamp >= startTime) { uint32 lastUp = lastUpdate; uint32 tsLastUp = ts.lastUpdate; uint32 tsTokens = ts.tokens; uint32 blockT = uint32(block.timestamp); // set lastUpdates if need be if (tsLastUp == 0) { ts.lastUpdate = blockT; tsLastUp = blockT; } if (lastUp == 0) { lastUpdate = uint32(block.timestamp); lastUp = blockT; } ... // update users unstreamed balance uint32 acctTimeDelta = blockT - tsLastUp; if (acctTimeDelta > 0 && tsTokens > 0) { // some time has passed since this user last interacted // update ts not yet streamed ts.tokens -= uint112(acctTimeDelta * tsTokens / (endStream - tsLastUp));

Findings Information

🌟 Selected for report: hyh

Labels

bug
G (Gas Optimization)

Awards

100.2104 USDC - $100.21

External Links

Handle

hyh

Vulnerability details

Impact

Gas is overspent on storage access and operations.

Proof of Concept

claimReward function has some excess operations and the number of times it reads storage can be reduced: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L555

Save repeatedly accessed storage variables to memory and use them, simplify the logic:

Now:

... cumulativeRewardPerToken = rewardPerToken(); // update user rewards ts.rewards = earned(ts, cumulativeRewardPerToken); // update users last cumulative reward per token ts.lastCumulativeRewardPerToken = cumulativeRewardPerToken; lastUpdate = lastApplicableTime(); uint256 rewardAmt = ts.rewards; ts.rewards = 0; require(rewardAmt > 0, "amt"); // transfer the tokens ERC20(rewardToken).safeTransfer(msg.sender, rewardAmt); ...

To be:

... uint256 _cumulativeRewardPerToken = rewardPerToken(); uint256 rewardAmt = earned(ts, _cumulativeRewardPerToken); require(rewardAmt > 0, "amt"); cumulativeRewardPerToken = _cumulativeRewardPerToken; lastUpdate = lastApplicableTime(); // update users last cumulative reward per token and rewards ts.lastCumulativeRewardPerToken = _cumulativeRewardPerToken; ts.rewards = 0; // transfer the tokens ERC20(rewardToken).safeTransfer(msg.sender, rewardAmt); ...
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