Attacker can claim reward for any user, lead to loss of profit in extreme case
In Distribution
contract, function claim()
is used to claim reward. Anyone can claim reward for other user:
function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {
Pool storage pool = pools[poolId_];
PoolData storage poolData = poolsData[poolId_];
UserData storage userData = usersData[user_][poolId_];
require(block.timestamp > pool.payoutStart + pool.claimLockPeriod, "DS: pool claim is locked");
uint256 currentPoolRate_ = _getCurrentPoolRate(poolId_); //<---
uint256 pendingRewards_ = _getCurrentUserReward(currentPoolRate_, userData);
require(pendingRewards_ > 0, "DS: nothing to claim");
// Update pool data
poolData.lastUpdate = uint128(block.timestamp);
poolData.rate = currentPoolRate_;
// Update user data
userData.rate = currentPoolRate_;
userData.pendingRewards = 0;
// Transfer rewards
L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());
emit UserClaimed(poolId_, user_, pendingRewards_);
}
And pool rate is calculated as below:
function _getCurrentPoolRate(uint256 poolId_) private view returns (uint256) {
PoolData storage poolData = poolsData[poolId_];
if (poolData.totalDeposited == 0) {
return poolData.rate;
}
uint256 rewards_ = getPeriodReward(poolId_, poolData.lastUpdate, uint128(block.timestamp)); //<-----
return poolData.rate + (rewards_ * PRECISION) / poolData.totalDeposited;
}
And function getPeriodReward()
use data from pools
storage to calculate period reward:
function getPeriodReward(uint256 poolId_, uint128 startTime_, uint128 endTime_) public view returns (uint256) {
if (!_poolExists(poolId_)) {
return 0;
}
Pool storage pool = pools[poolId_]; // <----
return
LinearDistributionIntervalDecrease.getPeriodReward( // <---
pool.initialReward,
pool.rewardDecrease,
pool.payoutStart,
pool.decreaseInterval,
startTime_,
endTime_
);
}
And it can be manipulated by owner:
function editPool(uint256 poolId_, Pool calldata pool_) external onlyOwner poolExists(poolId_) {
_validatePool(pool_);
require(pools[poolId_].isPublic == pool_.isPublic, "DS: invalid pool type");
PoolData storage poolData = poolsData[poolId_];
uint256 currentPoolRate_ = _getCurrentPoolRate(poolId_);
// Update pool data
poolData.rate = currentPoolRate_;
poolData.lastUpdate = uint128(block.timestamp);
pools[poolId_] = pool_; // <---
emit PoolEdited(poolId_, pool_);
}
LinearDistributionIntervalDecrease.getPeriodReward()
function:
uint256 firstPeriodReward_ = _calculatePartPeriodReward(
payoutStart_,
startTime_,
interval_,
initialAmount_,
decreaseAmount_,
true
);
uint256 secondPeriodReward_ = _calculateFullPeriodReward(
payoutStart_,
startTime_,
endTime_,
interval_,
initialAmount_,
decreaseAmount_
);
uint256 thirdPeriodReward_ = _calculatePartPeriodReward(
payoutStart_,
endTime_,
interval_,
initialAmount_,
decreaseAmount_,
false
);
return firstPeriodReward_ + secondPeriodReward_ + thirdPeriodReward_;
In both _calculatePartPeriodReward()
and _calculateFullPeriodReward()
function, there is a condition like this:
if (decreaseRewardAmount_ >= initialAmount_) {
return 0;
}
So the impact is user's rate
is updated, but do not have any reward
Consider scenario:
Owner update pools
data of pool id that have benefit for user
Attacker front-running that update, claim rewards for user
User will lost reward that they should have if they claim after update
User will lose reward that they should have
Manual review
Only user can claim reward of themself:
function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {/
+ require(msg.sender == user_);
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.