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: 23/179
Findings: 7
Award: $524.98
๐ Selected for report: 1
๐ Solo Findings: 0
๐ Selected for report: GimelSec
Also found by: 0x52, 0xA5DF, 0xSky, 0xsanson, Bahurum, CertoraInc, GalloDaSballo, JohnSmith, Lambda, MEP, Twpony, arcoun, berndartmueller, cryptphi, hansfriese, kenzo, kyteg, panprog, rajatbeladiya, scaraven, simon135, zzzitron
26.7695 USDC - $26.77
The _writeCheckpoint function will revert if nCheckpoints == 0
because it will try to calculate nCheckpoints - 1
which will revert.
function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) { oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; } else { checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); numCheckpoints[toTokenId] = nCheckpoints + 1; } }
This will cause the delegate function to revert on the first delegation, because the user won't have any checkpoints, which will lead to the first delegation to revert and to the delegation mechanism to not function at all.
Alice tries to delegate the voting power from tokenId 1 to tokenId 2 by calling VoteEscrowDelegation.delegate(1, 2)
. It's the first time that tokenId is delegated to tokenId 2 so numCheckpoints[toTokenId] == 0
.
Now, the delegate
function will call _writeCheckpoint
, which will try to access checkpoints[toTokenId][nCheckpoints - 1]
, but this will make the function revert because nCheckpoints == 0
.
Alice won't be able to delegate her tokenId's votes to another tokenId, and the delegate mechanism simply won't work.
Manual audit (VS Code & my mind)
Access checkpoints[toTokenId][nCheckpoints - 1]
only if nCheckpoints > 0
. This is a possible fix for that issue:
function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); if (nCheckpoints > 0) { Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; if (oldCheckpoint.fromBlock == block.number) { oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; return; } } checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); numCheckpoints[toTokenId] = nCheckpoints + 1; }
#0 - KenzoAgada
2022-08-02T10:54:08Z
Duplicate of #630
๐ Selected for report: CertoraInc
Also found by: 0xA5DF, 0xsanson, Bahurum, GalloDaSballo, MEP, TrungOre, carlitox477, cryptphi, kenzo
365.0893 USDC - $365.09
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/vote-escrow/VoteEscrowDelegation.sol#L242 https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/vote-escrow/VoteEscrowDelegation.sol#L211
The VoteEscrowDelegation._transferFrom
function won't work because it calls this.removeDelegation(_tokenId)
. The removeDelegation
function is external, so when the call is done by this.removeDelegation(_tokenId)
msg.sender changes to the contract address.
This causes the check in the `` function to (most likely) fail because the contract is not the owner of the NFT, and that will make the function revert.
require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
Manual audit (VS Code & my mind)
Make the removeDelegation
function public and call it without changing the context (i.e. without changing msg.sender to the contract's address).
#0 - zeroexdead
2022-09-03T19:10:48Z
93.2805 USDC - $93.28
The removeDelegation
function removes tokenId
from it's own _delegatedTokenIds
array, but the _delegatedTokenIds
contains the NFTs that were delegated to tokenId
. So basically this function removes tokenId
from the array of the tokenId
s that were delegated to him, instead of removing tokenId
from the _delegatedTokenIds
array of the token id that tokenId
was delegated to.
This can cause an attacker delegating his NFT to another NFT he owns, and selling this NFT but still own the votes of it.
An attacker owns two NFTs - letโs assume he owns token ids 1 and 2.
The attacker delegates the votes of token id 1 to token id 2 by calling delegate(1, 2)
.
The attacker now sells the NFT with token id 1 to another user (using the GolomTrader
contract for example).
The sell triggers the transferFrom function, which eventually calls removeDelegation
, but this function will (try to) remove the tokenId from the _delegatedTokenIds
array. This array will contain all the token ids that are delegated to token id 1 (we can assume it is empty), but it wont remove token id 1 from the _delegatedTokenIds
array of token id 2, and that will make the NFT's votes still being delegated to token id 2.
The attacker can use this attack vector to deceive users to buy his NFT and still use its votes.
Manual audit (VS Code & my mind)
Instead of (trying to) remove the transferred token id from its own _delegatedTokenIds
array, remove it from all the arrays of the token ids that it is delegated to.
A way to implement this mechanism can add an array to every token id that will contain the token ids that this token id is delegated to and update it in the delegate
function. That way you can remove the tokenId from all the arrays of the token ids that it is delegated to.
#0 - KenzoAgada
2022-08-02T08:24:06Z
Duplicate of #751
๐ 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
Some contracts can contain some logic in their receive/fallback function, and these kind of contracts won't be able to use the GolomTrader
contract because of the usage of transfer
which limits the gas to 2300.
https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
Manual audit (VS Code & my mind)
Use a low level call instead of the transfer function to transfer ETH to other addresses.
#0 - KenzoAgada
2022-08-03T14:06:08Z
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
The transferFrom
function is used to transfer ERC721
s, but this is unsafe. It can lead for example to an NFT getting stuck in a contract which doesn't have a way to get it out.
OpenZeppelinโs documentation discourages the use of transferFrom(), use safeTransferFrom() whenever possible.
Manual audit (VS Code & my mind)
Use safeTransferFrom
instead of transferFrom
to assure that the only contract that can accept the NFT is a contract that has a way to handle the NFT transfer, and this is indicated by implementing the onERC721Received
function.
#0 - KenzoAgada
2022-08-03T15:20:35Z
Duplicate of #342
๐ 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
A user that sent more ether than he needed to in order to fill an order will lose the excess ether. This can happen if for some reason the total amount in the order is greater than what the user actually needs to pay. In that case the user must assign more ether that will be used to the transaction, but the remaining ether will remain in the contract without a way for the user to get it back.
Manual audit (VS Code & my mind)
Return the excess ether amount to the user in the end of the fillAsk
function
#0 - KenzoAgada
2022-08-04T14:36:37Z
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
GolomToken
it's ^0.8.11
while in the other contracts it is 0.8.11
/// @notice Explain to an end user what this does
removeElement
internal function.MIN_VOTING_POWER_REQUIRED
lower
votes
index
MIN_VOTING_POWER_REQUIRED
or adding/removing delegations).addFee
function.
A fix to that issue is to calculate one of the expressions, and calculate the other one by subtracting it fromrewardTrader[epoch] = ((tokenToEmit - stakerReward) * 67) / 100; rewardExchange[epoch] = ((tokenToEmit - stakerReward) * 33) / 100;
(tokenToEmit - stakerReward)
:
uint remainingRewards = (tokenToEmit - stakerReward); // caching the result to avoid calculating it twice uint rewardTrader = (remainingRewards * 67) / 100; rewardTrader[epoch] = rewardsTrader; // 2/3 of remainingRewards rewardExchange[epoch] = remainingRewards - rewardsTrader; // 1/3 of remainingRewards
addFee
functionstakerRewards
function will return (in the array return value) 0 for epochs that are not claimed and 1 for epochs that are claimed, when it should do the opposite.addFee
function (line 137)_governance != address(0)
payEther
internal functionvalidateOrder
function (line 160)
/// OrderStatus = 1 , if deadline has been
This comment should be "OrderStatus = 1 , if deadline has passed"If the wanted behavior is to revert in the case whererequire(signaturesigner == o.signer, 'invalid signature'); if (signaturesigner != o.signer) { return (0, hashStruct, 0); }
signaturesigner != o.signer
, then lines 178-180 are redundant. If the wanted behavior is to return 0, then line 177 is redundant. In any way, one of this checks is redundant.(o.totalAmt * 50) / 10000)
will be zero when o.totalAmt * 50 < 10000)
, i.e. when o.totalAmt < 200
)o.totalAmt * amount == (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt) * amount + p.paymentAmt