Omniscia Parabola Finance Audit
MultiStakingRewards Manual Review Findings
MultiStakingRewards Manual Review Findings
MSR-01M: Potential Circumvention of Lock Time
Type | Severity | Location |
---|---|---|
Logical Fault | Medium | MultiStakingRewards.sol:L402, L424, L544-L555 |
Description:
The earned rewards are meant to be locked for a predeterminate lockDuration
, however, when they are re-staked for a non-zero week the lock is not evaluated. As such, if the lockDuration
is greater than the weeks that the user selects in withdrawEarnedOrLock
, they are incentivized to circumvent the lock and re-stake them for a shorter period.
Example:
393// Earn new tokens394// Earned tokens receive rewards normally but incur a 50% penalty when395// withdrawn before lockDuration has passed.396function earn(address user, uint256 amount) external updateReward(user) {397 require(earners[msg.sender]);398 totalSupply = totalSupply.add(amount);399 Balances storage bal = balances[user];400 bal.total = bal.total.add(amount);401 bal.earned = bal.earned.add(amount);402 uint256 unlockTime = block.timestamp.div(rewardsDuration).mul(rewardsDuration).add(lockDuration);403 LockedBalance[] storage earnings = userEarnings[user];404 uint256 idx = earnings.length;405
406 if (idx == 0 || earnings[idx - 1].unlockTime < unlockTime) {407 earnings.push(LockedBalance({amount : amount, boostedAmount : 0, unlockTime : unlockTime}));408 } else {409 earnings[idx - 1].amount = earnings[idx - 1].amount.add(amount);410 }411 emit Staked(user, 0, amount);412}413
414// Withdraw earned tokens415event WithdrawEarnedOrLock(address indexed source, uint256 total, uint256 amount, uint256 penatly);416
417function withdrawEarnedOrLock(uint256 pid, uint256 week) external nonReentrant updateReward(msg.sender) {418 require(pid >= 0);419 require(pid <= userEarnings[msg.sender].length - 1);420
421 uint256 userEarnedAmount = userEarnings[msg.sender][pid].amount;422 uint256 userEarnedUnlockTime = userEarnings[msg.sender][pid].unlockTime;423
424 if (week != 0) {425 require(weekMultiplier[week] != 0, "Week not supported!");426
427 Balances storage bal = balances[msg.sender];428
429 uint boostedAmount = userEarnedAmount.mul(weekMultiplier[week]).div(PRICE_PRECISION);430 lockedSupply = lockedSupply.add(userEarnedAmount);431 boostedSupply = boostedSupply.add(boostedAmount);432 bal.locked = bal.locked.add(userEarnedAmount);433 bal.boosted = bal.boosted.add(boostedAmount);434 bal.earned = bal.earned.sub(userEarnedAmount);435 uint256 unlockTime = block.timestamp.div(rewardsDuration).mul(rewardsDuration).add(rewardsDuration * week);436 uint256 idx = userLocks[msg.sender][week].length;437 if (idx == 0 || userLocks[msg.sender][week][idx - 1].unlockTime < unlockTime) {438 userLocks[msg.sender][week].push(LockedBalance({amount : userEarnedAmount, boostedAmount : boostedAmount, unlockTime : unlockTime}));439 } else {440 userLocks[msg.sender][week][idx - 1].amount = userLocks[msg.sender][week][idx - 1].amount.add(userEarnedAmount);441 userLocks[msg.sender][week][idx - 1].boostedAmount = userLocks[msg.sender][week][idx - 1].boostedAmount.add(boostedAmount);442 }443 delete userEarnings[msg.sender][pid];444
445 emit Staked(msg.sender, week, userEarnedAmount);446 } else {447 uint256 penaltyAmount = 0;448 uint256 remaining = 0;449 if (userEarnedAmount > 0 && userEarnedUnlockTime > block.timestamp) {450 penaltyAmount = userEarnedAmount.div(2);451 remaining = userEarnedAmount.sub(penaltyAmount);452 } else {453 remaining = userEarnedAmount;454 }455
456 delete userEarnings[msg.sender][pid];457
458 totalSupply = totalSupply.sub(userEarnedAmount);459 Balances storage bal = balances[msg.sender];460 bal.total = bal.total.sub(userEarnedAmount);461 bal.earned = bal.earned.sub(userEarnedAmount);462
463 emit WithdrawEarnedOrLock(address(msg.sender), userEarnedAmount, remaining, penaltyAmount);464
465 stakingToken.safeTransfer(msg.sender, remaining);466 if (penaltyAmount > 0) {467 _notifyReward(address(stakingToken), penaltyAmount);468 }469 }470}
Recommendation:
We advise the withdrawEarnedOrLock
function to properly evaluated that the new unlockTime
is greater than the existing earning unlockTime
to mandate proper unlock times for all funds.
Alleviation:
The Parabola team opted not to apply a remediation for this finding in the current iteration of the codebase.
MSR-02M: Inexistent Validation of Proper Withdrawal
Type | Severity | Location |
---|---|---|
Logical Fault | Minor | MultiStakingRewards.sol:L609-L633 |
Description:
The withdrawExpiredLocks
function does not properly validate that a non-zero amount is being unlocked.
Example:
608// Withdraw all currently locked tokens where the unlock time has passed609function withdrawExpiredLocks() external {610 uint256 amount = 0;611 uint256 boostedAmount = 0;612 Balances storage bal = balances[msg.sender];613 for (uint w = 0; w < supportWeek.length; w++) {614 LockedBalance[] storage locks = userLocks[msg.sender][supportWeek[w]];615
616 for (uint i = 0; i < locks.length; i++) {617 if (locks[i].unlockTime > block.timestamp) {618 break;619 }620 amount = amount.add(locks[i].amount);621 boostedAmount = boostedAmount.add(locks[i].boostedAmount);622 delete locks[i];623 }624 }625 bal.locked = bal.locked.sub(amount);626 bal.boosted = bal.boosted.sub(boostedAmount);627 bal.total = bal.total.sub(amount);628 lockedSupply = lockedSupply.sub(amount);629 boostedSupply = boostedSupply.sub(boostedAmount);630 totalSupply = totalSupply.sub(amount);631
632 stakingToken.safeTransfer(msg.sender, amount);633}
Recommendation:
We advise such validation to be introduced by ensuring that the amount
is non-zero after accumulation.
Alleviation:
The function was refactored entirely and the component that withdraws immediately was omitted, thereby we consider this exhibit inapplicable to the latest iteration of the codebase.
MSR-03M: Potentially Incorrect Multiplier Sanitization
Type | Severity | Location |
---|---|---|
Input Sanitization | Minor | MultiStakingRewards.sol:L117, L123 |
Description:
The linked sanitizations ensure that the multiplier is non-zero, however, if it is less than 1000000
it will result in a multiplier less than 1x
for stakes.
Example:
116function setMaxMultipliers(uint256 _lockedStakeMaxMultiplier) external onlyOwner {117 require(_lockedStakeMaxMultiplier >= 1, "Multiplier must be greater than or equal to 1");118 lockedStakeMaxMultiplier = _lockedStakeMaxMultiplier;119}
Recommendation:
We advise the limit imposed to be equivalent to the 1x
multiplier (1000000
).
Alleviation:
The Parabola team opted not to apply a remediation for this finding in the current iteration of the codebase.