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: 33/179
Findings: 5
Award: $382.10
π Selected for report: 1
π 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
https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L154
The use of the deprecated transfer
function for an address will inevitably make the transaction fail when:
The receiver smart contract does not implement a payable receive
function.
The receiver smart contract does implement a payable fallback
function which uses more than 2300 gas unit.
The receiver smart contract implements a payable fallback
/receive
function that needs less than 2300 gas units but is called through proxy, raising the call's gas usage above 2300.
Additionally, using higher than 2300 gas might be mandatory for some multisig wallets.
Use call
instead of transfer
to send ETH. Also, the return value must be checked to see if the call is successful.
#0 - KenzoAgada
2022-08-03T14:03:40Z
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
The transferFrom
function is discouraged by OpenZeppelin. Function safeTransferFrom
will check that recipients are aware of the ERC721 protocol and thereby prevent tokens from being locked forever in contracts that don't provide functionality to transfer them back out.
Simply replace transferFrom
with safeTransferFrom
on line 236.
#0 - KenzoAgada
2022-08-03T15:05:36Z
Duplicate of #342
π Selected for report: sseefried
Also found by: 0x4non, IllIllI, Jmaxmanblue, JohnSmith, Lambda, arcoun, berndartmueller, cccz, csanuragjain, minhquanym, rbserver, rotcivegaf
61.4192 USDC - $61.42
While VoteEscrowCore.safeTransferFrom
does try to call onERC721Received
on the receiver it does not check the for the required "magic bytes" which is IERC721.onERC721received.selector
in this case. See OpenZeppelin docs for more information.
It's quite possible that a call to onERC721Received
could succeed because the contract had a fallback
function implemented, but the contract is not ERC721 compliant.
The impact is that NFT tokens may be sent to non-compliant contracts and lost.
Lines 604 - 605 are:
try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4) {} catch ( bytes memory reason
but they should be:
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason)
Implement safeTransferReturn
so that it checks the required magic bytes: IERC721Receiver.onERC721Received.selector
.
#0 - zeroexdead
2022-09-03T19:06:44Z
π Selected for report: zzzitron
Also found by: GimelSec, GiveMeTestEther, berndartmueller, sseefried
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/core/GolomTrader.sol#L444-L451 https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/rewards/RewardDistributor.sol#L298-L305
The Golom project wisely uses time-locks for changing sensitive values like the distributor and the vote escrow contract. However, it has a small flaw. It is possible, once the address is not address(0)
to set the address to address(0)
. Users may be fooled into thinking this is completely benign.
However, once the time delay of one day has passed, setDistributor
/addVoteEscrow
can be called with a malicious contract address.
The following is for setDistributor
but a similar PoC applies to addVoteEscrow
evilDistributor
be a malicious distributor contractsetDistributor(address(0))
executeSetDistributor()
setDistributor(evilDistributor)
executeSetDistributor
and watch the mayhem unfoldAdd an extra require
in GolomTrader.setDistributor
as follows (and make the analogous change to RewardDistributor.addVoteEscrow
):
function setDistributor(address _distributor) external onlyOwner { if (address(distributor) == address(0)) { distributor = Distributor(_distributor); } else { require(_distributor != address(0)); pendingDistributor = _distributor; distributorEnableDate = block.timestamp + 1 days; } }
#0 - 0xsaruman
2022-08-20T11:42:53Z
#1 - dmvt
2022-10-17T11:07:11Z
duplicate of #698
π 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
RewardDistributor.sol:57 can be remove and you can use the literal value 1 day
. It is guaranteed to be 86400 seconds
Repeated calculations are error prone. What if you change on and forget to change the others? e.g. The percentage calculate of (o.totalAmt * 50) / 10000
here and here and in other places.
The Timelock
contract is saved in a file called Timlock.sol
GolomTrader
has the following comments in payEther
but it's a general purpose function used for all sorts of things.// if royalty has to be paid payable(payAddress).transfer(payAmt); // royalty transfer to royaltyaddress
validateOrder
as follows:function validateOrder(Order calldata o) public view returns ( uint256 /* order status */, bytes32 /* hashed order */, uint256 /* amount remaining to be filled */ )
This avoids having to give them real variable names but makes it really clear what is going on.
Change totalAmt
to totalAmtPerUnit
in struct Order
. The name is confusing as it is.
The comment // require eth amt is sufficient
appears in both fillBid
and fillCriteriaBid
even though these functions don't accept ETH (and also not marked as payable
). Remove this comment.
Lines 1108-1109 in VoteEscrowCore
seems to be relics of the original Vyper implementation.
// Copying and pasting totalSupply code because Vyper cannot pass by // reference yet
RewardDistributor.specs.ts
's test it('should revert if the date is wrong'...
const block = await ethers.provider.getBlock(await ethers.provider.getBlockNumber()); // wait one day await ethers.provider.send("evm_mine", [block.timestamp + 86400]); await expect(golomToken.executeSetMinter()).not.to.be.reverted;
The title of GolomTrader.specs.ts
's test it('should revert if order status is not equal to 3'
seems wrong. Is this meant to test whether filling an order twice fails?
At the bottom of GolomTrader.specs.ts
's test it("should update the filled mapping"...
we have
// TODO: fix hash struct // await expect(golomTrader.filled()).to.be.revertedWith('invalid orderType');
You're not actually testing anything with this test at the moment.
No tests for GolomTrader
's cancelOrder
, fillCriteriaBid
, incrementNonce
and executeSetDistributor
(and stub for fillCriteriaBid
is misspelled as fillCriticalBid
)
No tests in VoterEscrowDelegation.specs.ts
at all
fillAsk
The check-effects-interaction pattern should be followed even when there is re-entrancy protection on the function as it is often the case that not all functions have re-entrancy protection.
The effect Line 269 should be moved to before the interaction, the payEther
call on line 242.
GolemTrader
This is because check on line 217 is
require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm');
But only an amount equal to o.totalAmt * amount + p.paymentAmt
is ever paid out.
o.tokenAmt == type(uint256).max
A maker can create an uncancellable order by setting o.tokenAmt
to type(uint256).max
. This might sound unlikely but it is possible to create a collection that produces 2**256 - 1
combinations pretty easily.
However, an order made with this collection cannot be cancelled because line 315 causes an overflow.
This means that the only way they can cancel the order is to use incrementNonce
but this forces them to recreate and sign all other orders they have with that nonce.
epochTotalFee[epochs[index]]
could be division by zero in RewardsDistributor
Although extremely unlikely, no trading could happen for 24 hours leading to, for a given epoch
, epochTotalFee[epoch] == 0
. This will lead to reversions in functions traderClaim
and exchangeClaim
.
The impact is not high since there would have been no reward for that epoch but it might be a good idea to add an if
statement around the calculation e.g.
if (epochTotalFee[epochs[index]] > 0) { reward = reward + (rewardExchange[epochs[index]] * feesExchange[addr][epochs[index]]) / epochTotalFee[epochs[index]]; }
GolomTrader.validateOrder
The code immediately following the require
on line 177 is dead code as it can't ever evaluate to true.
require(signaturesigner == o.signer, 'invalid signature'); if (signaturesigner != o.signer) { return (0, hashStruct, 0); }