Closed sherlock-admin3 closed 5 months ago
1 comment(s) were left on this issue during the judging contest.
0xmystery commented:
invalid because blockMultiplier, pointsPerBlock pool.allocPoint, totalAllocPoint are universally linked to pointReward
Escalate
I am not disputing that the aforementioned variables are not universally linked. Notice that I mentioned in my explanation that
pointReward has not changed because blockMultiplier, pointsPerBlock pool.allocPoint, totalAllocPoint have not changed at this point
However, whenever _pendingPoints(...)
is called, the accPointsPerShare
will not only be fetched directly from the storage but will now depend on lpSupply
as shown on L377 and considering that blockMultiplier
, pointsPerBlock
, pool.allocPoint
, totalAllocPoint
have not changed (in this scenario), pointReward
remains the same but lpSupply
has reduced, as such the calculated _pendingPoints(...)
for the user will differ from the user.rewardSettled
as shown on L383
365: function _pendingPoints(uint256 _pid, address _user) internal view returns (uint256) {
366: PoolInfo storage pool = poolInfo[_pid];
367: UserInfo storage user = userInfo[_pid][_user];
368:
369: uint256 accPointsPerShare = pool.accPointsPerShare * 1e18; // *1e36
370:
371: uint256 lpSupply = pool.amount;
372: if (getBlockNumber() > pool.lastRewardBlock && lpSupply != 0) {
373: uint256 blockMultiplier = _getBlockMultiplier(pool.lastRewardBlock, getBlockNumber());
374:
375: uint256 pointReward = blockMultiplier * pointsPerBlock * pool.allocPoint / totalAllocPoint;
376:
377: @> accPointsPerShare = pointReward * 1e18 / lpSupply + accPointsPerShare;
378: }
379:
380: // We do some fancy math here. Basically, any point in time, the amount of points
381: // entitled to a user but is pending to be distributed is:
382: // pending reward = (user.amount * pool.accPointsPerShare) - user.rewardDebt
383: @> return user.amount * accPointsPerShare / 1e36 + user.rewardSettled - user.rewardDebt;
Hence the calculation,
user.amount * accPointsPerShare / 1e36 + user.rewardSettled - user.rewardDebt;
// 10e18 * 120e18 * 1e18 / 1e36 + 10000e18
// = 11200e18
And thus, the the discrepancy between the user.rewardSettled and the pending points returned by _pendingPoints(...).
_pendingPoints(uint256 _pid, address _user) - user.rewardSettled
11200e18 - 10000e18 = 1200e18
The _pendingPoints(...)
shows that the user has 1200e18 pints more than he actually does
Escalate
I am not disputing that the aforementioned variables are not universally linked. Notice that I mentioned in my explanation that
pointReward has not changed because blockMultiplier, pointsPerBlock pool.allocPoint, totalAllocPoint have not changed at this point
However, whenever
_pendingPoints(...)
is called, theaccPointsPerShare
will not only be fetched directly from the storage but will now depend onlpSupply
as shown on L377 and considering thatblockMultiplier
,pointsPerBlock
,pool.allocPoint
,totalAllocPoint
have not changed (in this scenario),pointReward
remains the same butlpSupply
has reduced, as such the calculated_pendingPoints(...)
for the user will differ from theuser.rewardSettled
as shown on L383365: function _pendingPoints(uint256 _pid, address _user) internal view returns (uint256) { 366: PoolInfo storage pool = poolInfo[_pid]; 367: UserInfo storage user = userInfo[_pid][_user]; 368: 369: uint256 accPointsPerShare = pool.accPointsPerShare * 1e18; // *1e36 370: 371: uint256 lpSupply = pool.amount; 372: if (getBlockNumber() > pool.lastRewardBlock && lpSupply != 0) { 373: uint256 blockMultiplier = _getBlockMultiplier(pool.lastRewardBlock, getBlockNumber()); 374: 375: uint256 pointReward = blockMultiplier * pointsPerBlock * pool.allocPoint / totalAllocPoint; 376: 377: @> accPointsPerShare = pointReward * 1e18 / lpSupply + accPointsPerShare; 378: } 379: 380: // We do some fancy math here. Basically, any point in time, the amount of points 381: // entitled to a user but is pending to be distributed is: 382: // pending reward = (user.amount * pool.accPointsPerShare) - user.rewardDebt 383: @> return user.amount * accPointsPerShare / 1e36 + user.rewardSettled - user.rewardDebt;
Hence the calculation,
user.amount * accPointsPerShare / 1e36 + user.rewardSettled - user.rewardDebt; // 10e18 * 120e18 * 1e18 / 1e36 + 10000e18 // = 11200e18
And thus, the the discrepancy between the user.rewardSettled and the pending points returned by _pendingPoints(...).
_pendingPoints(uint256 _pid, address _user) - user.rewardSettled 11200e18 - 10000e18 = 1200e18
The
_pendingPoints(...)
shows that the user has 1200e18 pints more than he actually does
You've created a valid escalation!
To remove the escalation from consideration: Delete your comment.
You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.
The following assumption is wrong:
pool.amount = 0
pool.accPointsPerShare = 100e18 points
user.rewardDebt = 0
When pool.amount == 0
, lpSupply
will be 0
too. Hence, pool.accPointsPerShare
remains as 0
.
I agree with the previous comment by the lead judge. Leaving some time for the escalating Watson to provide counterarguments and a fixed scenario.
If nothing is provided, planning to reject the escalation and leave the issue as it is.
Result: Invalid Unique
Audinarey
high
user.rewardSettled
after withdrawal and_pendingPoints(...)
calculation breaks accounting for user 's pending pointsSummary
user.rewardDebt
is the amount of point entitled to an already distributed to a user and recorded.user.rewardSettled
on the other hand is the amount of reward entitled to a used and pending distribution. However, the_pendingPoints(...)
calculation breaks accounting for the protocol due to its discrepancy in the actual reward point earned at the point of withdrawal leading to a potential leak of value as described belowVulnerability Detail
For simplicity, I'll use ETH as the deposit asset Assume that:
pool.amount
= 0pool.accPointsPerShare
= 100e18 pointsuser.rewardDebt
= 0pool.amount
= 100ETHuser.rewardSettled
= 10000e18 anduser.rewardDebt
= 10000e18pool.amount
= 10ETHuser.rewardSettled
= 10000e18 anduser.rewardDebt
= 1000e18 and_pendingPoints(...)
is called for userpool.amount
has reduced to 10ETH,pointReward
has not changed becauseblockMultiplier
,pointsPerBlock
pool.allocPoint
,totalAllocPoint
have not changed at this pointaccPointsPerShare
will increase (pointReward * 1e18 / 10e18 + 100e18
) as shown on L377 to say 120e18As shown below, the the discrepancy between the
user.rewardSettled
and the pending points returned by_pendingPoints(...)
.The user is seen here to have a pending 1200e18 rewards manufactured out of thin air
Impact
This:
Code Snippet
https://github.com/sherlock-audit/2024-05-sophon/blob/main/farming-contracts/contracts/farm/SophonFarming.sol#L574-L595
https://github.com/sherlock-audit/2024-05-sophon/blob/main/farming-contracts/contracts/farm/SophonFarming.sol#L719-L725
https://github.com/sherlock-audit/2024-05-sophon/blob/main/farming-contracts/contracts/farm/SophonFarming.sol#L379-L384
Tool used
Manual Review
Recommendation
A trivial solution is not in sight at this time, however returning the
user.rewardSettled
could just be the solution here whenever_pendingPoints(...)
is called and theblockMultiplier
,pointsPerBlock
pool.allocPoint
,totalAllocPoint
have not changed. Also worhty of note is that decreasing the pool'slpSupply
actually increases theaccPointsPerShare
.