Omniscia Stakewise Audit

RewardEthToken Manual Review Findings

RewardEthToken Manual Review Findings

RET-01M: Circumvention of Checkpointing Mechanism

TypeSeverityLocation
Logical FaultMajorRewardEthToken.sol:L173

Description:

The _updateRewardCheckpoint function assigns the existing rewardPerToken to a user should they have a zero balance, meaning that the checkpointing system can be circumvented to take advantage of a huge delta between newRewardPerToken and cp.rewardPerToken on new accounts, acquiring a disproportionate reward.

Example:

contracts/tokens/RewardEthToken.sol
159function _updateRewardCheckpoint(address account, uint128 newRewardPerToken) internal {
160 Checkpoint memory cp = checkpoints[account];
161 if (newRewardPerToken == cp.rewardPerToken) return;
162
163 uint256 stakedEthAmount;
164 if (account == address(0)) {
165 // fetch merkle distributor current principal
166 stakedEthAmount = stakedEthToken.distributorPrincipal();
167 } else {
168 stakedEthAmount = stakedEthToken.balanceOf(account);
169 }
170 if (stakedEthAmount == 0) {
171 checkpoints[account] = Checkpoint({
172 reward: cp.reward,
173 rewardPerToken: cp.rewardPerToken
174 });
175 } else {
176 uint256 periodRewardPerToken = uint256(newRewardPerToken).sub(cp.rewardPerToken);
177 checkpoints[account] = Checkpoint({
178 reward: _calculateNewReward(cp.reward, stakedEthAmount, periodRewardPerToken).toUint128(),
179 rewardPerToken: newRewardPerToken
180 });
181 }
182}

Recommendation:

We advise the rewardPerToken to be set to the latest one whenever the target account has no ETH staked to ensure such a circumvention is not possible.

Alleviation:

The code now properly introduces a checkpoint of the latest value when the stakedEthAmount is equal to 0, alleviating this exhibit.