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: 38/179
Findings: 3
Award: $302.93
🌟 Selected for report: 1
🚀 Solo Findings: 0
246.4353 USDC - $246.44
In VoteEscrowDelegation._writeCheckpoint
, when the checkpoint is overwritten in the same block the new value is set with memory oldCheckpoint
and thus is never written to storage.
Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) { oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; }
Users that remove and delegate a token (or call delegate
on the same token twice) in the same block will only have their first delegation persisted.
tokenId
by calling delegate
.delegate
again which calls _writeCheckpoint
. Since this is the second transaction in the same block the if statement in the code block above executes and stores _delegatedTokenIds
in memory oldCheckpoint
, thus not persisting the array of _delegatedTokenIds
in the checkpoint.Manual analysis
Define the oldCheckpoint
variable as a storage
pointer:
Checkpoint storage oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1];
#0 - 0xA5DF
2022-08-11T09:09:41Z
Just wanna add to the impact (in case the judges consider to decrease severity), in my report of this bug (#625) I've mentioned a more severe impact:
An attacker can use this to multiplying his delegation power endlessly, by adding a delegation and removing it in the same block (using a contract to run those 2 functions in the same tx). The delegation will succeed but the removal will fail, this way each time this runs the user delegates again the same token.
#1 - zeroexdead
2022-09-03T19:07:35Z
🌟 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
In GolomTrader
L178
return (0,...)
will never execute because the preceeding require
statement prevents it.require(signaturesigner == o.signer, 'invalid signature'); if (signaturesigner != o.signer) { return (0, hashStruct, 0); }
Other protocols that wish to integrate with Golom will expect a return of 0 if the orderStatus
is invalid. The natSpec comment for validateOrder
states that "OrderStatus = 0 , if signature is invalid"
. If a protocol wishes to extend or integrate with Golom there may be unintended consequences if they expect a return value of 0 from an invalid signature.
validateOrder
In GolomTrader
L176
ecrecover
function is used to verify the signaturesigner
. Ecrecover is known to be susceptible to signature malleabiliity which could lead to replay attacks. Since the message hash includes a fresh nonce this is not much of a concern. However, consider using OpenZeppelin ECDSA.recover to perform additional checks.In GolomTrader
fillBid
comparison operator bugIn GolomTrader
L286
>=
require( o.totalAmt * amount > (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt) * amount + p.paymentAmt );
OrderCancelled
event can be spammedIn GolomTrader
cancelOrder
there is no check to validate if an order has been previously canceled. This would allow an individual to spam the OrderCancelled
event and increment the filled[hashStruct]
unbounded which may have unintended consequences._settleBalances
inaccurate natSpec commentIn GolomTrader
L370
_settleBalances
states that "@param o the Order struct to be filled must be orderType 1"
. This seems inaccurate because _settleBalances
is called by both fillBid
(order type 1) and fillCriteriaBid
(order type 2).In GolomTrader
L444
setDistributor
and executeSetDistributor
distributor
but no event is emitted to notify downstreams consumers that the distributor
is being changed. Consider adding events to important setter functions to notify users of pending protocol changes.In RewardDistributor
L285
changeTrader
, executeChangeTrader
, addVoteEscrow
and executeAddVoteEscrow
In RewardDistributor
L57
rewardLP
& claimedUpto
In VoteEscrowDelegation
🌟 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
In GolomTrader
The Order
struct can be packed more efficently to reduce storage costs and save gas. Each storage slot in EVM holds a total of 32 bytes so if we reduce the storage size of some variables we can pack the storage slot and save gas on reads from storage.
uint8 orderType - this can be changed to a smaller uint since there are only 3 order types. A uint8 is 1 byte
uint32 nonce - a uint32 has a max value of 4,294,967,295 - 4 bytes, it's doubtful a nonce will ever reach this number
uint48 deadline - a uint48 has a max value of 241,474,976,710,656 - 6bytes, it's doubtful a deadline will surpass this number
struct Order { //@audit can pack this more efficently address collection; // NFT contract address 20 bytes uint256 tokenId; // order for which tokenId of the collection 32 bytes // @audit the 4 variables below add up to 32 bytes and will pack in to a single storage slot. The ordering of variables is important when trying to efficiently pack slots which is why I restructured their declaration ordering. address signer; // maker of order address 20 bytes + uint8 orderType; // 1 byte + uint32 nonce; // nonce of order usefull for cancelling in bulk //4 bytes + uint48 deadline; // timestamp till order is valid epoch timestamp in secs //@audit change to uint48 - max value 241,474,976,710,656 6 bytes + bool isERC721; // standard of the collection , if 721 then true , if 1155 then false //@audit pack with orderType - uint256 orderType; // 0 if selling nft for eth , 1 if offering weth for nft,2 if offering weth for collection with special criteria root //@audit change to uint8 uint256 totalAmt; // price value of the trade // total amt maker is willing to give up per unit of amount Payment exchange; // payment agreed by maker of the order to pay on succesful filling of trade this amt is subtracted from totalamt Payment prePayment; // another payment , can be used for royalty, facilating trades - bool isERC721; // standard of the collection , if 721 then true , if 1155 then false //@audit pack with orderType uint256 tokenAmt; // token amt useful if standard is 1155 if >1 means whole order can be filled tokenAmt times uint256 refererrAmt; // amt to pay to the address that helps in filling your order bytes32 root; // A merkle root derived from each valid tokenId — set to 0 to indicate a collection-level or tokenId-specific order. address reservedAddress; // if not address(0) , only this address can fill the order - uint256 nonce; // nonce of order usefull for cancelling in bulk //@audit uint32 - uint256 deadline; // timestamp till order is valid epoch timestamp in secs //@audit change to uint48 - max value 241,474,976,710,656 6 bytes uint8 v; bytes32 r; bytes32 s; }
In RewardDistributor
startTime
can be set as a constant variable.MAX_SUPPLY = 1000000000 * 10**18
so it's easier to reason about.uint256 tokenToEmit = (dailyEmission * (rewardToken.totalSupply() - rewardToken.balanceOf(address(ve)))) / rewardToken.totalSupply(); uint256 stakerReward = (tokenToEmit * rewardToken.balanceOf(address(ve))) / rewardToken.totalSupply();
Consider storing rewardToken.totalSupply
& rewardToken.balanceOf(address(ve)))
as local variables. This will save gas on two external calls.
In VoteEscrowDelegation
lower
with a 0 value and leaving it uninitialized will save gas.