Platform: Code4rena
Start Date: 23/05/2022
Pot Size: $50,000 USDC
Total HM: 44
Participants: 99
Period: 5 days
Judge: hickuphh3
Total Solo HM: 11
Id: 129
League: ETH
Rank: 39/99
Findings: 5
Award: $203.47
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MiloTruck
Also found by: CertoraInc, PP1004, SmartSek, VAD37, WatchPug, berndartmueller, cccz, minhquanym, oyc_109, sorrynotsorry, unforgiven
In BathToken._deposit()
function, it uses underlyingToken balance of address(this) and totalSupply
to calculate shares.
Attacker can transfer a large amount of underlyingToken
into without calling deposit()
, make the share’s price inflated.
Attacker deposit 1 wei to mint 1 share
Attacker transfers huge amount to BathToken
to greatly inflate the share’s price.
Subsequent depositors instead have to deposit an equivalent sum to avoid minting 0 shares. Otherwise, their deposits accrue to the attacker who holds the only share.
Manual Review
Uniswap V2 solved this problem by sending the first 1000 LP tokens to the zero address. The same can be done in this case i.e. when totalSupply() == 0, send the first min liquidity LP tokens to the zero address to enable share dilution.
Ensure the number of shares to be minted is non-zero: require(_shares != 0, "zero shares minted");
Call deposit() once in initialize() to achieve the same effect as the suggestion above.
#0 - bghughes
2022-06-03T23:41:47Z
Duplicate of #323 #128
🌟 Selected for report: berndartmueller
Also found by: 0x1f8b, 0xDjango, 0xsomeone, ACai, Bahurum, BouSalman, CertoraInc, Deivitto, Dravee, GimelSec, IllIllI, JMukesh, Kaiziron, PP1004, Ruhum, SmartSek, VAD37, WatchPug, _Adam, aez121, antonttc, blockdev, broccolirob, camden, cccz, cryptphi, defsec, dipp, ellahi, fatherOfBlocks, gzeon, horsefacts, ilan, jayjonah8, joestakey, kenta, kenzo, minhquanym, oyc_109, pauliax, pedroais, peritoflores, sashik_eth, shenwilly, simon135, throttle, xiaoming90, z3s
0.1022 USDC - $0.10
Judge has assessed an item in Issue #184 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-27T14:06:51Z
🌟 Selected for report: WatchPug
Also found by: Chom, Dravee, Hawkeye, MaratCerby, Ruhum, csanuragjain, fatherOfBlocks, minhquanym
42.6857 USDC - $42.69
Judge has assessed an item in Issue #184 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-27T14:06:09Z
isClosed() is useless when always return false Impact Function isClosed() always return false so it’s useless.
dup of #148
🌟 Selected for report: IllIllI
Also found by: 0x1337, 0x1f8b, 0x4non, 0xDjango, 0xKitsune, 0xNazgul, 0xf15ers, ACai, AlleyCat, Bahurum, BouSalman, CertoraInc, Chom, Dravee, ElKu, FSchmoede, Funen, GimelSec, Hawkeye, JC, JMukesh, Kaiziron, MaratCerby, Metatron, PP1004, Picodes, Ruhum, SmartSek, StErMi, TerrierLover, UVvirus, UnusualTurtle, WatchPug, Waze, _Adam, asutorufos, berndartmueller, blackscale, blockdev, broccolirob, c3phas, catchup, cryptphi, csanuragjain, defsec, delfin454000, dipp, eccentricexit, ellahi, fatherOfBlocks, gzeon, hansfriese, horsefacts, hubble, ilan, joestakey, kebabsec, minhquanym, oyc_109, parashar, pauliax, rotcivegaf, sach1r0, sashik_eth, shenwilly, simon135, sorrynotsorry, sseefried, throttle, unforgiven, xiaoming90
52.0545 USDC - $52.05
stopped
can be set to true but never use anywhereIn RubiconMarket
there is a state stopped
that can be set to true by using function stop()
But this stopped
state is never used anywhere (functions, modifiers, …) else, so it’s useless
https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L480 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L449
Remove state stopped
ExpiringMarket.isClosed()
In ExpiringMarket.isClosed()
function, return variable is bool closed
but in the function, it’s just return false
instead of assigning closed = false
Remove name closed
for return variable or assigning closed = false
inside function.
isOfferSorted()
return true after call _unsort()
RubiconMarket._unsort()
function, we should revmoe offer from the sorted list. The sorted list is the double linked list._unsort
, state prev
and next
of offer id is not deleted.isOfferSorted()
check if next
or prev
not equal 0 then return true even the offer is _unsort()
https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L823 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L1182-L1195
Set prev
and next
of _rank[id]
to 0 in _unsort
function.
isClosed()
is useless when always return false
Function isClosed()
always return false
so it’s useless.
Remove function isClosed()
It is good to add a require() statement that checks the return value of token transfers or to use something like OpenZeppelin’s safeTransfer/safeTransferFrom unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in contract.
https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L353 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L357 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L565 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L602 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L605 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L601 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L615 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L114
Consider using safeTransfer/safeTransferFrom or require() consistently.
underlyingToken
of BathToken
In BathToken
, we add infinite approval of Rubicon Market for underlyingToken
.
BathToken
have a function setMarket()
to set new Rubicon Market address and a function approveMarket()
to add infinite approval
But contract don’t have any function to set approval to zero. So after set new Rubicon Market, old market still have infinite approval and still can use token of BathToken
.
If a attacker has a way to manipulate Rubicon Market, even when team find out early and change to new version, he/she still can take all the funds in BathToken
https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L245-L247 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L260-L262
Set approval of old rubicon market to zero in setMarket()
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xf15ers, 0xkatana, Chom, DavidGialdi, Dravee, ElKu, FSchmoede, Fitraldys, Funen, GimelSec, JC, Kaiziron, MaratCerby, Metatron, MiloTruck, Picodes, Randyyy, RoiEvenHaim, SmartSek, Tomio, UnusualTurtle, WatchPug, Waze, _Adam, antonttc, asutorufos, berndartmueller, blackscale, blockdev, c3phas, catchup, csanuragjain, defsec, delfin454000, ellahi, fatherOfBlocks, gzeon, hansfriese, ilan, joestakey, minhquanym, oyc_109, pauliax, pedroais, reassor, rfa, rotcivegaf, sach1r0, samruna, sashik_eth, simon135, z3s
30.8421 USDC - $30.84
In ERC20.transferFrom()
, we use 2 internal functions _transfer()
and _approve()
, both check address != address(0). This is duplicated and not neccessary.
We are actually check != address(0)
for sender
2 times and 1 time for msg.sender
is not neccessary since msg.sender
will always != address(0).
Similarly in approve()
, increaseAllowance()
and decreaseAllowance()
, we check msg.sender != address(0)
which is not necessary.
https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/ERC20.sol#L269-L270 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/ERC20.sol#L343-L344
Validate input params in public functions instead of internal functions.
In RubiconMarket
, you uses normal linked-list for unsorted order list. And in _hide()
you have to use a while loop through the whole list to find an offer with a given id which is inefficient.
Instead you can use a double linked-list for this, then find an offer with a given id and the previous, next ones will be in O(1).
Use double linked-list in unsorted order book, so remove 1 element from the list will be O(1)