Platform: Code4rena
Start Date: 26/07/2022
Pot Size: $75,000 USDC
Total HM: 29
Participants: 179
Period: 6 days
Judge: LSDan
Total Solo HM: 6
Id: 148
League: ETH
Rank: 54/179
Findings: 7
Award: $212.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: cloudjunky
Also found by: 0x1f8b, 0x4non, 0x52, 0xDjango, 0xHarry, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 8olidity, Bnke0x0, CertoraInc, Chom, Deivitto, Dravee, GalloDaSballo, GimelSec, IllIllI, Jmaxmanblue, JohnSmith, Jujic, Kenshin, Krow10, Lambda, MEP, Noah3o6, RedOneN, Ruhum, StErMi, StyxRave, TomJ, Treasure-Seeker, TrungOre, _Adam, __141345__, arcoun, asutorufos, bardamu, bearonbike, bin2chen, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, cccz, codexploder, cryptonue, cryptphi, cthulhu_cult, dharma09, dipp, djxploit, durianSausage, ellahi, giovannidisiena, hansfriese, horsefacts, hyh, immeas, indijanc, jayjonah8, jayphbee, joestakey, kenzo, kyteg, ladboy233, minhquanym, navinavu, obront, oyc_109, peritoflores, rbserver, reassor, rokinot, rotcivegaf, saian, scaraven, shenwilly, simon135, sseefried, teddav, zzzitron
0.0037 USDC - $0.00
Function Basket.withdrawETH()
uses transfer
function to send ETH to address _to
.
This is unsafe as transfer has hard coded gas budget (2300 gas) and can fail when the user is a smart contract.
Manual code review
Using low-level call.value(amount) with the corresponding result check or using the OpenZeppelin Address.sendValue is advised https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L60
#0 - KenzoAgada
2022-08-03T14:03:35Z
Duplicate of #343
🌟 Selected for report: TomJ
Also found by: 0x4non, 0x52, 0xDjango, 0xNazgul, 0xf15ers, 0xsanson, 8olidity, Bnke0x0, CertoraInc, Ch_301, Chom, Dravee, GalloDaSballo, GimelSec, JC, Jujic, Kenshin, Kumpa, Lambda, M0ndoHEHE, PaludoX0, RedOneN, Ruhum, Sm4rty, Treasure-Seeker, TrungOre, Twpony, Waze, _Adam, __141345__, apostle0x01, arcoun, benbaessler, bin2chen, brgltd, cccz, cloudjunky, cryptonue, djxploit, ellahi, erictee, hansfriese, i0001, minhquanym, oyc_109, peritoflores, rbserver, reassor, rokinot, rotcivegaf, saian, shenwilly, sseefried
0.1513 USDC - $0.15
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/core/GolomTrader.sol#L236 https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/core/GolomTrader.sol#L301 https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/core/GolomTrader.sol#L361
In the function fillAsk()
, fillBid()
and fillCriteriaBid()
, ERC721 token is sent and the transferFrom
keyword is used instead of safeTransferFrom
. If receiver
is a contract and is not aware of incoming ERC721 tokens, the sent tokens could be locked forever
Please refer to this report from another contest https://code4rena.com/reports/2022-05-cally#m-09-use-safetransferfrom-instead-of-transferfrom-for-erc721-transfers
Consider changing transferFrom
to safeTransferFrom
.
#0 - KenzoAgada
2022-08-03T15:05:21Z
Duplicate of #342
🌟 Selected for report: sseefried
Also found by: 0x4non, IllIllI, Jmaxmanblue, JohnSmith, Lambda, arcoun, berndartmueller, cccz, csanuragjain, minhquanym, rbserver, rotcivegaf
47.2456 USDC - $47.25
Broke the EIP 721, possible lost of NFTs
If somebody use safeTransferFrom
to send an NFT to a contract, and this contract don't revert the onERC721Received
call and instead of that, reject the NFT by returning a different value of bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
, the NFT may be lost in this contract
Acording the EIP 721 in interface ERC721TokenReceiver, function onERC721Received: "Return of other than the magic value MUST result in the transaction being reverted."
In the safeTransferFrom
function check the return of the IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)
call is equal to bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
to safisfy the EIP-721
function safeTransferFrom( address _from, address _to, uint _tokenId, bytes memory _data ) public { _transferFrom(_from, _to, _tokenId, msg.sender); if (_isContract(_to)) { // Throws if transfer destination is a contract which does not implement 'onERC721Received' try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 retval) { require(retval == IERC721Receiver.onERC721Received.selector, 'ERC721: transfer to non ERC721Receiver implementer'); } catch (bytes memory reason) { if (reason.length == 0) { revert('ERC721: transfer to non ERC721Receiver implementer'); } else { assembly { revert(add(32, reason), mload(reason)) } } } } }
#0 - KenzoAgada
2022-08-04T02:02:00Z
Duplicate of #577
🌟 Selected for report: horsefacts
Also found by: GalloDaSballo, IllIllI, berndartmueller, csanuragjain, hansfriese, kenzo, minhquanym, rotcivegaf
104.014 USDC - $104.01
Users who are approved, but do not own a particular NFT, are supposed to be eligible to call merge and withdraw from the NFT.
Currently _burn()
, used by merge()
and withdraw()
to remove the NFT from the system, will revert unless the sender is the owner of NFT as the function tries to update the accounting for the sender, not the owner.
_removeTokenFrom()
requires _from
to be the NFT owner as it removes _tokenId
from the _from
account:
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/vote-escrow/VoteEscrowCore.sol#L506
function _removeTokenFrom(address _from, uint256 _tokenId) internal { // Throws if `_from` is not the current owner assert(idToOwner[_tokenId] == _from); // Change the owner idToOwner[_tokenId] = address(0); // Update owner token index tracking _removeTokenFromOwnerList(_from, _tokenId); // Change count tracking ownerToNFTokenCount[_from] -= 1; }
_burn()
allows _tokenId
to approved or owner, but calls _removeTokenFrom()
with msg.sender
as _from
:
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/vote-escrow/VoteEscrowCore.sol#L1234
function _burn(uint256 _tokenId) internal { require(_isApprovedOrOwner(msg.sender, _tokenId), 'caller is not owner nor approved'); address owner = ownerOf(_tokenId); // Clear approval approve(address(0), _tokenId); // Remove token _removeTokenFrom(msg.sender, _tokenId); emit Transfer(owner, address(0), _tokenId); }
This way if _burn()
is called by an approved account who isn't an owner, it will revert on _removeTokenFrom()
assert(idToOwner[_tokenId] == _from)
check.
Now _burn()
is used by merge()
:
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/vote-escrow/VoteEscrowCore.sol#L906
function merge(uint256 _from, uint256 _to) external { require(attachments[_from] == 0 && !voted[_from], 'attached'); require(_from != _to); require(_isApprovedOrOwner(msg.sender, _from)); require(_isApprovedOrOwner(msg.sender, _to)); LockedBalance memory _locked0 = locked[_from]; LockedBalance memory _locked1 = locked[_to]; uint256 value0 = uint256(int256(_locked0.amount)); uint256 end = _locked0.end >= _locked1.end ? _locked0.end : _locked1.end; locked[_from] = LockedBalance(0, 0); _checkpoint(_from, _locked0, LockedBalance(0, 0)); _burn(_from); _deposit_for(_to, value0, end, _locked1, DepositType.MERGE_TYPE); }
And withdraw(): https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/vote-escrow/VoteEscrowCore.sol#L1026
function withdraw(uint256 _tokenId) external nonreentrant { assert(_isApprovedOrOwner(msg.sender, _tokenId)); require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); LockedBalance memory _locked = locked[_tokenId]; require(block.timestamp >= _locked.end, "The lock didn't expire"); uint256 value = uint256(int256(_locked.amount)); locked[_tokenId] = LockedBalance(0, 0); uint256 supply_before = supply; supply = supply_before - value; // old_locked can have either expired <= timestamp or zero end // _locked has only 0 end // Both can have >= 0 amount _checkpoint(_tokenId, _locked, LockedBalance(0, 0)); assert(IERC20(token).transfer(msg.sender, value)); // Burn the NFT _burn(_tokenId); emit Withdraw(msg.sender, _tokenId, value, block.timestamp); emit Supply(supply_before, supply_before - value); }
Consider changing _removeTokenFrom()
argument to be the owner
_removeTokenFrom(owner, _tokenId);
#0 - KenzoAgada
2022-08-02T06:04:19Z
Duplicate of #858
🌟 Selected for report: AuditsAreUS
Also found by: 0xSky, CertoraInc, GimelSec, GiveMeTestEther, Green, Lambda, Ruhum, RustyRabbit, Treasure-Seeker, Twpony, arcoun, bin2chen, cccz, codexploder, cryptonue, dipp, horsefacts, jayphbee, joestakey, minhquanym, obront, peritoflores, rbserver, reassor, rotcivegaf, scaraven, ych18
4.5163 USDC - $4.52
In GolomTrader
contract, it is possible for a user filling a signed order of ordertype 0 to accidentally overpay in fillAsk()
Any excess funds paid for in excess of total value needed will be locked in the contract forever since there is no way to recover in the contract
The value of one NFT and other payments value are fixed at the time order is signed and extra payment is set by taker. Hence there is no need to allow to overpay since there will be no benefit.
fillAsk()
allows msg.value > total value needed
require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm');
Consider modifying the check such that the msg.value
is exactly equal to the total value needed. e.g.
require(msg.value == o.totalAmt * amount + p.paymentAmt, 'mgmtm');
#0 - KenzoAgada
2022-08-04T02:50:16Z
Duplicate of #75
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0x52, 0xA5DF, 0xDjango, 0xLovesleep, 0xNazgul, 0xNineDec, 0xSmartContract, 0xackermann, 0xc0ffEE, 0xf15ers, 0xmatt, 0xsanson, 0xsolstars, 8olidity, AuditsAreUS, Bahurum, Bnke0x0, CRYP70, CertoraInc, Ch_301, Chom, CryptoMartian, Deivitto, DevABDee, Dravee, ElKu, Franfran, Funen, GalloDaSballo, GimelSec, GiveMeTestEther, Green, JC, Jmaxmanblue, JohnSmith, Jujic, Junnon, Kenshin, Krow10, Kumpa, Lambda, MEP, Maxime, MiloTruck, Mohandes, NoamYakov, Picodes, RedOneN, Rohan16, Rolezn, Ruhum, RustyRabbit, Sm4rty, Soosh, StErMi, StyxRave, Tadashi, TomJ, Treasure-Seeker, TrungOre, Waze, _Adam, __141345__, ajtra, ak1, apostle0x01, arcoun, asutorufos, async, benbaessler, berndartmueller, bin2chen, brgltd, c3phas, cRat1st0s, carlitox477, chatch, codetilda, codexploder, cryptonue, cryptphi, csanuragjain, cthulhu_cult, delfin454000, dipp, dirk_y, djxploit, ellahi, exd0tpy, fatherOfBlocks, giovannidisiena, hansfriese, horsefacts, hyh, idkwhatimdoing, indijanc, jayfromthe13th, jayphbee, joestakey, kenzo, kyteg, lucacez, luckypanda, mics, minhquanym, obront, oyc_109, pedr02b2, rajatbeladiya, rbserver, reassor, robee, rokinot, rotcivegaf, sach1r0, saian, saneryee, sashik_eth, scaraven, shenwilly, simon135, sseefried, supernova, teddav, ych18, zuhaibmohd, zzzitron
35.1687 USDC - $35.17
Id | Title |
---|---|
1 | Unnecessary fallback and receive function can lock ETH forever |
2 | Implementation is not align with documentation |
3 | Division before multiplication can lead to precision errors |
4 | Typos |
In GolomTrader
, there is no function to recover ETH accidentally sent to the contract. And there is no need for fallback()
and receive()
functions. Consider remove these 2 functions in GolomTrader
Comments in GolomTrader.payEther()
is not matched with the function since payEther()
is used for all the payments in contract, not only royalty.
🌟 Selected for report: JohnSmith
Also found by: 0x1f8b, 0xA5DF, 0xDjango, 0xKitsune, 0xLovesleep, 0xNazgul, 0xSmartContract, 0xmatt, 0xsam, Aymen0909, Bnke0x0, CRYP70, Chandr, Chinmay, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, Funen, GalloDaSballo, Green, IllIllI, JC, Jmaxmanblue, Junnon, Kaiziron, Kenshin, Krow10, Maxime, Migue, MiloTruck, Noah3o6, NoamYakov, Randyyy, RedOneN, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StyxRave, TomJ, Tomio, _Adam, __141345__, ajtra, ak1, apostle0x01, asutorufos, async, benbaessler, brgltd, c3phas, cRat1st0s, carlitox477, delfin454000, djxploit, durianSausage, ellahi, erictee, fatherOfBlocks, gerdusx, gogo, hyh, jayfromthe13th, jayphbee, joestakey, kaden, kenzo, kyteg, ladboy233, lucacez, m_Rassska, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, rfa, robee, rokinot, sach1r0, saian, samruna, sashik_eth, simon135, supernova, tofunmi, zuhaibmohd
21.3211 USDC - $21.32
Id | Title |
---|---|
1 | Caching returned values from function calls can save gas |
2 | Total ETH value is calculated multiple times and should be cached |
3 | Should cache returned values from ve to save gas |
addFee()
External call to other contract is gas consuming and should avoid as much as possible.
For example, in RewardDistributor.addFee()
function, totalSupply()
is called 3 times and balanceOf()
is called 2 times. We should call only 1 time for each and cache the returned values.
Total value of ETH needed to fill an order in fillAsk()
is calculated repeatedly. We can cache this value to save gas.
ve
to save gasIn multiStakerClaim()
function, balanceOfAtNFT()
and totalSupplyAt()
is called repeatedly and should be cached to save gas.