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: 17/99
Findings: 6
Award: $1,009.01
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: kenzo
Also found by: IllIllI, PP1004, blackscale, hansfriese
By calling RubiconMarket.buy(id, quantity)
(as a consequence of L239 and L241) with
quantity = currentAmount - currentAmount * expectedMarketFeeBPS / 10000
...the fee calculated by the buy function amounts to:
(currentAmount - currentAmount * expectedMarketFeeBPS / 10000) * expectedMarketFeeBPS / 10000 = currentAmount * expectedMarketFeeBPS / 10000 - currentAmount * (expectedMarketFeeBPS / 10000)^2
Adding that fee to the quantity (that the buyer sells, possibly as part of a multihop route) results in:
currentAmount - currentAmount * expectedMarketFeeBPS / 10000 + currentAmount * expectedMarketFeeBPS / 10000 - currentAmount * (expectedMarketFeeBPS / 10000)^2 = currentAmount - currentAmount * (expectedMarketFeeBPS / 10000)^2
...which is the amount that will be spent by the contract from the buyer's wallet, spending over 99.99% of the (fillAmount) output from each loop iteration as the input of the next, and leaving a trail of dust in the buyer's wallet for each intermediary token.
The first token of a route is not affected by this issue because of LL229-230, meaning also that a swap requires the taker to have pay_amt + fee
pay_gems available in his wallet and allowed to the router.
Do not subtract the fee at this stage.
#0 - bghughes
2022-06-07T22:45:13Z
Duplicate of #104
🌟 Selected for report: Ruhum
Also found by: IllIllI, berndartmueller, blackscale, eccentricexit, hansfriese
https://github.com/RubiconDeFi/rubicon-protocol-v1/blob/master/contracts/RubiconRouter.sol#L282
function _swap( uint256 pay_amt, uint256 buy_amt_min,
maxAmount
is a pay_amt denominated in token route[0]
(=pay_gem)
buy_amt_min
is a buy_amt denominated in token route[last]
(=buy_gem)
buy_amt_min * expectedMarketFeeBPS / 1000 is the fee denominated in token `route[last]`(=buy_gem) -- takers pay fees with the token they receive from the swap
Line 282 subtracts a buy_gem amount (the fee) from a pay_gem amount, which makes no sense. Moreover, the fee is taken in pay_gems (from the tokens sent by the taker) so it has no place being mentioned in a buy_amt_min argument.
Set buy_amt_min
argument of _swap to buy_amt_min
that was given as argument to this function (swapEntireBalance
).
#0 - bghughes
2022-06-07T22:46:41Z
Duplicate of #248
#1 - HickupHH3
2022-06-16T14:49:28Z
duplicate of #52
🌟 Selected for report: Ruhum
Also found by: 0x1f8b, 0x4non, 0xDjango, Dravee, GimelSec, StErMi, berndartmueller, blackscale, catchup, cccz, csanuragjain, defsec, eccentricexit, ellahi, horsefacts, hubble, joestakey, kenzo, pedroais, peritoflores, reassor, rotcivegaf, sashik_eth, shenwilly, sseefried, throttle, xiaoming90
1.8534 USDC - $1.85
Judge has assessed an item in Issue #416 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-25T03:31:08Z
RubiconMarket.sol#L1231 This function allows an attacker with a compromised key to set an arbitrarily high fee. The fee will steal an arbitrary amount from buyers (with an infinite allowance) or up to the maximum allowance set for this contract. This is in some cases mitigated by the use of the router, which allows to set a minimum amount to control slippage, but any contract or user that interacts directly with RUbiconMarket remains vulnerable.
Consider bounding feeBPS to a sensible maximum value, or introducing a timelock to allow enough time for detection and reaction in case of wrongdoing.
Dup of #21
🌟 Selected for report: berndartmueller
Also found by: 0x1f8b, blackscale
240.9621 USDC - $240.96
Judge has assessed an item in Issue #416 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-25T03:29:52Z
RubiconMarket.sol#L298 If feeTo is set to 0x0, most ERC20 implementations (e.g. OpenZeppelin's) will revert on this transfer, making offers unbuyable.
dup of #319
🌟 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.074 USDC - $52.07
This report describes low severity issues by file, in their order of appearance in the code. It includes a mix of notes related to best practices and code clarity, and low impact or low probability issues.
The error message is misleading.
The error message is misleading.
If feeTo is set to 0x0, most ERC20 implementations (e.g. OpenZeppelin's) will revert on this transfer, making offers unbuyable.
Market can never be reopened once it has been stopped.
Although this function can not be called during a reentrancy, it is itself reentrant, via _sort
. Consider adding a warning for future dev, so the risk does not compound during composition or evolution of the contract.
This function allows an attacker with a compromised key to set an arbitrarily high fee. The fee will steal an arbitrary amount from buyers (with an infinite allowance) or up to the maximum allowance set for this contract. This is in some cases mitigated by the use of the router, which allows to set a minimum amount to control slippage, but any contract or user that interacts directly with RUbiconMarket remains vulnerable.
Consider bounding feeBPS
to a sensible maximum value, or introducing a timelock to allow enough time for detection and reaction in case of wrongdoing.
This function allows an attacker with a compromised key to immediately divert all future fees to an arbitrary address. When combined with the issue described on RubiconMarket.sol#L1231
, this allows to siphon a large amount of user funds until the changes get noticed and a mitigation solution is found and implemented.
Consider introducing a timelock mechanism to allow enough time for detection and reaction, in order to better protect users.
There is no check on the input not being zero. Considering the issue on RubiconMarket.sol#L298
, such a mistake could have even worst consequences than simply not collecting fees until corrected.
Consider checking the input against the zero address.
The comment is misleading and makes wrong assumptions.
Calling RubiconMarket.sellAllAmount
with a zero minimum amount means that all intermediary swaps of the route bypass the minimum. The rationale is that if the route ends up fulfilling the buy_amt_min requirement of the whole swap, we don't care what happened in between. It would also be very complex to calculate a sensible value for each step.
Will revert if to
is set to zero.
Consider checking to
argument against zero value, and in case replacing it with msg.sender
.
commented-out code should be removed.
🌟 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
31.0028 USDC - $31.00
This report describes opportunity for gas optimization in the order of appearance in the code. It is not an exhaustive list of extreme gas optimization, but a series of quick wins that do not hinder code clarity (in some cases even enhance it), while having a perceptible net gain.
Spend depends on quantity, and this condition is superfluous. Proof below:
as per L280, spend = quantity * buy_amt / pay_amt so spend > buy_amt (L290) is equivalent to: (quantity * buy_amt / pay_amt) > buy_amt quantity / pay_amt > 1 quantity > pay_amt = L289
Being equivalent, there is no case where the condition on L289 evaluates to false and L290 would evaluate to true, so evaluating both is a waste of gas.
Therefore, this condition is redundant and can be removed, sparing a boolean operation, 2 memory reads and a comparison check on most calls to the buy function.
Error message is long and wasteful in terms of gas both at deployment time (contributing to reaching the 24k limit faster) and at runtime.
Recommendation: A good strategy to minimize gas costs while maintaining functionality is to encode error messages in the following way.
The rationale is that these error messages are meant for developers or code to consume, not for end-users, which might require more verbose output anyway, possibly even localized. Such encoding is still human-readable and very useful for developers, while minimizing gas cost and maintaining full functionality in terms of testing and frontend integration.
Long error message, see note at RubiconMarket.sol#L299
Long error message, see note at RubiconMarket.sol#L299
Long error message, see note at RubiconMarket.sol#L299
Long error message, see note at RubiconMarket.sol#L299
Both paths of this function end up calling super.offer(...)
eventually, which is already protected by the synchronized
modifier. This requirement is redundant.
Recommendation: Remove the superfluous requirement to save on gas.
This function calls super.offer(...)
, which is already protected by the synchronized
modifier. This requirement is redundant.
Recommendation: Remove the superfluous requirement to save on gas.
All paths of this function end up calling super.buy(...)
eventually, which is already protected by the synchronized
modifier. This requirement is redundant.
Recommendation: Remove the superfluous requirement to save on gas.
This function calls super.cancel(...)
, which is already protected by the synchronized
modifier. This requirement is redundant.
Recommendation: Remove the superfluous requirement to save on gas.
Message is a duplicate and will be ambiguous regarding its emission source. Consider encoding as described in note RubiconMarket.sol#L299
Message is a duplicate and will be ambiguous regarding its emission source. Consider encoding as described in note RubiconMarket.sol#L299
Message is a duplicate and will be ambiguous regarding its emission source. Consider encoding as described in note RubiconMarket.sol#L299
Message is a duplicate and will be ambiguous regarding its emission source. Consider encoding as described in note RubiconMarket.sol#L299