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: 12/99
Findings: 6
Award: $1,067.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: hyh
Also found by: 0x29A, 0xNineDec, 0xf15ers, 0xkowloon, GreyArt, IllIllI, KIntern, Kenshin, Lambda, WatchPug, Wayne, berndartmueller, byterocket, cccz, codexploder, horsefacts, kenzo, obront, obtarian, oyc_109, peritoflores, rajatbeladiya, rfa, saian, unforgiven, zer0dot
11.084 USDC - $11.08
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1228-L1232 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L348-L352
The rescueETH()
function uses msg.value
instead of the contract’s balance as the amount to be sent back to the owner, thereby defeating its purpose since it merely sends back whatever ETH was sent with the transaction. No rescue performed at all :(
Exchange fees (in the case of the InfinityExchange
contract) and funds in InfinityStaker
aren’t rescued. I guess you can say that they are locked up till infinity and beyond.
/// @dev used for rescuing exchange fees paid to the contract in ETH function rescueETH(address destination) external onlyOwner { (bool sent, ) = destination.call{value: address(this).balance}(''); require(sent, 'failed'); }
#0 - nneverlander
2022-06-22T11:15:40Z
Duplicate
#1 - nneverlander
2022-07-05T11:41:52Z
#2 - HardlyDifficult
2022-07-09T15:57:35Z
I guess you can say that they are locked up till infinity and beyond.
🤣
#3 - HardlyDifficult
2022-07-09T16:51:54Z
🌟 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/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L300 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L362
Users aren’t refunded any excess ETH funds if they sent more than the total price of the orders. It becomes unfairly taken by the exchange as fees.
Either ensure strict equality
require(msg.value == totalPrice, 'invalid total price');
or refund users the excess funds.
#0 - nneverlander
2022-07-05T12:59:01Z
#1 - HardlyDifficult
2022-07-10T12:40:21Z
276.9248 USDC - $276.92
Funds for later but shorter schedules will be permanently lost when unstaking for vested earlier but longer schedules.
_updateUserStakedAmounts()
fails to account for the case when shorter but later schedules are in the midst of vesting, but the longer ones have been vested. Because the shorter ones will have 0
amount, their state is being overridden to 0
as well, causing funds to be permanently lost.
// Take the 3 and 6 months vestings for instance // 3 months unvested => vestedThreeMonths = 0 // 6 months vested => vestedThreeMonths > 0 // amount = sum of all vested > 0 // both conditions are true, so amounts and timestamps in both durations // get overridden if (amount > vestedThreeMonths) { userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0; userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0; amount = amount - vestedThreeMonths; if (amount > vestedSixMonths) { userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0; userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0; amount = amount - vestedSixMonths;
See the commented POC for details.
it.only('Will cause funds of shorter but later vesting durations to be set to zero', async function () { // step 0: setup await approveERC20(signer1.address, token.address, amountStaked2, signer1, infinityStaker.address); // step 1: stake 6 months await infinityStaker.connect(signer1).stake(amountStaked2.div(2), 2); // step 2: move forward by 4 months, then stake 3 months // so that 6 months duration will vest first // but 3 months duration still in vesting progress await network.provider.send('evm_increaseTime', [120 * DAY]); await network.provider.send('evm_mine', []); await infinityStaker.connect(signer1).stake(amountStaked2.div(2), 1); // step 3: move forward by 2+ months, unstake 6 months duration await network.provider.send('evm_increaseTime', [61 * DAY]); await network.provider.send('evm_mine', []); await infinityStaker.unstake(amountStaked2.div(2)); // verify staked amounts become zero for all durations // user lost amount for 3 month duration let userStakedAmounts = await infinityStaker.userstakedAmounts(signer1.address, 0); expect(userStakedAmounts.amount).to.equal(toBN(0)); userStakedAmounts = await infinityStaker.userstakedAmounts(signer1.address, 1); expect(userStakedAmounts.amount).to.equal(toBN(0)); userStakedAmounts = await infinityStaker.userstakedAmounts(signer1.address, 2); expect(userStakedAmounts.amount).to.equal(toBN(0)); userStakedAmounts = await infinityStaker.userstakedAmounts(signer1.address, 3); expect(userStakedAmounts.amount).to.equal(toBN(0)); });
Hardhat
The if
conditions should also check that the relevant vested amounts are greater than zero.
if (amount > noVesting && noVesting > 0) { ... if (amount > vestedThreeMonths && vestedThreeMonths > 0) { ... if (amount > vestedSixMonths && vestedSixMonths > 0) { ... if (amount > vestedTwelveMonths && vestedTwelveMonths > 0) { ...
#0 - nneverlander
2022-06-22T12:57:38Z
Duplicate
#1 - nneverlander
2022-07-05T11:29:28Z
#2 - HardlyDifficult
2022-07-10T14:56:48Z
🌟 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
It is possible for the owner to set the protocol fee to be above the theoretical maximum fee allowable of BPS = 10_000
.
Should this be done, order executions will be bricked because the protocol fee calculated will exceed the amount sent.
Enforce a maximum fee limit.
require(_protocolFeeBps < 10_000, "cannot exceed BPS");
#0 - HardlyDifficult
2022-07-11T00:05:27Z
🌟 Selected for report: unforgiven
Also found by: GreyArt
625.2993 USDC - $625.30
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L54 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L94 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L138-L140 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L163
The function doItemsIntersect()
only checks if order2Nfts
is a subset of order1Nfts
but it does not ensure that there are no duplicates in the array. For brevity, let’s assume that makerOrder
is [1,2,3,4,5] and manyMakerOrders
is [[1], [1], [1], [1], [1]] in the canExecMatchOneToMany()
function.
Running through the code, numItems
will have a value of 5 and itemsIntersect = itemsIntersect && doItemsIntersect(makerOrder.nfts, manyMakerOrders[i].nfts);
will always be true since [1] is always be an intersect of [1,2,3,4,5]. Assuming _isTimeValid
and _isPriceValid
are both true then canExecMatchOneToMany
will return true but this is not true since you cannot match [1,2,3,4,5] with [[1], [1], [1], [1], [1]].
Note that isOrderValid
will not be able to catch this either because it only checks if the nonce is still valid which is true as the nonce is only updated later in either _execMatchOneMakerSellToManyMakerBuys
or _execMatchOneMakerBuyToManyMakerSells
.
The simplest way to solve this is to sort the orders and loop through to ensure there are no duplicate orders). Moreover, sorting the orders offchain and hashing them can also allow you to save a lot of gas when checking if orders match.
#0 - nneverlander
2022-06-22T18:43:59Z
Solved by checking for buyer != seller and removing ERC1155 support
#1 - nneverlander
2022-07-05T13:00:41Z
🌟 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
49.1477 USDC - $49.15
https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L54-L57 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L119-L121
There doesn't seem to be a use case for the existence of the receive()
 and fallback()
functions. Removing them is recommended as it will prevent accidental ETH transfers to the contract, which will then require the owner to call rescueETH()
, creating needless customer support queries.
Remove the receive()
and fallback()
functions.
#0 - HardlyDifficult
2022-07-12T06:44:58Z
This is valid feedback to help prevent user error. Lowering risk and converting it into a QA report for the warden.
#1 - HardlyDifficult
2022-07-12T06:46:39Z