Platform: Code4rena
Start Date: 04/01/2022
Pot Size: $25,000 USDC
Total HM: 3
Participants: 40
Period: 3 days
Judge: Ivo Georgiev
Total Solo HM: 1
Id: 75
League: ETH
Rank: 5/40
Findings: 3
Award: $939.38
π Selected for report: 0
π Solo Findings: 0
768.967 USDC - $768.97
kenzo
The lock
function calls safeMint
in the midst of it.
This can lead to reentry to updateDistribution
, thereby ruining contract invariants.
At worst case, loss of user funds: distributableXDEFI
will be set to be bigger than it really is, and so when a legitimate user will try to unlock his tokens, the tx will fail because the contract will try sending the user rewards which the contract does not have.
There might be other ways to exploit the reentrancy but I believe this simple example is enough to show that this is a high risk issue.
lock
function safeMints to a user before totalDepositedXDEFI
has been updated. (Code ref)
The user can craft a contract that upon receiving the NFT, will call updateDistribution
.
Since totalDepositedXDEFI
has not been updated yet, _updateXDEFIBalance
will think that the deposit is a reward deposit, not a lock deposit. It will update distributableXDEFI
as if there is XDEFI to distribute: [(Code ref])()
uint256 currentDistributableXDEFI = distributableXDEFI = IERC20(XDEFI).balanceOf(address(this)) - totalDepositedXDEFI;
So when a user will try to withdraw his funds, the contract will try to reward him with funds that are either aren't his or aren't in the contract at all. Additionally, this line might underflow further on.
Here is a simple POC scenario:
updateDistribution
updateDistribution
.Move the safeMint
to the end of lock
function.
Additionally, you can add a nonReenter
modifier on updateDistribution
.
#0 - deluca-mike
2022-01-09T05:28:19Z
Valid and duplicate of #25
π Selected for report: leastwood
Also found by: MaCree, WatchPug, cmichel, egjlmn1, kenzo, onewayfunction, sirhashalot
140.1442 USDC - $140.14
kenzo
Token IDs are defined as concatenation of points, total supply + 1
. The total supply can decrease when merging. This means that the contract might try to mint a token with an ID which already exists.
Under specific circumstances, users won't be able to lock or merge tokens.
The merge
function calls _burn
, which decreases the total supply.
When generating a new NFT, it's id is determined in _generateNewTokenId
using only it's points and current total supply: (Code ref)
function _generateNewTokenId(uint256 points_) internal view returns (uint256 tokenId_) { return (points_ << uint256(128)) + uint128(totalSupply() + 1); }
Since the totalSupply will decrease after merge
has called _burn
, the contract might try to mint an ID which already exists.
Here is one example scenario:
Similarly, the lock function might fail when trying to mint a new token after the supply has decreased.
I've created a test POC for the first example. Add it to XDEFIDistribution.js to see it fails. POC
Probably do not use totalSupply for the IDs, but save a different counter which never decreases and use it.
#0 - deluca-mike
2022-01-09T05:05:55Z
Valid and duplicate of #17
30.2712 USDC - $30.27
kenzo
Since unlock
updates distributableXDEFI
without updating _pointsPerUnit
, if unlock
is called in the gap between when a funder is sending rewards to the contract and between somebody calling updateDistribution
, the rewards will be lost.
Loss of rewards.
updateDistribution
calls _updateXDEFIBalance
, which updates distributableXDEFI
, to calculate the addition needed to _pointsPerUnit
. [(Code ref])(https://github.com/XDeFi-tech/xdefi-distribution/blob/master/contracts/XDEFIDistribution.sol#L147:#L151)
unlock
calls _updateXDEFIBalance
without updating _pointsPerUnit
(rightfully).
But this means that if somebody calls unlock
, after rewards have been sent and before updateDistribution
has been called, the contract will update distributableXDEFI
to be up to date without correspondingly updating _pointsPerUnit
. Therefore the rewards will be lost.
I've created a simple POC to demonstrate this. Insert the test to XDEFIDistribution.js to see it fails. POC
Consider changing updateDistribution() functionality to be atomic - so pull the funds from the sender using transferFrom
and\or using permit.
#0 - deluca-mike
2022-01-09T06:20:14Z
Technically valid, but this was expected, and we disagree with severity, as explained in the duplicate issue #30.