Streaming Protocol contest - kenzo's results

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: 8/36

Findings: 5

Award: $4,548.09

🌟 Selected for report: 4

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: WatchPug

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

Labels

bug
duplicate
3 (High Risk)

Awards

385.4189 USDC - $385.42

External Links

Handle

kenzo

Vulnerability details

Only the governance should be able to collect flash loan fees, however due to a bug, the stream creator can claim deposit token flashloan fees too.

Impact

Lost fees for the protocol.

Proof of Concept

When a flash loan of deposit token is being made, and fees are added to the contract's depositToken balance, Streaming increases depositTokenFlashloanFeeAmount :

depositTokenFlashloanFeeAmount += feeAmt;

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L714 However, in recoverTokens, Streaming does not substract that amount from the amount that is rescueable:

uint256 excess = ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens); ERC20(token).safeTransfer(recipient, excess);

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L654 Therefore, the deposit token flashloan fees would be considered as "excess" and the stream creator could claim them.

Include depositTokenFlashloanFeeAmount in the excess calculation above.

#0 - 0xean

2022-01-14T20:53:51Z

dupe 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

kenzo

Vulnerability details

In claimReward, Streaming does not save the amount of rewards claimed. This leads to wrong excess calculation in recoverTokens which will underflow the calculation and revert the rescue attempt if enough rewards have been claimed.

Impact

Rescuing of reward tokens will mostly not work if we assume people will claim their rewards.

Severity?

I classified this issue as High Risk because of few reasons:

  1. It relates to loss of user funds
  2. This was one of the "areas to focus on" in the readme
  3. The recoverTokens functionality is not implemented in all protocols, but here it gives the users this functionality/assurance. Therefore, since the functionality is not working I consider it as a case of lost user funds. However, since the loss of users funds relates to accidentally sent tokens and not protocol funds, perhaps this issue should be downgraded to medium. I am not sure, but because of the abovementioned reasons I classified it as high.

Proof of Concept

When a stream is being funded, the funded amount is saved to rewardTokenAmount:

