Platform: Code4rena
Start Date: 14/09/2022
Pot Size: $50,000 USDC
Total HM: 25
Participants: 110
Period: 5 days
Judge: hickuphh3
Total Solo HM: 9
Id: 162
League: ETH
Rank: 5/110
Findings: 7
Award: $4,088.76
π Selected for report: 4
π Solo Findings: 2
π Selected for report: unforgiven
Also found by: carrotsmuggler, cccz
320.0832 USDC - $320.08
In the deposit function of the Vault contract, the user is allowed to deposit assets when block.timestamp < idEpochBegin[id] - timewindow, but the user can only withdraw assets after triggerDepeg or triggerEndEpoch has been called. Since the isDisaster modifier of the Controller contract only allows users to call the triggerDepeg function between epochBegin and epochEnd, which means that the user's assets are locked in the contract before the insurance takes effect. If users accidentally deposit assets into the incorrect vault, they cannot withdraw the assets even if the insurance is not yet in effect.
https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L87-L99 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L152-L174 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L203-L214 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Controller.sol#L101-L109
None
Consider allowing users to withdraw assets before the insurance takes effect
#0 - HickupHH3
2022-10-31T14:05:55Z
User-conditional mistake of depositing to the wrong vault (https://github.com/code-423n4/org/issues/53). Could be that user clicks wrong, or wishes to reverse his decision. Being allowed to withdraw before the insured period seems like a reasonable user action to have IMO.
#1 - HickupHH3
2022-11-03T14:23:33Z
dup #447
116.6703 USDC - $116.67
In the Vault contract, when the changeTimewindow function is called to change the timewindow variable, the epochHasNotStarted modifier is affected, thereby affecting the deposit opening window. Consider the following scenario timewindow = 100 EpochBegin = 1000 At time 900, user A observes that the ratio of assets in insrVault to riskVault is 1:5. User A likes this ratio and thinks that the ratio will not change in the future, so user A chooses to deposit assets into insrVault. However, the changeTimewindow function changes the timewindow to 50, users have a longer time window to deposit, and the ratio of assets in insrVault to riskVault reaches 1:1, which breaks user A's expectations.
https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L87-L91 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L287-L289
None
Consider saving the current timewindow in the createAssets function and use the saved timewindow in the epochHasNotStarted modifier for calculations
#0 - HickupHH3
2022-10-31T13:25:29Z
Agreed; time window changes shouldnt be retroactively applied as it changes the T&Cs of users that we were fine with (feeling a sense of rug).
π Selected for report: cccz
Also found by: Respx, Saintcode_, csanuragjain, fatherOfBlocks, pashov, unforgiven
117.0037 USDC - $117.00
Similar to https://github.com/code-423n4/2022-02-concur-findings/issues/210 StakingRewards.recoverERC20 rightfully checks against the stakingToken being sweeped away. However, thereβs no check against the rewardsToken. This is the case of an admin privilege, which allows the owner to sweep the rewards tokens, perhaps as a way to rug depositors.
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner { require( tokenAddress != address(stakingToken), "Cannot withdraw the staking token" ); ERC20(tokenAddress).safeTransfer(owner, tokenAmount); emit Recovered(tokenAddress, tokenAmount); }
None
Add an additional check
require( tokenAddress != address(rewardsToken), "Cannot withdraw the rewards token" );
#0 - scaraven
2022-09-19T20:21:47Z
I'm curious what others think about this issue, if users do not receive any reward tokens is that really a problem? Users are still able to withdraw their ERC1155 tokens at any time, and vaults still work as expected. If the admin is malicious, users will miss out on tokens which will be worthless after a rugpull anyway.
#1 - MiguelBits
2022-09-30T04:03:39Z
this is how synthetix rewards work, I forked their smart contracts
#2 - HickupHH3
2022-10-29T09:21:38Z
Including this issue, issues #50, #51 and #52 can be considered to be admin privilege findings. There's active discussion revolving how findings of this category should be handled / standardized.
For now, I'm keeping the medium severity due to historical context (past contest references). l also reproduce the classification rationale that I gave for a previous contest below:
Classification and thought process
The issues raised about rugpull vulnerabilities via centralisation risks can be broadly classified into 2 categories:
- Those that can be mitigated with contract modifications. Examples include:
- ensuring upper / lower proper bounds on key variables (fees not exceeding max threshold, for instance)
- adding safeguards and conditional checks (require statements)
- Those that can't be strictly enforced
- Use multisig, put admin under timelock
Category 1 can be separated into the various attack vectors and actors (admin / strategist), as the mitigation is more tangible in nature. This way, the recommended fixes can also be easily identified and adopted. A warden that grouped multiple attack vectors together will have their issue made the primary issue; the rest will be marked as duplicates of it.
Regarding category 2, for issues that are generic "put admin under timelock" without explaining how and why a compromised owner / strategist can rug, as per the rulebook and judges' general consensus, I will downgrade their severity to QA. Those that explained the impact and vulnerability in detail will be grouped together with medium severity because there isn't much that can be done about it.
π Selected for report: cccz
Also found by: csanuragjain, pashov
416.1082 USDC - $416.11
Similar to https://github.com/code-423n4/2022-02-concur-findings/issues/209
uint256 balance = rewardsToken.balanceOf(address(this)); require( rewardRate <= balance.div(rewardsDuration), "Provided reward too high" );
In the current implementation, the contract only checks if balanceOf rewardsToken is greater than or equal to the future rewards.
However, under normal circumstances, since users can not withdraw all their rewards in time, the balance in the contract contains rewards that belong to the users but have not been withdrawn yet. This means the current checks can not be sufficient enough to make sure the contract has enough amount of rewardsToken.
As a result, if the rewardsDistribution mistakenly notifyRewardAmount with a larger amount, the contract may end up in a wrong state that makes some users unable to claim their rewards.
Given:
Expected Results:
The tx in step 5 should revert.
None
Consider changing the function notifyRewardAmount to addRward and use transferFrom to transfer rewardsToken into the contract:
function addRward(uint256 reward) external updateReward(address(0)) { require( msg.sender == rewardsDistribution, "Caller is not RewardsDistribution contract" ); if (block.timestamp >= periodFinish) { rewardRate = reward / rewardsDuration; } else { uint256 remaining = periodFinish - block.timestamp; uint256 leftover = remaining * rewardRate; rewardRate = (reward + leftover) / rewardsDuration; } rewardsToken.safeTransferFrom(msg.sender, address(this), reward); lastUpdateTime = block.timestamp; periodFinish = block.timestamp + rewardsDuration; emit RewardAdded(reward); }
#0 - HickupHH3
2022-10-31T14:25:55Z
selected as primary because it's the most complete.
π Selected for report: cccz
1541.1415 USDC - $1,541.14
Similar to https://github.com/code-423n4/2022-02-concur-findings/issues/183.
if (block.timestamp >= periodFinish) { rewardRate = reward.div(rewardsDuration); } else { uint256 remaining = periodFinish.sub(block.timestamp); uint256 leftover = remaining.mul(rewardRate); rewardRate = reward.add(leftover).div(rewardsDuration); }
The StakingRewards.notifyRewardAmount function receives a reward amount and extends the current reward end time to now + rewardsDuration. It rebases the currently remaining rewards + the new rewards (reward + leftover) over this new rewardsDuration period. This can lead to a dilution of the reward rate and rewards being dragged out forever by malicious new reward deposits.
Imagine the current rewardRate is 1000 rewards / rewardsDuration. 20% of the rewardsDuration passed, i.e., now = lastUpdateTime + 20% * rewardsDuration. A malicious actor notifies the contract with a reward of 0: notifyRewardAmount(0). Then the new rewardRate = (reward + leftover) / rewardsDuration = (0 + 800) / rewardsDuration = 800 / rewardsDuration. The rewardRate just dropped by 20%. This can be repeated infinitely. After another 20% of reward time passed, they trigger notifyRewardAmount(0) to reduce it by another 20% again: rewardRate = (0 + 640) / rewardsDuration = 640 / rewardsDuration.
None
The rewardRate should never decrease by a notifyRewardAmount call. Consider not extending the reward payouts by rewardsDuration on every call. periodFinish probably shouldn't change at all, the rewardRate should just increase by rewardRate += reward / (periodFinish - block.timestamp).
Alternatively, consider keeping the rewardRate constant but extend periodFinish time by += reward / rewardRate.
#0 - HickupHH3
2022-10-29T15:53:11Z
Admin privilege issue that would allow the admin to dilute current rewards. Medium severity due to loss of yield for all depositors from dilution of rewardRate
.
π Selected for report: cccz
1541.1415 USDC - $1,541.14
When the triggerEndEpoch function of the Controller contract is called, the assets in the insrVault will be sent to the riskVault, which also means that the tokens in the insrVault will be worthless.
insrVault.setClaimTVL(epochEnd, 0); ... else { entitledAmount = amount.divWadDown(idFinalTVL[id]).mulDivDown( idClaimTVL[id], 1 ether ); }
However, if the periodFinish > _epochEnd in the StakingRewards contract corresponding to the insrVault, the user can continue to stake his insrToken and receive rewards.
https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Controller.sol#L223 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L418-L423 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/rewards/StakingRewards.sol#L183-L210
None
Add the following code at the end of the notifyRewardAmount function of the StakingRewards contract to limit the periodFinish
lastUpdateTime = block.timestamp; periodFinish = block.timestamp.add(rewardsDuration); + require(periodFinish <= id);
#0 - HickupHH3
2022-10-31T13:23:26Z
Agree with issue; I view this as protocol leaked value (rewards) because it enables expired vault tokens that have no / little worth to "steal" rewards from future valid epochs. 1 time mint, lifetime rewards doesn't seem right.
π Selected for report: Respx
Also found by: 0x1f8b, 0xDecorativePineapple, 0xNazgul, 0xPanas, 0xSmartContract, 0xc0ffEE, 0xmuxyz, Aymen0909, Bahurum, Bnke0x0, CodingNameKiki, Deivitto, Jeiwan, Lambda, Picodes, PwnPatrol, R2, RaymondFam, Rolezn, Ruhum, Saintcode_, SooYa, Tointer, V_B, ajtra, ak1, async, auditor0517, brgltd, c3phas, carrotsmuggler, cccz, csanuragjain, datapunk, djxploit, durianSausage, eierina, erictee, gogo, imare, joestakey, jonatascm, kv, ladboy233, leosathya, lukris02, oyc_109, pashov, pauliax, rbserver, robee, rokinot, rvierdiiev, scaraven, simon135, unforgiven, wagmi, zzzitron
36.6223 USDC - $36.62
Similar to https://github.com/code-423n4/2022-02-concur-findings/issues/223. notifyRewardAmount will be inoperable if rewardsDuration bet set to zero. If will cease to produce meaningful results if rewardsDuration be too small or too big.
The setter do not control the value, allowing zero/near zero/enormous duration:
Division by the duration is used in notifyRewardAmount:
None
Check for min and max range in the rewardsDuration setter, as too small or too big rewardsDuration breaks the logic
#0 - HickupHH3
2022-10-29T15:42:06Z
While the finding is valid,
notifyRewardAmount
has no impact on funds nor existing rewards. function can be called again to re-set its value. one can also view it as a way to prevent further reward distributionsrewardsRate
to be zero, thus bricking reward distribution.Because the former point has minimal impact while the latter lacks a crucial step in explaining the adverse impact of failing reward distributions, I have to downgrade the issue to QA.
User's primary QA.