Open sherlock-admin4 opened 4 weeks ago
request poc
Is there a permisionless update functionality?
PoC requested from @etherSky111
Requests remaining: 25
Thanks for judging.
There is a clear issue. https://github.com/sherlock-audit/2024-06-new-scope-judging/issues/473 To test this issue properly, we need to resolve the above issue first.
function getSupplyBalance(DataTypes.PositionBalance storage self, uint256 index) public view returns (uint256 supply) {
- uint256 increase = self.supplyShares.rayMul(index) - self.supplyShares.rayMul(self.lastSupplyLiquidtyIndex);
- return self.supplyShares + increase;
-
+ return self.supplyShares.rayMul(index);
}
Below is test code.
function supplyForUser(address user, uint256 supplyAmount, uint256 tokenId, bool mintNewToken) public {
uint256 mintAmount = supplyAmount;
DataTypes.ExtraData memory data = DataTypes.ExtraData(bytes(''), bytes(''));
INFTPositionManager.AssetOperationParams memory params =
INFTPositionManager.AssetOperationParams(address(tokenA), user, supplyAmount, tokenId, data);
_mintAndApprove(user, tokenA, mintAmount, address(nftPositionManager));
vm.startPrank(user);
if (mintNewToken == true) {
nftPositionManager.mint(address(pool));
}
nftPositionManager.supply(params);
vm.stopPrank();
}
function borrowForUser(address user, uint256 borrowAmount, uint256 tokenId) public {
DataTypes.ExtraData memory data = DataTypes.ExtraData(bytes(''), bytes(''));
INFTPositionManager.AssetOperationParams memory params =
INFTPositionManager.AssetOperationParams(address(tokenA), user, borrowAmount, tokenId, data);
vm.startPrank(user);
nftPositionManager.borrow(params);
vm.stopPrank();
}
function testRewardDistribution() external {
DataTypes.ReserveData memory reserveData_0 = pool.getReserveData(address(tokenA));
console2.log('initial liquidity index => ', reserveData_0.liquidityIndex);
address U1 = address(11);
address U2 = address(12);
address U3 = address(13);
/**
User U1 wants to mint a new NFT (tokenId = 1) and supply 100 ether token
*/
supplyForUser(U1, 100 ether, 1, true);
/**
User U2 wants to mint a new NFT (tokenId = 2) and supply 100 ether token
*/
supplyForUser(U2, 100 ether, 2, true);
bytes32 assetHash = nftPositionManager.assetHash(address(pool), address(tokenA), false);
uint256 balancesOfU1 = nftPositionManager.balanceOfByAssetHash(1, assetHash);
uint256 balancesOfU2 = nftPositionManager.balanceOfByAssetHash(2, assetHash);
console2.log('initial balance of U1 for rewards => ', balancesOfU1);
console2.log('initial balance of U2 for rewards => ', balancesOfU2);
/**
For testing purposes, Alice mints a new NFT (tokenId = 3), supplies 1000 ether, and borrows 600 Ether.
This action increases the pool's liquidity rate to a non-zero value.
*/
supplyForUser(alice, 1000 ether, 3, true);
borrowForUser(alice, 600 ether, 3);
DataTypes.ReserveData memory reserveData_1 = pool.getReserveData(address(tokenA));
console2.log('current liquidity rate => ', reserveData_1.liquidityRate);
/**
Skipping 2000 days is done for testing purposes to increase the liquidity index.
In a real environment, the liquidity index would increase continuously over time.
*/
vm.warp(block.timestamp + 2000 days);
pool.forceUpdateReserve(address(tokenA));
DataTypes.ReserveData memory reserveData_2 = pool.getReserveData(address(tokenA));
console2.log('updated liquidity index => ', reserveData_2.liquidityIndex);
/**
User U2 supplies 100 wei (a dust amount) to trigger an update of the balances for rewards.
*/
supplyForUser(U2, 100, 2, false);
uint256 balancesOfU1Final = nftPositionManager.balanceOfByAssetHash(1, assetHash);
uint256 balancesOfU2Final = nftPositionManager.balanceOfByAssetHash(2, assetHash);
console2.log('final balance of U1 for rewards => ', balancesOfU1Final);
console2.log('final balance of U2 for rewards => ', balancesOfU2Final);
/**
User U3 wants to mint a new NFT (tokenId = 4) and supply 110 ether token
*/
supplyForUser(U3, 110 ether, 4, true);
uint256 balancesOfU3Final = nftPositionManager.balanceOfByAssetHash(4, assetHash);
console2.log('final balance of U3 for rewards => ', balancesOfU3Final);
}
Everything is in below log:
initial liquidity index => 1000000000000000000000000000
initial balance of U1 for rewards => 100000000000000000000
initial balance of U2 for rewards => 100000000000000000000
current liquidity rate => 43490566037735849056603774
updated liquidity index => 1238304471439648487981390542
final balance of U1 for rewards => 100000000000000000000
final balance of U2 for rewards => 123830447143964848898
final balance of U3 for rewards => 110000000000000000000
There is no reward system
that requires users to continuously update their balances
.
How can users realistically update their balances
every second to receive accurate rewards
?
Is this practical?
escalate
This issue does not meet Sherlock's criteria for a medium issue that requires the following:
Causes a loss of funds but requires certain external conditions or specific states, or a loss is highly constrained. The loss of the affected party must exceed 0.01% and 10 USD.
For this issue to cause a 0.01% loss there must be an unrealistic increase in the liquidityIndex in an extremely small 14 day period. The POC provided inflates the liquidity index by borrowing a 60% of the funds at a huge interest rate for 5.5 years, this is absolutely not realistic and will never happen.
escalate
This issue does not meet Sherlock's criteria for a medium issue that requires the following:
Causes a loss of funds but requires certain external conditions or specific states, or a loss is highly constrained. The loss of the affected party must exceed 0.01% and 10 USD.
For this issue to cause a 0.01% loss there must be an unrealistic increase in the liquidityIndex in an extremely small 14 day period. The POC provided inflates the liquidity index by borrowing a 60% of the funds at a huge interest rate for 5.5 years, this is absolutely not realistic and will never happen.
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.
This issue is a high severity bug:
escalate per comment
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.
ether_sky
Medium
The rewards distribution in the NFTPositionManager is unfair
Summary
In
NFTPositionManager
, users candeposit
orborrow
assets
and earnrewards
inzero
. However, the distribution of theserewards
is not working correctly.Vulnerability Detail
Let's consider a
pool
,P
, and anasset
,A
, with a currentliquidity index
of1.5
.U1
andU2
, eachdeposit
100
units ofA
intopool
P
. (Think ofU1
andU2
astoken IDs
.)assetHash(P, A, false)
asH
._supply
function, thebalance
is100
, since it represents theasset amount
, notshares
(as seen inline 57
).57: uint256 balance = pool.getBalance(params.asset, address(this), params.tokenId); _handleSupplies(address(pool), params.asset, params.tokenId, balance); }
Those values would be as below:
Total supply:
totalSupply[H] = 200
Balances:
_balances[U1][H] = _balances[U2][H] = 100
After some time:
The
liquidity index
increases to2
.A new user,
U3
,deposits
110
units ofA
into thepool
P
.U2
makes a smallwithdrawal
of just1 wei
to trigger an update to theirbalance
.Now, the
total supply
and userbalances
forassetHash
H
become:_balances[U1][H] = 100
_balances[U2][H] = 100 ÷ 1.5 × 2 = 133.3
_balances[U3][H] = 110
totalSupply[H] = 343.3
At this point, User
U1
’sasset balance
in thepool P
is the largest, being1 wei
more thanU2
's and23.3
more thanU3
's. Yet,U1
receives the smallestrewards
because theirbalance
was never updated in theNFTPositionManager
. In contrast, UserU2
receives morerewards
due to thebalance
update caused bywithdrawing
just1 wei
.The issue:
This system is unfair because:
U3
, who has fewerassets
in thepool
thanU1
, is receiving morerewards
.rewards
distribution favors users who perform frequent updates (likedeposits
owithdrawals
), which is not equitable.The solution:
Instead of using the
asset balance
as therewards
basis, we should use theshares
in thepool
. Here’s how the updated values would look:_balances[U1][H] = 66.67
_balances[U2][H] = 66.67 - 1 wei
_balances[U3][H] = 110 ÷ 2 = 55
totalSupply[H] = 188.33
This way, the
rewards
distribution becomes fair, as it is based on actual contributions to thepool
.Impact
Code Snippet
https://github.com/sherlock-audit/2024-06-new-scope/blob/c8300e73f4d751796daad3dadbae4d11072b3d79/zerolend-one/contracts/core/positions/NFTPositionManagerSetters.sol#L57-L58
Tool used
Manual Review
Recommendation
The same applies to the
_borrow
,_withdraw
, and_repay
functions.