Platform: Code4rena
Start Date: 09/12/2022
Pot Size: $90,500 USDC
Total HM: 35
Participants: 84
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 12
Id: 192
League: ETH
Rank: 35/84
Findings: 3
Award: $326.60
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: unforgiven
Also found by: 0xsomeone, KingNFT, debo, hihen, mookimgo, rotcivegaf, stealthyz, wait
201.2303 USDC - $201.23
Both the contracts of Position and Trading may not work correctly.
The Position.sol#mint calls _safeMint
will trigger a _checkOnERC721Received
callback, which can be used to reenter.
Crackers can use this vulnerability to attack the protocol.
For example, the cracker could send a reentrant attack tx with a call stack like this:
Trading.sol#initiateMarketOrder
Position.sol#mint
Cx#_checkOnERC721Received
Trading.sol#liquidatePosition
(reenter here)Position.sol#burn
Cx#initiateLimitOrder
Position.sol#mint
Cx#_checkOnERC721Received
In step 6, it burned the Position NFT that created in step 3, and the state will be updated in a confusing way because the burnt NFT hasn't done state initialization in step 3 yet. In step 8, it created another Position NFT with the same tokenId as the one in step 3, which is also an anomaly. After step 9, when the stack returns to step 3, the burnt NFT will now go on the state initialization.
Manual
Simplest solution: replace _safeMint
with _mint
.
#0 - c4-judge
2022-12-21T15:10:23Z
GalloDaSballo marked the issue as duplicate of #400
#1 - c4-judge
2023-01-22T17:41:18Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xA5DF
Also found by: 0xA5DF, 0xNazgul, 0xSmartContract, 0xbepresent, 0xdeadbeef0x, 8olidity, Englave, Faith, HE1M, JohnnyTime, Madalad, Mukund, Ruhum, SmartSek, __141345__, aviggiano, carlitox477, cccz, chaduke, francoHacker, gz627, gzeon, hansfriese, hihen, imare, jadezti, kwhuo68, ladboy233, orion, peanuts, philogy, rbserver, wait, yjrwkk
1.1472 USDC - $1.15
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L78-L83 https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L65-L72
All users may lose all their assets in StableVault
Owner can drain all the valuable assets in the StableVault easily and quickly like this:
10^15 * 10^18
Manual
All assets supported by the vault should be fixed at construction time and can not be modified. The owner will not be able to list any new token after the StableVault is deployed. If there is a real need, consider deploying a new StableVault contract.
#0 - GalloDaSballo
2022-12-19T20:39:09Z
Admin Privilege Drain Attack
#1 - c4-judge
2022-12-23T18:04:13Z
GalloDaSballo marked the issue as duplicate of #383
#2 - c4-judge
2023-01-15T14:04:00Z
GalloDaSballo marked the issue as duplicate of #377
#3 - c4-judge
2023-01-22T17:34:10Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: Ruhum
Also found by: Ermaniwe, HollaDieWaldfee, __141345__, rvierdiiev, wait
124.2162 USDC - $124.22
Some users will lose some rewards, and malicious users can use this to gain more rewards.
In function BondNFT.sol#claim(), if the claiming bond is expired, the excess of its rewards is re-distributed to all other users:
function claim( uint _id, address _claimer ) public onlyManager() returns(uint amount, address tigAsset) { Bond memory bond = idToBond(_id); require(_claimer == bond.owner, "!owner"); amount = bond.pending; tigAsset = bond.asset; unchecked { if (bond.expired) { uint _pendingDelta = (bond.shares * accRewardsPerShare[bond.asset][epoch[bond.asset]] / 1e18 - bondPaid[_id][bond.asset]) - (bond.shares * accRewardsPerShare[bond.asset][bond.expireEpoch-1] / 1e18 - bondPaid[_id][bond.asset]); if (totalShares[bond.asset] > 0) { accRewardsPerShare[bond.asset][epoch[bond.asset]] += _pendingDelta*1e18/totalShares[bond.asset]; } } bondPaid[_id][bond.asset] += amount; } IERC20(tigAsset).transfer(manager, amount); emit ClaimFees(tigAsset, amount, _claimer, _id); }
The following table illustrates the states and transitions of an BondNFT:
BH | Action or Info | Bond b1 | Bond b2 | Bond b3 | Bond b4 |
---|---|---|---|---|---|
h0 | init | 100s, 0u, 0c | 100s, 0u, 0c | 100s, 0u, 0c | 0s, 0u, 0c |
h1 | distribute(60r) | 100s, 20u, 0c | 100s, 20u, 0c | 100s, 20u, 0c | 0s, 0u, 0c |
h2 | b1 expired | ||||
h3 | distribute(60r) | 100s, 40u, 0c | 100s, 40u, 0c | 100s, 40u, 0c | 0s, 0u, 0c |
h4 | b2 expired | ||||
h5 | release(b2) | 100s, 40u, 0c | 100s, 0u, 40c | 100s, 40u, 0c | 0s, 0u, 0c |
h6 | create(b4, 100s) | 100s, 40u, 0c | 0s, 0u, 40c | 100s, 40u, 0c | 100s, 0u, 0c |
h7 | claim(b1) | 100s, 0u, 20c | 0s, 0u, 40c | 100s, 46.7u, 0c | 100s, 6.7u, 0c |
Let's use the above table as an example to analyze the problems.
The _pendingDelta
is the rewards to be re-distributed, and the formula is:
accRewardsPerShare[bond.asset][epoch[bond.asset]] += _pendingDelta*1e18/totalShares[bond.asset];
This formula is wrong. Because the totalShares[bond.asset]
includes the expired shares of the claiming bond, which should not be included.
For example, in h7 of the above table claim(b1)
is called:
_pendingDelta = 20u
is re-distributedtotalShares = 300s
(each of b1, b3, b4 has 100s)Thus, accRewardsPerShare
will increase by _pendingDelta/totalShares = 20u/300s = 0.06666667
, which results in the pending reward of b3 and b4 increasing 6.7u each.
This is not correct, the 20u/300s
should be corrected to 20u/(300s-100s)
, subtracting shares of b1 from total shares.
i.e. The h7 should be:
BH | Action or Info | Bond b1 | Bond b2 | Bond b3 | Bond b4 |
---|---|---|---|---|---|
h7 | claim(b1) | 100s, 0u, 20c | 0s, 0u, 40c | 100s, 50u, 0c | 100s, 10u, 0c |
The _pendingDelta
is re-distributed to all shares of the current epoch.
This is not fair for bonds that have been released after the expire time of this claiming bond.
For example, in h7 of the above table claim(b1)
is called. The _pendingDelta = 20u
is re-distributed to b3 and b4.
But in fact it should belong to b2 and b3. Because this _pendingDelta
is generated at h3, when b2's share is still there and has not expired, and b4 has not been created yet.
Manual
The wrong calculation at BondNFT.sol#claim() should be corrected as:
if (totalShares[bond.asset] > bond.shares) { accRewardsPerShare[bond.asset][epoch[bond.asset]] += _pendingDelta*1e18/(totalShares[bond.asset]-bond.shares); }
And for the "unfair re-distributed", there seems to be no easy way to fix it. Maybe a scheme like liquidity mining can be considered.
#0 - GalloDaSballo
2022-12-22T02:02:52Z
Making primary as it has well developed math
#1 - c4-judge
2022-12-22T02:02:56Z
GalloDaSballo marked the issue as primary issue
#2 - c4-judge
2022-12-22T15:24:09Z
GalloDaSballo marked the issue as duplicate of #630
#3 - c4-judge
2023-01-22T09:35:13Z
GalloDaSballo changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-01-22T17:56:34Z
GalloDaSballo marked the issue as satisfactory