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: 48/99
Findings: 4
Award: $126.10
π Selected for report: 0
π Solo Findings: 0
π 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
when calling transferFrom
or transfer
of an external token a bool indicating success / fail
is returned. Not checking this return value can cause the transaction to proceed even when the transfer has failed.
for example in BathToken._withdraw token.transfer
can fail for some reason and return false, but the withdrawal will succeed.
From the ERC20 docs:
transfer(address recipient, uint256 amount) β bool external Moves amount tokens from the callerβs account to recipient.
Returns a boolean value indicating whether the operation succeeded.
Emits a Transfer event.
and same for transferFrom
and approve
.
An example for such asset is the Basic Attention Token which implements this transfer() and this transferFrom()
Manual Inspection with VSCode.
check the return values of these calls:
token.transferFrom(msg.sender, address(this), amount);
->
require(token.transferFrom(msg.sender, address(this), amount), "transfer failed");
and the same for transfer()
or use the safe
versions of these function which checks the return value
#0 - KenzoAgada
2022-06-05T09:43:42Z
Duplicate of #316.
#1 - bghughes
2022-06-07T22:38:27Z
Duplicate of #316
π Selected for report: xiaoming90
Also found by: GimelSec, IllIllI, MaratCerby, PP1004, WatchPug, berndartmueller, blockdev, ilan
When creating a new offer with offer()
, any ERC20
can be used for the pay_gem
of the offer.
If pay_gem
is a token which takes a fee during the transfer, info.pay_amt
will hold more than the amount which was actually transferred in pay_gem.transferFrom(msg.sender, address(this), pay_amt)
.
This can cause the contract to not have enough funds to transfer when users buy an offer, or when a user cancels his offer.
In
SimpleMarket.offer
info.pay_amt
is set to be the given amount in the parameter.
However, there is no guarantee that the amount which is actually transferred is equal to that.
Example for such tokens is STA or PAXG. Example Implementation
A user can add an offer, and then call cancel
which will fail because the contract does not have enough balance, and the user will lose his tokens.
Manual Inspection with VSCode.
check the balance before and after the transfer, and set info.pay_amt
to be the difference.
#0 - bghughes
2022-06-04T01:16:35Z
Duplicate of #112
π 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.4365 USDC - $52.44
== true
when checking if a boolean expression is satisfied, require(expression)
is enough and there is no need for require(expression == true)
BathHouse.sol#L372 BathToken.sol#L228
The recommended format is:
/** * @dev general explanation * * @param param1 explanation * @param param2 explanation * * @return returnvalname explanation */
Many functions and complex logic are missing informative documentation and comments.
for example, RubicomMarket.getPayAmount is missing a general fucntion doc.
As written in the docs:
This contract represents a single-asset (underlyingToken) liquidity pool that users deposit into, in order to receive Bath Tokens (ERC-20 Token).
The bath token contract should is an ERC20. Even though it can still be used as an ERC20, it
is better practice to have it implement the IERC20
interface or inherit from ERC20
(openzeppelin or as implemented in peripheral_contracts).
BathPair.strategistBootyClaim
is reenterabledoes not have a reentracy guard and is does not follow Checks Effects Interactions pattern.
This can cause the strategist to reenter the code with certain tokens which call the msg.sender
during the transfer, and claim all of the contracts amount of that token.
π 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.8734 USDC - $30.87
require
error messagesLong messages take up a lot of gas. These messages should be short and informative. For example, in BathToken.withdraw
This long message:
"This implementation does not support non-sender owners from withdrawing user shares"
should be shortened, for example to
"Owner-only withdrawal"
Accesing a variable in storage takes al lot of gas, if the same variable is accessed multiple times but is not changed, it should be cached.
For example BathToken.sol#L635, in the loop:
for (uint256 index = 0; index < bonusTokens.length; index++) {
bonusTokens is a storage variable, and every access to it is expensive in gas. Changing the code to:
length = bonusTokens.length for (uint256 index = 0; index < length; index++) {
will save a lot calls to the variable and lots of gas.
Accesing a variable in memory takes al lot of gas, if the same variable is accessed multiple times but is not changed, it should be cached.
For example, BathPair.sol#L582 in the loop:
for (uint256 index = 0; index < ids.length; index++) {
should be
length = ids.length for (uint256 index = 0; index < length; index++) {
postfix is generally more expensive because it must increment a value and return the old value, so it may require holding two numbers in memory. prefix only uses one number in memory.
Example:
for (uint256 index = 0; index < ids.length; index++)
->
for (uint256 index = 0; index < ids.length; ++index)
BathPair.sol#L582
Use calldata instead of memory for external functions where the function argument is read-only. Reading directly from calldata using calldataload instead of going via memory saves the gas from the intermediate memory operations that carry the values.
If a variable is not initialized it is automatically set to the default value (0 for uint, false for bool, address(0) for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Example:
uint256 index = 0
->
uint256 index
BathPair.sol#L582