rewardTokenAmount += amt;

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L403 (also #L406) And when reward tokens are being rescued, this amount is removed from excess, so to not remove funds which belong to users:

uint256 excess = ERC20(token).balanceOf(address(this)) - (rewardTokenAmount + rewardTokenFeeAmount);

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L672 However, when users are claiming rewards, the stream's rewardToken balance will decrease but rewardTokenAmount is not updated to reflect it. It is not changed at all. https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L555:#L578 Therefore in the abovementioned excess calculation:

uint256 excess = ERC20(token).balanceOf(address(this)) - (rewardTokenAmount + rewardTokenFeeAmount);

(let's assume rewardTokenFeeAmount = 0 for simplicity) If people have claimed more rewards than tokens that need to be rescued, then ERC20(token).balanceOf(address(this)) < rewardTokenAmount, the calculation will underflow and the rescue attempt will revert.

If I am not mistaken, rewardTokenAmount can be decreased in claimReward. Alternatively, create another variable rewardTokenClaimed, increase it in claimReward, and remove it from the excess in recoverTokens.

#0 - ninek9

2021-12-06T18:21:24Z

ADDITIONAL COMMENT FROM WARDEN:

Note: throughout this issue when I wrote "rescue reward tokens", I meant "rescue excess rewardToken tokens". Obviously the reward tokens themselves shouldn't be rescued, only the excess.

#1 - 0xean

2022-01-14T21:14:38Z

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

kenzo

Vulnerability details

If the stream is a sale, after the creator has claimed the deposit tokens, Streaming does not save the amount of tokens claimed. Therefore if trying to rescue excess depositToken tokens, the calculation will underflow and the rescue attempt will revert and fail.

Impact

Rescuing of depositToken tokens does not work if the stream is a sale and the deposit tokens have been claimed.

Severity?

I classified this issue as High Risk because of few reasons:

  1. It relates to loss of user funds
  2. This was one of the "areas to focus on" in the readme
  3. The recoverTokens functionality is not implemented in all protocols, but here it gives the users this functionality/assurance. Therefore, since the functionality is not working I consider it as a case of lost user funds. However, since the loss of users funds relates to accidentally sent tokens and not protocol funds, perhaps this issue should be downgraded to medium. I am not sure, but because of the abovementioned reasons I classified it as high.

Proof of Concept

When users stake their depositToken tokens, the amount is saved to depositTokenAmount:

depositTokenAmount += trueDepositAmt;

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L431 When not in a sale, when users claim their deposit tokens, the amount redeemed is saved in redeemedDepositTokens: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L544 In recoverTokens, when calculating the amount of depositToken tokens that can be rescued, Streaming removes the amount of deposit tokens which have been redeemed by depositors:

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

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L654 However, this does not take into account the amount of depositToken tokens which have been redeemed by the creator in the case of a sale. In that case, in creatorClaimSoldTokens, there is nothing that updates the amount of tokens that have been claimed: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L583:#L600 Therefore, the abovementioned excess calculation:

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

Will underflow if the creator has claimed sold deposits and the amount of tokens needing to be rescued is smaller than that.

Either decrease depositTokenAmount in creatorClaimSoldTokens, Or, save there the amount of tokens the creator has claimed, and substract that from the excess calculation above.

#0 - brockelmore

2021-12-08T22:22:10Z

ignore above mention ^

#1 - 0xean

2022-01-15T01:26:58Z

dupe of #121

Findings Information

🌟 Selected for report: mtz

Also found by: hubble, kenzo

Labels

bug
duplicate
1 (Low Risk)

Awards

217.5701 USDC - $217.57

External Links

Handle

kenzo

Vulnerability details

In StreamFactory, the protocol max fee is defined to be 500. However in Stream, it is checked whether the protocol fee is < 10000 (total basis points).

Impact

I think this might be considered "function incorrect as to spec" and this is why I rated this issue low severity. At the very least this issue can raise some confusion when reading the code.

Proof of Concept

In StreamFactory, max fee is hardcoded to be 500: ` `` uint16 constant MAX_FEE_PERCENT = 500; // 500/10000 == 5%

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L773 And upon update of fee, it is checked against that variable:

require(newFeeParams.feePercent <= MAX_FEE_PERCENT, "fee");

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L851 In Stream, the fee percent is limited to 10000.

require(feePercent < 10000, "fee");

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L286 ## Recommended Mitigation Steps I suggest having the two values match - either by reading the max fee from the factory, or by hardcoding it to match.

#0 - brockelmore

2022-01-05T17:35:48Z

duplicate #246

Findings Information

🌟 Selected for report: kenzo

Labels

bug
1 (Low Risk)

Awards

805.8152 USDC - $805.82

External Links

Handle

kenzo

Vulnerability details

The comment in claimReward says:

Allows a receipt token holder (or original depositor in case of a sale) to claim their rewardTokens

However, the function only allows the original depositor to claim the rewards. Additionally, the C4 readme doesn't mention that the receipt token holder should be able to withdraw the reward.

Impact

Confusion regarding working of protocol.

Proof of Concept

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

Change the comment to reflect the fact that only original depositor may withdraw rewards.

Findings Information

🌟 Selected for report: kenzo

Labels

bug
1 (Low Risk)

Awards

805.8152 USDC - $805.82

External Links

Handle

kenzo

Vulnerability details

unstreamed (global unstreamed) is not updated on withdrawal.

Impact

As far as I see it is not used in calculations, so only wrong information presented to users.

Proof of Concept

Upon user staking, the global unstreamed amount is increased:

unstreamed += trueDepositAmt

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L438 This value is kept up to date in updateStreamInternal:

uint256 globalStreamingSpeedPerSecond = (uint256(unstreamed) * 10**6)/ (endStream - lastUpdate); unstreamed -= uint112((uint256(tdelta) * globalStreamingSpeedPerSecond) / 10**6);

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L236:#L239 However, when a user withdraws funds via withdraw (/exit), there is no decrease of unstreamed: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L455:#L479 Therefore, unstreamed will return more tokens than truly are.

I believe the following line should be added to withdraw:

unstreamed -= amount;

Findings Information

🌟 Selected for report: 0x0x0x

Also found by: WatchPug, kenzo, pauliax

Labels

bug
duplicate
G (Gas Optimization)
sponsor confirmed

Awards

18.2633 USDC - $18.26

External Links

Handle

kenzo

Vulnerability details

exit function has updateStream modifier, which is unnecessary because basically all exit does is call withdraw which already has updateStream modifier.

Impact

Some gas can be saved. Second run of modifier will just do some checks and nothing is needed to be changed.

Proof of Concept

exit has the updateStream modifier and only calls withdraw:

function exit() public updateStream(msg.sender) { TokenStream storage ts = tokensNotYetStreamed[msg.sender]; uint112 amount = ts.tokens; withdraw(amount); }

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L487:#L493 withdraw already has that modifier:

function withdraw(uint112 amount) public lock updateStream(msg.sender) {

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L455 So the second run of the modifier is unnecessary and will just waste gas.

Remove the modifier from exit.

#0 - ninek9

2021-12-07T01:49:29Z

ADDITIONAL COMMENT FROM WARDEN:

On further thought, I realize that if we don't have the modifier on the exit function, then ts.tokens wouldn't get updated, and we'll send the wrong amounts to withdraw. So I think the issue is invalid but not sure.

#1 - 0xean

2022-01-17T13:52:33Z

dupe of #187

Findings Information

🌟 Selected for report: kenzo

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

100.2104 USDC - $100.21

External Links

Handle

kenzo

Vulnerability details

Since claimReward can only be called after endRewardLock, lastApplicableTime will always return endStream.

Impact

Some gas can be saved.

Proof of Concept

claimReward will only run if time > endRewardLock (which is >= endStream):

require(block.timestamp > endRewardLock, "lock");

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L556 claimReward is calling lastApplicableTime:

lastUpdate = lastApplicableTime();

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L567 And this is lastApplicableTime:

return block.timestamp <= endStream ? uint32(block.timestamp) : endStream;

https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L340 Therefore, it will always return endStream.

In claimReward, change this line:

lastUpdate = lastApplicableTime();

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

lastUpdate = endStream;

Findings Information

🌟 Selected for report: kenzo

Labels

bug
G (Gas Optimization)

Awards

100.2104 USDC - $100.21

External Links

Handle

kenzo

Vulnerability details

In updateStreamParams and updateFeeParams, the old values are being saved (probably in order to log them) before being updated. This is unnecessary.

Impact

Small amount of gas can be saved. (Although maybe it was done this way for code clarity..?)

Proof of Concept

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

function updateFeeParams(GovernableFeeParams memory newFeeParams) public governed { require(newFeeParams.feePercent <= MAX_FEE_PERCENT, "fee"); GovernableFeeParams memory old = feeParams; feeParams = newFeeParams; emit FeeParametersUpdated(old, newFeeParams); }

The code can be changed to:

function updateFeeParams(GovernableFeeParams memory newFeeParams) public governed { require(newFeeParams.feePercent <= MAX_FEE_PERCENT, "fee"); emit FeeParametersUpdated(feeParams, newFeeParams); feeParams = newFeeParams; }

Same thing can be done in updateStreamParams. https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L841:#L848

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