Platform: Code4rena
Start Date: 14/06/2022
Pot Size: $50,000 USDC
Total HM: 19
Participants: 99
Period: 5 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 136
League: ETH
Rank: 20/99
Findings: 5
Award: $569.40
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: horsefacts
Also found by: 0x29A, GimelSec, GreyArt, Lambda, Ruhum, antonttc, berndartmueller, byterocket, cccz, codexploder, dipp, oyc_109, unforgiven
84.0967 USDC - $84.10
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L326 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L362
The contract allows the user to overpay in ETH. It verifies that msg.value >= totalPrice
instead of checking that it's equal: msg.value == totalPrice
.
Generally, you can say that it's probably a mistake by the buyer if they pay more than necessary. Those funds are also not transferred to the seller. Instead, they are left in the Exchange contract which can be retrieved by the owner with the collected fees.
User is allowed to pay more:
The user only gets the actual sale amount (follow the call stack for this function):
none
Change msg.value >= totalPrice
to msg.value == totalPrice
.
#0 - nneverlander
2022-07-05T13:02:32Z
#1 - HardlyDifficult
2022-07-10T12:40:16Z
276.9248 USDC - $276.92
Following scenario:
Alice has staked X token for 6 months that have vested. She stakes Y tokens for another three months. If she now calls unstake(X)
to take out the tokens that have vested, the Y tokens she staked for three months will be locked up.
First, here's a test showcasing the issue:
describe('should cause trouble', () => { it('should lock up funds', async function () { await approveERC20(signer1.address, token.address, amountStaked, signer1, infinityStaker.address); await infinityStaker.connect(signer1).stake(amountStaked, 2); await network.provider.send("evm_increaseTime", [181 * DAY]); await network.provider.send('evm_mine', []); // The funds we staked for 6 months have vested expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(amountStaked); // Now we want to stake funds for three months await approveERC20(signer1.address, token.address, amountStaked2, signer1, infinityStaker.address); await infinityStaker.connect(signer1).stake(amountStaked2, 1); // total staked is now the funds staked for three & six months // total vested stays the same expect(await infinityStaker.getUserTotalStaked(signer1.address)).to.eq(amountStaked.add(amountStaked2)); expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(amountStaked); // we unstake the funds that are already vested. const userBalanceBefore = await token.balanceOf(signer1.address); await infinityStaker.connect(signer1).unstake(amountStaked); const userBalanceAfter = await token.balanceOf(signer1.address); expect(userBalanceAfter).to.eq(userBalanceBefore.add(amountStaked)); expect(await infinityStaker.getUserTotalStaked(signer1.address)).to.eq(ethers.BigNumber.from(0)); expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(ethers.BigNumber.from(0)); }); });
The test implements the scenario I've described above. In the end, the user got back their amountStaked
tokens with the amountStaked2
tokens being locked up in the contract. The user has no tokens staked at the end.
The issue is in the _updateUserStakedAmounts()
function:
if (amount > noVesting) { userstakedAmounts[user][Duration.NONE].amount = 0; userstakedAmounts[user][Duration.NONE].timestamp = 0; amount = amount - noVesting; if (amount > vestedThreeMonths) { // MAIN ISSUE: // here `vestedThreeMonths` is 0. The current staked tokens are set to `0` and `amount` is decreased by `0`. // Since `vestedThreeMonths` is `0` we shouldn't decrease `userstakedAmounts` at all here. userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0; userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0; amount = amount - vestedThreeMonths; // `amount == vestedSixMonths` so we enter the else block if (amount > vestedSixMonths) { userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0; userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0; amount = amount - vestedSixMonths; if (amount > vestedTwelveMonths) { userstakedAmounts[user][Duration.TWELVE_MONTHS].amount = 0; userstakedAmounts[user][Duration.TWELVE_MONTHS].timestamp = 0; } else { userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount; } } else { // the staked amount is set to `0`. userstakedAmounts[user][Duration.SIX_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.THREE_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.NONE].amount -= amount; }
none
Don't set userstakedAmounts.amount
to 0
if none of its tokens are removed (vestedAmount == 0
)
#0 - nneverlander
2022-06-22T12:56:11Z
Duplicate
#1 - nneverlander
2022-07-05T11:29:32Z
#2 - HardlyDifficult
2022-07-10T14:55:17Z
Switching this submission to be the primary for including PoC code & a clear description of the issue.
When unstaking, unvested tokens may become locked in the contract forever.
Accepting this as a High risk issue.
136.753 USDC - $136.75
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L149 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L202 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L273
The way the gas refunds are computed in the InfinityExchange contract, the first orders pay less than the latter ones. This causes a loss of funds for the buyers whose orders came last in the batch.
The issue is that the startGasPerOrder
variable is computed within the for-loop. That causes the first iterations to be lower than later ones.
Here's an example for the following line: https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L202 To make the math easy we use the following values:
startGas = 1,000,000 gasPerOrder = 100,000 (so fulfilling an order costs us 100,000 gas) ordersLength = 10
For the 2nd order we then get:
startGasPerOrder = 900,000 + ((1,000,000 + 20,000 - 900,000) / 10) startGasPerOrder = 912,000
For the 9th order we get:
startGasPerOrder = 200,000 + ((1,000,000 + 20,000 - 200,000) / 10) startGasPerOrder = 282,000
The startGasPerOrder
variable is passed through a couple of functions without any modification until it reaches a line like this: https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L231
uint256 gasCost = (startGasPerOrder - gasleft() + wethTransferGasUnits) * tx.gasprice;
There, the actual gas costs for the user are computed.
In our case, that would be:
# 2nd order # gasleft() is 800,00 because we said that executing the order costs ~100,000 gas. At the beginning of the order, it was 900,000 so now it's 800,000. This makes the computation a little more straightforward although it's not 100% correct. gasCost = (912,000 - 800,000 + 50,000) * 1 gasCost = 162,000 # 9th order gasCost = (282,000 - 100,000 + 50,000) * 1 gasCost = 232,000
So the 2nd order's buyer pays 162,000
while the 9th order's buyer pays 232,000
.
As I said the math was dumbed down a bit to make it easier. The actual difference might not be as big as shown here. But, there is a difference.
none
The startGasPerOrder
variable should be initialized outside the for-loop.
#0 - nneverlander
2022-06-22T09:33:11Z
Duplicate. Fixed in https://github.com/infinitydotxyz/exchange-contracts-v2/commit/5a3f81b82a9bee2de7517b3a5f18953cb5ec3684
Agree with risk assessment.
#1 - nneverlander
2022-07-05T11:09:26Z
#2 - nneverlander
2022-07-05T11:09:53Z
#3 - nneverlander
2022-07-05T11:11:35Z
#4 - HardlyDifficult
2022-07-09T18:01:46Z
When multiple orders are processed in batch, some users pay more than their expected share of gas costs.
Although the impact may be relatively small values, this appears to be a common path and would result in taking more value than expected from many users during normal usage. Rating this a Medium risk issue as it leaks value impacting users who are not first in a batch transaction.
🌟 Selected for report: WatchPug
Also found by: BowTiedWardens, GreyArt, Ruhum, berndartmueller, cccz, csanuragjain, defsec, joestakey, m9800, peritoflores, reassor, shenwilly, throttle, zer0dot
21.1924 USDC - $21.19
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1267 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1135-L1136
Currently, there's no limit on the InfinityExchange contract's protocol fee. Setting it to a value higher than 100% will make the contract unusable because of an underflow.
There should be a reasonable limit in the setProtocolFee()
function, e.g. 5%.
setProtocolFee()
function with no constraints: https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1267
If the fee is higher than 100%, > 10000
, the following line will trigger an underflow: https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1135-L1136
none
Add a constraint to the setProtocolFee()
function.
#0 - HardlyDifficult
2022-07-11T00:06:13Z
🌟 Selected for report: joestakey
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xkowloon, 0xmint, 8olidity, BowTiedWardens, Chom, Cityscape, Czar102, ElKu, FSchmoede, Funen, GimelSec, GreyArt, IllIllI, KIntern, Kaiziron, Kenshin, Lambda, MadWookie, MiloTruck, PPrieditis, Picodes, Ruhum, Sm4rty, StErMi, TerrierLover, TomJ, Treasure-Seeker, VAD37, WatchPug, Wayne, _Adam, a12jmx, abhinavmir, antonttc, apostle0x01, asutorufos, berndartmueller, cccz, cloudjunky, codexploder, cryptphi, csanuragjain, defsec, delfin454000, fatherOfBlocks, georgypetrov, hake, hansfriese, horsefacts, hyh, k, kenta, nxrblsrpr, oyc_109, peritoflores, rajatbeladiya, reassor, rfa, robee, sach1r0, saian, samruna, shenwilly, simon135, sorrynotsorry, sseefried, throttle, unforgiven, wagmi, zzzitron
50.4365 USDC - $50.44
The staking levels follow a logical order, Bronze -> Silver -> Gold -> Platinum. The updateStakeLevelThreshold()
function responsible for specifying each level's threshold should have constraints to now allow the levels to overlap. Currently, it's possible for the caller to set the threshold for silver lower than that of bronze.
Relevant code:
InfinityStaker.getRageQuitAmounts()
has a require statement that's a tautologyInside the function there's the following require statement:
require(totalStaked >= 0, 'nothing staked to rage quit');
totalStaked
is a uint
meaning it will always be >= 0
. I guess you meant to use > 0
Relevant code:
The owner can freely choose the penalty value. There should be a reasonable limit on it. The user might potentially call ragequit()
without first checking the actual amount of tokens they would receive. They might have a rude awakening.
Relevant code:
There are multiple functions that handle part of a contract's configuration. Those functions should emit events.
Relevant code:
#0 - nneverlander
2022-06-23T11:37:19Z
Duplicate
#1 - HardlyDifficult
2022-07-12T05:13:13Z