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: 13/179
Findings: 10
Award: $1,190.39
🌟 Selected for report: 0
🚀 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 function _writeCheckpoint
performs: checkpoints[toTokenId][nCheckpoints - 1];
which, on first checkpoint will always revert as the value nCheckpoints
will be zero
/** * @notice Writes the checkpoint to store current NFTs in the specific block */ 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 is because nCheckpoints
on first checkpoint will always be zero and on solidity >0.8 that will cause a revert
Add a extra case to handle 0 nCheckpoints
.
I've rewritten and ran the following code which seems to work fine
Rewrite to
/** * @notice Writes the checkpoint to store current NFTs in the specific block */ 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; // End Early } } checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); numCheckpoints[toTokenId] = nCheckpoints + 1; }
#0 - KenzoAgada
2022-08-04T13:02:21Z
The delegate
issue is duplicate of #630
The removeDelegation
needs to be deduped later
#1 - zeroexdead
2022-08-28T16:30:00Z
Duplicate of #630 and #419
26.7695 USDC - $26.77
NOTE: This finding requires fixing another bug in the codebase:
delegate
and removeDelegation
are broken due to _writeCheckpoint
underflow"Once the code is fixed as below, (handing of first checkpoint), another vulnerability will emerge
function delegate(uint256 tokenId, uint256 toTokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power'); delegates[tokenId] = toTokenId; uint256 nCheckpoints = numCheckpoints[toTokenId]; if (nCheckpoints > 0) { Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1]; checkpoint.delegatedTokenIds.push(tokenId);
Because delegate
does: checkpoint.delegatedTokenIds.push(tokenId);
then any new delegated token will be added to the list, allowing users to self-delegate, delegate to other people and ultimately to inflate their voting power infinitely.
Note about POC:
The POC requires fixing a bug in _writeCheckpoint
, see my report: "delegate
and removeDelegation
are broken due to _writeCheckpoint
underflow"
To bring further clarity to the POC I've changed _getCurrentDelegated
to be a public function so we can see the internal data structure for delegation
I've also changed _deposit_for
to have the transfer commented to avoid needless setup
Launching 'ganache-cli --accounts 10 --hardfork istanbul --fork https://mainnet.infura.io/v3/uFEqPEkn09U --gasLimit 12000000 --mnemonic brownie --port 8545 --chainId 1'... Brownie environment is ready. ## Deploy Lib >>> t = TokenUriHelper.deploy({"from": a[0]}) Transaction sent: 0x8743a7e9b2fff2e69d14f76fdce12588fb8c5c648f1ee5e6f38227dd4f873320 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 4 TokenUriHelper.constructor confirmed Block: 15246511 Gas used: 2092588 (17.44%) TokenUriHelper deployed at: 0xe0aA552A10d7EC8760Fc6c246D391E698a82dDf9 ## And Deploy Contract >>> x = VoteEscrow.deploy(ETH_ADDRESS, {"from": a[0]}) Transaction sent: 0x094a75c8868c5b94cfa511898953b0802251518b4bd9fbe73855d362c82ce476 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 5 VoteEscrow.constructor confirmed Block: 15246512 Gas used: 3865003 (32.21%) VoteEscrow deployed at: 0x6b4BDe1086912A6Cb24ce3dB43b3466e6c72AFd3 ## Create Lock Id 1 || Edited to require no tokens || Just comment the transfer >>> x.create_lock(123, chain.time() / 1000, {"from": a[0]}) Transaction sent: 0xb58b326e54e9d08079abf3c527882a975d2abd0e755efb1c63f9b191196d40ee Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 6 VoteEscrow.create_lock confirmed Block: 15246513 Gas used: 314838 (2.62%) <Transaction '0xb58b326e54e9d08079abf3c527882a975d2abd0e755efb1c63f9b191196d40ee'> ## Create Lock Id 2 >>> x.create_lock(123, chain.time() / 1000, {"from": a[1]}) Transaction sent: 0x6fe2dbc56b3bd130ccf681415390968f6b5e8a105fe2f856ea4b274f6b1bf286 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 5 VoteEscrow.create_lock confirmed Block: 15246514 Gas used: 261993 (2.18%) <Transaction '0x6fe2dbc56b3bd130ccf681415390968f6b5e8a105fe2f856ea4b274f6b1bf286'> ## Delegate 1 to 2 >>> x.delegate(1, 2, {"from": a[0]}) Transaction sent: 0x3ecbc209b9ad674286288fa4260a3df700241d3d3cc3f9fdda214b8ace4a96b9 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 7 VoteEscrow.delegate confirmed Block: 15246515 Gas used: 134655 (1.12%) <Transaction '0x3ecbc209b9ad674286288fa4260a3df700241d3d3cc3f9fdda214b8ace4a96b9'> ## Delegate 1 to 1 (to itself) >>> x.delegate(1, 1, {"from": a[0]}) Transaction sent: 0xdd9e30473694cc67fd44f98fb2ed3581b3ba335c2148d4f1a2a3c50ec74419b2 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 8 VoteEscrow.delegate confirmed Block: 15246516 Gas used: 119655 (1.00%) <Transaction '0xdd9e30473694cc67fd44f98fb2ed3581b3ba335c2148d4f1a2a3c50ec74419b2'> ## Notice duplicate delegated 1 has delegated to themselves >>> x._getCurrentDelegated(1) (1) ## Notice duplicate delegated 1 has delegated to 2 >>> x._getCurrentDelegated(2) (1) ## Increase lock to get some voting power >>> x.increase_amount(1, 1e18, {"from": a[0]}) Transaction sent: 0xf757aa367cda95901946093468bbd6cdefe1a960e32b008a9a179f6f590a5934 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 12 VoteEscrow.increase_amount confirmed Block: 15246520 Gas used: 195232 (1.63%) <Transaction '0xf757aa367cda95901946093468bbd6cdefe1a960e32b008a9a179f6f590a5934'> ## This is the voting power of 1, all good so far >>> x.getVotes(1) 12367556124863535 ## 2 also received 1s voting power (2 total amount is 123 wei which is negligible) >>> x.getVotes(2) 12367556124863535 ## === Infinite votes POC === ## >>> x.delegate(1, 1, {"from": a[0]}) Transaction sent: 0x1dc8de65a2c931f3308b75233dc039a3fbc3189ab9f0bc25969bdca53b7f01fd Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 13 VoteEscrow.delegate confirmed Block: 15246521 Gas used: 153803 (1.28%) <Transaction '0x1dc8de65a2c931f3308b75233dc039a3fbc3189ab9f0bc25969bdca53b7f01fd'> >>> x.delegate(1, 1, {"from": a[0]}) Transaction sent: 0xbc4a1654db09ed63698f7be1737dde9c1d43a4ebbcf85d5b5b011ff0b2be06ce Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 14 VoteEscrow.delegate confirmed Block: 15246522 Gas used: 175579 (1.46%) <Transaction '0xbc4a1654db09ed63698f7be1737dde9c1d43a4ebbcf85d5b5b011ff0b2be06ce'> >>> x.getVotes(1) ## Voting Power of 1 is tripled 37101645733799250 ## Because 1 has delegated to themselves 3 times as duplicated delegates are pushed to the list >>> x._getCurrentDelegated(1) (1, 1, 1)
I believe that remediation here won't be straightforward and may require plenty of time to avoid additional vulnerabilities. (see other submissions I've sent, unfortunately quite a few)
That said, mitigation to this exact duplication may be achieved by using a Set or simply by checking for duplicates.
To avoid duplicates you may want to use Openzeppelin's EnumerableSet
Which can be used in this way
contract DemoSetContract { // Example setup using EnumerableSet for EnumerableSet.UintSet; mapping(uint256 => EnumerableSet.UintSet) public delegates; // Example usage delegates.add(Id) delegates.remove(Id) }
You may find a duplicate by looping, then use removeElement
to remove the entry from the list
#0 - KenzoAgada
2022-08-02T12:05:17Z
Probably either a duplicate of #169 or #81
#1 - zeroexdead
2022-08-28T16:28:16Z
Duplicate of #169
🌟 Selected for report: CertoraInc
Also found by: 0xA5DF, 0xsanson, Bahurum, GalloDaSballo, MEP, TrungOre, carlitox477, cryptphi, kenzo
Because VoteEscrowDelegation.transferFrom
is performing an external call to removeDelegation
, the msg.sender
will always be address(this)
, causing the function to always revert.
this.removeDelegation(_tokenId);
A simple fix would be to make removeDelegation
public and then changing the code to JUMP to it by removing this
Any call will always revert
<Transaction '0xa7b81478677907a369cea2b73fd410ebf7842d46631689cc66e2ba6a1857736b'> >>> history[-1].call_trace(True) Call trace for '0xa7b81478677907a369cea2b73fd410ebf7842d46631689cc66e2ba6a1857736b': Initial call cost [21940 gas] VoteEscrow.transferFrom 0:386 [527 / 5285 gas] └── VoteEscrowCore.transferFrom 146:386 [26 / 4758 gas] └── VoteEscrow._transferFrom 154:386 [2659 / 4732 gas] │ └── VoteEscrow.removeDelegation [CALL] 229:374 [2073 gas] ├── address: 0x6b4BDe1086912A6Cb24ce3dB43b3466e6c72AFd3 ├── value: 0 ├── input arguments: │ └── tokenId: 1 └── revert reason: VEDelegation: Not allowed
#0 - KenzoAgada
2022-08-02T05:49:53Z
Duplicate of #377
93.2805 USDC - $93.28
NOTE: This finding requires fixing another bug in the codebase:
delegate
and removeDelegation
are broken due to _writeCheckpoint
underflow"removeDelegation
will receive the tokenId of the token to remove the delegation, and it will then remove itself from it's own list of tokens delegated.
/// @notice Remove delegation /// @param tokenId TokenId of which delegation needs to be removed function removeDelegation(uint256 tokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); uint256 nCheckpoints = numCheckpoints[tokenId]; Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1]; removeElement(checkpoint.delegatedTokenIds, tokenId); _writeCheckpoint(tokenId, nCheckpoints, checkpoint.delegatedTokenIds); }
This is fine to remove voting from one-self (if that makes sense)
However this process will not remove the token from any other checkpoint, meaning that any user can create a token of id tokenId
and then delegate it to any number of additional tokens tokenA
, tokenB
, etc.. and all of those will receive delegation, as there seems to be no way to remove the delegation given.
This results in the ability to re-use a token votes by delegating to a myriad of other tokens
## Create a new Lock >> x.create_lock(1e18, chain.time() / 1000, {"from": a[0]}) Transaction sent: 0x8c6b7a8eb6ed92ab4dbfa27c391981da8b8e8c00c583f0a87bc50e55fa625727 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 15 VoteEscrow.create_lock confirmed Block: 15246523 Gas used: 319653 (2.66%) <Transaction '0x8c6b7a8eb6ed92ab4dbfa27c391981da8b8e8c00c583f0a87bc50e55fa625727'> ## Id is 3 >>> history[-1].return_value 3 ## Create a new lock (id will be 4), which we'll delegate to demonstrate the bug >>> x.create_lock(1, chain.time() / 1000, {"from": a[1]}) Transaction sent: 0x9bb3b3f1ae6ae69d2631378cff1ebc2f9022644138a665fc0416d8d101c80e0a Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 6 VoteEscrow.create_lock confirmed Block: 15246524 Gas used: 296193 (2.47%) <Transaction '0x9bb3b3f1ae6ae69d2631378cff1ebc2f9022644138a665fc0416d8d101c80e0a'> ## Delegate to 4 >>> x.delegate(3, 4, {"from": a[0]}) Transaction sent: 0x63be9d38cc19443024c6b138bdb85489f7214d957cd46886f98ac9f51c82861c Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 16 VoteEscrow.delegate confirmed Block: 15246525 Gas used: 134655 (1.12%) <Transaction '0x63be9d38cc19443024c6b138bdb85489f7214d957cd46886f98ac9f51c82861c'> ## Prove we have delegated as expected >>> x._getCurrentDelegated(3) () ## We delegated to 4 as intended >>> x._getCurrentDelegated(4) (3) ## Remove Delegation >>> x.removeDelegation(3, {"from": a[0]}) Transaction sent: 0xfc7ba1a1b194eeba4f24282a623b7932ecc14606b0f3ed73597d42262eea6d4f Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 17 VoteEscrow.removeDelegation confirmed Block: 15246526 Gas used: 66642 (0.56%) <Transaction '0xfc7ba1a1b194eeba4f24282a623b7932ecc14606b0f3ed73597d42262eea6d4f'> ## It doesn't work, our votes are duplicated for 4 now >>> x._getCurrentDelegated(4) (3)
An INCOMPLETE remediation would be to check the list of tokens that have been delegated to via
uint[] listToRemove = _getCurrentDelegated(tokenId)
And then looping over it to remove the token from each of those checkpoints
uint[] listToRemove = _getCurrentDelegated(tokenId) uint length = listToRemove.length for (uint i; i < length; ) { uint256 nCheckpoints = numCheckpoints[tokenId]; // Note: we also do check for 0 just in case Checkpoint storage checkpoint = checkpoints[listToRemove[i]][nCheckpoints > 0 nCheckpoints ? 0]; // Remove our token of `tokenId` from the checkpoint of the token to remove `listToRemove[i]` removeElement(checkpoint.delegatedTokenIds, tokenId); unchecked { ++i; } }
In continuation of the POC you can see how, if we try to remove delegation via 4, we still cannot do that.
Ultimately removeDelegation
in it's current implementation only removes the tokenId from it's own checkpoint, which is not the intended functionality
>>> x.removeDelegation(4, {"from": a[1]}) Transaction sent: 0x4895973bc32687d8e26fbbb35eb81832cb43f65a879a603e4e69f1e41fa49a04 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 7 VoteEscrow.removeDelegation confirmed Block: 15246528 Gas used: 99995 (0.83%) <Transaction '0x4895973bc32687d8e26fbbb35eb81832cb43f65a879a603e4e69f1e41fa49a04'> >>> x._getCurrentDelegated(4) (3)
#0 - zeroexdead
2022-08-15T16:32:58Z
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
Judge has assessed an item in Issue #696 as Medium risk. The relevant finding follows:
L05 - Usage of trasfer over call to send Ether could cause unexpected Reverts https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/core/GolomTrader.sol#L154-L155
payable(payAddress).transfer(payAmt); // royalty transfer to royaltyaddress
The function payEther sends ether via transfer which passes a 2.3k gas stipend to the recipient, this sometimes could not be enough and could cause a Out Of Gas Revert which would prevent users from using the contract.
From my experience transfer is sufficient to send even to a Gnosis Safe, however the contract could break when interacting with other smart contracts
#0 - dmvt
2022-10-21T14:16:19Z
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 code for fillCriteriaBid
and fillBid
is using transferFrom
instead of safeTransferFrom
/// @notice Potentailly Unsafe nftcontract.transferFrom(msg.sender, o.signer, o.tokenId); /// @notice Safe nftcontract.safeTransferFrom(msg.sender, o.signer, o.tokenId, amount, '');
Per EIP-721 all ERC721 will have the function safeTransferFrom
which ensures the receiver can handle the token they are receiving.
Because the standard guarantees the function to be implemented, considering you are already using safeTransferFrom
for ERC1155s, and because safeTransferFrom
performs additional checks for safety to the receiver, it is best to simply change the code to use safeTransferFrom
Change
nftcontract.transferFrom(msg.sender, o.signer, o.tokenId);
nftcontract.safeTransferFrom(msg.sender, o.signer, o.tokenId);
#0 - KenzoAgada
2022-08-04T01:56:31Z
Duplicate of #342
🌟 Selected for report: horsefacts
Also found by: GalloDaSballo, IllIllI, berndartmueller, csanuragjain, hansfriese, kenzo, minhquanym, rotcivegaf
Because VoteEscrowCore.withdraw
calls _burn
the function will eventually try to remove the token from msg.sender
while it belongs to owner
causing a revert
function withdraw(uint256 _tokenId) external nonreentrant { assert(_isApprovedOrOwner(msg.sender, _tokenId));
Rewrite _burn
to:
_removeTokenFrom(owner, _tokenId);
#0 - KenzoAgada
2022-08-02T06:05:40Z
Duplicate of #858
🌟 Selected for report: GimelSec
Also found by: GalloDaSballo, kebabsec, kenzo
NOTE: This finding requires fixing another bug in the codebase, see: "TransferFrom doesn't work because of the external call"
After fixing that bug through a simple remediation, another bug will show in the code, which will break composability of the smart contract.
TL;DR
Due to changes to _transferFrom
the function will only work for transfers but not when a contract or account uses transferFrom
meaning that composability is completely negated.
function _transferFrom( address _from, address _to, uint256 _tokenId, address _sender ) internal override { require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); // remove the delegation // Already fixed per "TransferFrom doesn't work because of the external call" removeDelegation(_tokenId); // Check requirements require(_isApprovedOrOwner(_sender, _tokenId));
_transferFrom
rightfully checks for _isApprovedOrOwner
to perform a transfer, which enables Pull Payments.
However, the overriden version calls removeDelegation
function removeDelegation(uint256 tokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
removeDelegation
will check that the caller is the owner of the token, meaning that transferFrom
will always revert if performed by a contract or a approved
account.
Ensure that delegation accounts approval, instead of ownership exclusively.
Change
require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
To
require(_isApprovedOrOwner(_sender, _tokenId));
#0 - KenzoAgada
2022-08-02T10:45:56Z
Duplicate of #631
🌟 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
167.5763 USDC - $167.58
The codebase leverages a couple ideas very well to minimize smart contracts while allowing for a:
After a thorough review, I would recommend more time is invested in polishing up the Smart Contracts, most importantly the newer contracts such as VoteEscrowDelegation
.
The code would benefit by spending additional time in adding refactorings and further polish both for saving gas and to make the code more maintainable.
At least 30% of the gas cost can be trimmed down fairly easily and the code could benefit by simplifying it further
See below for actionable advice on how to simplify the code and check the Gas Report for how to squeeze more bang for your buck
// if 24 hours have passed since last epoch change if (block.timestamp > startTime + (epoch) * secsInDay) {
Comment says that 24 hours must have passed, however the math for the check will check if the time since the startTime
% secsInDay would bring us to a new epoch, meaning that the change of the epoch can be enacted every day at the same time, however the interval of time between one epoch and another will not necessarily be constant.
In the worst case a epoch could be skipped, or a epoch could last 1 second (or 1 block which would be 13 seconds)
A epoch could last just one block if there has been a day without trades, and at the 23rd hour and 59 minutes, one trade happens.
Epoch would change to Epoch + 1
A minute after another trade could happen, and epochs would change again to Epoch + 2
Either enforce a 24hr delay between each epoch, or update the comment to inform about the actual behaviour
VoteEscrowDelegation._writeCheckpoint
enforces a 500 token limit for delegation, unfortunately this can be used by a malicious attacker to spam with no value tokens.
function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more');
The limit could be used to negate derivative, "liquid lockers" (See bveOXD by BadgerDAO) or delegation based autocompounders. (See Union for vlCVX)
An alternative system, that sums the total voting power of a tokenId, and uses storage mapping for delegation may prove resilient against DOS (due to not looping)
function setMinter(address _minter) external onlyOwner {
Because of the fact that minter can be changed, the owner has the ability to mint as many tokens as they'd like.
A mitigation step of marking setMinter take an extra day has been taken.
However it would be best to lock in the minter
as an immutable variable to ensure only RewardDistributor
can ever mint new tokens, giving stronger security guarantees to end users (and making operations cheaper)
function changeTrader(address _trader) external onlyOwner {
Similarly to setMinter
changeTrader
allows the owner to change the contract that is reporting trades, which indirectly has control over tokens minted, meaning those tokens could be accrued to an arbitrary address.
A mitigation step of marking changeTrader
take an extra day has been taken.
However it would be ideal to set the trader
as immutable to prevent reward math from being changeable in an unfair way
While no funds are at risk, variable shadowing is a strong code-smell as a distracted developer may end up confusing a variable from storage with a function parameter
The following 2 variables are repeatedly declared as local variables but their names clash with storage variables:
tokenId
checkpoint
Consider changing the names to the convention you've already used in VoteEscrowCore
e.g. Change
/// @notice Remove delegation /// @param tokenId TokenId of which delegation needs to be removed function removeDelegation(uint256 tokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); uint256 nCheckpoints = numCheckpoints[tokenId]; Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1]; removeElement(checkpoint.delegatedTokenIds, tokenId); _writeCheckpoint(tokenId, nCheckpoints, checkpoint.delegatedTokenIds); }
To
/// @notice Remove delegation /// @param tokenId TokenId of which delegation needs to be removed function removeDelegation(uint256 _tokenId) external { require(ownerOf(_tokenId) == msg.sender, 'VEDelegation: Not allowed'); uint256 nCheckpoints = numCheckpoints[_tokenId]; Checkpoint storage _checkpoint = checkpoints[_tokenId][nCheckpoints - 1]; removeElement(_checkpoint.delegatedTokenIds, tokenId); _writeCheckpoint(_tokenId, nCheckpoints, _checkpoint.delegatedTokenIds); }
safeTransferFrom
safeTransferFrom
is supposed to check that the receiving contract not only implements the function onERC721Received
but also that the returned value is exactly bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
.`
Implement the exact check per the Spec: https://eips.ethereum.org/EIPS/eip-721
rewardStaker[epoch] = stakerReward; rewardTrader[epoch] = ((tokenToEmit - stakerReward) * 67) / 100; rewardExchange[epoch] = ((tokenToEmit - stakerReward) * 33) / 100;
Due to integer division, minor amounts of dust will be lost and not tracked.
It would be better to perform the divisions and lastly use subtraction to ensure that the remaining amount captures that dust.
There's a 50% change of getting 1 wei of dust on each division (odd number), from my testing there's 98% chance of getting 1 wei on each function call.
See Python Script:
from random import seed from random import random NUMBER_OF_SIMS = 10_000 DAILY_TOKEN_MAX = 600000 """ rewardStaker[epoch] = stakerReward; rewardTrader[epoch] = ((tokenToEmit - stakerReward) * 67) / 100; rewardExchange[epoch] = ((tokenToEmit - stakerReward) * 33) / 100; """ def simulation(): tokenToEmit = int(random() * DAILY_TOKEN_MAX) stakerReward_percent = int(random() * 100) stakerReward = tokenToEmit * stakerReward_percent // 100 rewardTrader = ((tokenToEmit - stakerReward) * 67) // 100 rewardExchange = ((tokenToEmit - stakerReward) * 33) // 100 total = stakerReward + rewardTrader + rewardExchange dust = tokenToEmit - total return dust def main(): counter = 0 for x in range(NUMBER_OF_SIMS): dust = simulation() if dust != 0: counter += 1 print("counter") print(counter)
Running it
Running 'scripts/dust_golom_sim.py::main'... counter 9880
Change the code to do a subtraction from the tokens left
rewardExchange = tokenToEmit - stakerReward - rewardTrader
This will save the cost of a divison (5 gas for DIV
vs 3 gas for SUB
), and will avoid the rounding error
Due to a lack of checks delegate
can be called on an tokenId
that doesn't exist
function delegate(uint256 tokenId, uint256 toTokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
>>> x = VoteEscrow.deploy(ETH_ADDRESS, {"from": a[0]}) >>> x.create_lock(123, chain.time() / 1000, {"from": a[0]}) ## I'm able to delegate to a non existing tokenId >>> x.delegate(1, 12, {"from": a[0]}) Transaction sent: 0x3b76a56fc70dea99aa07226b20511b380b7cb767118cf251244559a9c8d99bb5 Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 8 VoteEscrow.delegate confirmed Block: 15246417 Gas used: 134655 (1.12%)
A simple check for existence would prevent that from happening
require(ownerOf(toTokenId) != address(0), 'VEDelegation: !Token');
trasfer
over call
to send Ether could cause unexpected Revertspayable(payAddress).transfer(payAmt); // royalty transfer to royaltyaddress
The function payEther
sends ether via transfer
which passes a 2.3k gas stipend to the recipient, this sometimes could not be enough and could cause a Out Of Gas Revert which would prevent users from using the contract.
From my experience transfer
is sufficient to send even to a Gnosis Safe, however the contract could break when interacting with other smart contracts
require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm');
GolomTrader.fillAsk
is checking that the msg.value
is sufficient to cover all costs, however the check allows for more ETH than necessary to be sent.
Additionally the >=
check costs more gas.
I'd recommend refactoring the code to require an exact amount, ensuring no dust amounts of ETH are sent to the contract needlessly
Consider instead using constants, this will help make the code easier to maintain
_verifyProof
is marked as public
, however it's name is prefixed by an _
which is used throghout the code to signify internal
/ private
functions
function _verifyProof( uint256 leaf, bytes32 root, bytes32[] memory proof ) public pure {
Change to
function _verifyProof( uint256 leaf, bytes32 root, bytes32[] memory proof ) internal pure {
Or rename the function to verifyProof
removeDelegation
removeDelegation
is meant to reset the delegation for a given tokenId
However the function can be called repeteadly, to no effect beside increasing the amount of checkpoints numCheckpoints
This will increase cost of operation but has no major impact.
If no delegation was given, revert instead of adding more entries to checkpoint[tokenId]
require(signaturesigner == o.signer, 'invalid signature');
Because ecrecover
will return address(0) on failure, any order can be constructed by setting the o.signer as address(0).
Consider reverting when signaturesigner
is address(0)
signaturesigner != address(0)
Because Cryptopunks are not conformant to ERC721, the system will not allow trading them.
While there's no vulnerability here, this may be a wasted opportunity.
An alternative that alleviates this is to use WrappedPunks https://www.wrappedpunks.com/
Document how users can still trade CryptoPunks by wrapping them
Because of a lack of checks for o.signer and receiver, orders can be placed by the same account.
This can be used to produce orders that have no cost beside the fees, as a way to game the liquidity mining program.
Beside gaming fees, which can have negative price impact on the GolomToken, I don't believe this to be cause for any vulnerabilities
Because of the lack of checks for o.totalAmt
, anyone can create orders with zero cost.
This has no practical usage beside the cost of gas and the emission of the event which may allow to display spam orders on a UI
Consider whether it's ok to have 0 value orders, and if you'd want to hide them from your UIs
Most setters don't have an event, making off-chain tracking (and TheGraph) integrations more difficult
setMinter executeSetMinter https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/governance/GolomToken.sol#L56-L72
https://github.com/code-423n4/2022-07-golom/blob/e5efa8f9d6dda92a90b8b2c4902320acf0c26816/contracts/core/GolomTrader.sol#L442-L457 setDistributor executeSetDistributor
Also for one off minting mintGenesisReward mintAirdrop
Throws if _owner
is the zero address. NFTs assigned to the zero address are considered invalid.
Function will return 0 instead of throwing with zero-address
Interestingly the code from CRV will also not throw: https://etherscan.io/address/0x5f3b5dfeb7b28cdbd7faba78963ee202a494e2a2#code#L526
Either delete the comment, or add a require for address(0)
Thought I'd mention that I did check your Merkle Validation and while some variable names have been changed, it ultimately is consistent with how OZ does it.
reward = reward + (rewardExchange[index] * feesExchange[addr][index]) / epochTotalFee[index];
Due to integer division as well as the constant changes between epochTotalFee
and rewardExchange
, minor amounts of dust will accrue in the contract.
It may be worth having a function to sweep the dust, perhaps after all token distribution was zero for a certain amount of days.
My estimate is that there's a 50% chance (even / odd) that 1 wei of dust may be left in the contract, for each division that is done.
Meaning 1 million claims would be needed for 1e6 tokens worth of dust to be left in the contract.
For that reason, it's not economically worth it to track those amounts, but it may still be worth it to sweep it eventually
Also:
Also see GolomTrader:
I have no doubt other warden will submit the fact that the code is using transfer
without a checked return.
rewardToken.transfer(addr, reward)
However I believe that the usage is fine as we know that rewardToken
is GolomToken
which is ERC20Votes which inherits from OZs ERC20 which as you can see will revert on error: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ce0068c21ecd97c6ec8fb0db08570f4b43029dde/contracts/token/ERC20/ERC20.sol#L226
While no vulnerability was found, the functions:
Allow to perform a claim on someone elses behalf.
This can cause issues for the following two cases:
While not a vulnerability, this can cause issues for integrators or gotchas for developers.
We had a similar gotcha at Badger when integrating with Aura Finance: https://github.com/Badger-Finance/vested-aura/commit/475119a678dbc679781ce95edb3bfaff0bf9175f#diff-f8da72ae916d33dc152fcb1f98526aee546d93502123f04280e848bc82e20532R236#L236
// allows sellers of nft to claim there previous epoch rewards
Recommendation: allows sellers of NFT to claim their previous epoch rewards
// allows exchange that facilated the nft trades to claim there previous epoch rewards
Recommendation: Allows the exchange that facilitated the NFT trades to claim their previous epoch rewards
/// @dev allows VeNFT holders to claim there token and eth rewards
Recommendation: allows VeNFT holders to claim their token and eth rewards
/// @dev returns unclaimed golom rewards of a trader
Recommendation: Returns unclaimed Golom rewards for a trader
🌟 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
94.6571 USDC - $94.66
Most contracts can benefit by receiving further gas optimizations.
I roughly estimate that at least 30% of gas cost can be deducted by applying the suggestions below.
All suggestions are sorted by impact and ease of application.
At the bottom I list a set of "usual suspects" which I assume most other wardens will have sent.
Unless otherwise noted, all gas savings are estimates, where benchmarks are provided they are generated via Gas Lab
Variables that never change would be better of set as immutable.
An immutable variable doesn't occupy storage, saving thousands of gas.
This will save a 2.1k gas cost each time they are read the first time and an additional 100 gas for each subsequent read.
Because addFee
is called when orders are settled I highly recommend applying this fix as it will make it at least 6.1k gas cheaper to settle an order.
Make the following variables immutable:
Will save 2.1k for each time a transfer happens (create_lock
, increase_amount
)
The check _isApprovedOrOwner
is meant to help verify if allowance was given.
Because allowance is calculated as a Or statement, an early return on the first true
value would avoid additional reads from Storage.
In the case of the owner performing a transfer, the early return will save up to 6.3k gas (3 less SLOADs from cold slots)
/// @dev Returns whether the given spender can transfer a given token ID /// @param _spender address of the spender to query /// @param _tokenId uint ID of the token to be transferred /// @return bool whether the msg.sender is approved for the given token ID, is an operator of the owner, or is the owner of the token function _isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) { address owner = idToOwner[_tokenId]; bool spenderIsOwner = owner == _spender; if (spenderIsOwner) { return true; } bool spenderIsApproved = _spender == idToApprovals[_tokenId]; if (spenderIsApproved) { return true; } bool spenderIsApprovedForAll = (ownerToOperators[owner])[_spender]; if (spenderIsApprovedForAll) { return true } // This is what peak performance looks like return false }
This can be further optimized to perform the comparisons inside the ifs, however the refactoring showed above will saved 3 orders of magnitude more gas and as such I highly recommend it
isGenesisRewardMinted
and isAirdropMinted
- Up to 35k gas saveduint256 public minterEnableDate;
address public pendingMinter; // Move pendingMinter above so it goes in slot2 uint64 public minterEnableDate; // Change to uint64 it will pack with the above bool public isAirdropMinted; // These will also pack with the above bool public isGenesisRewardMinted; // These will also pack with the above
Resorting Variable Declaration and then setting minterEnableDate
to uint64 will cause it to be packed with
pendingMinter
isAirdropMinted
isGenesisRewardMinted
uint64
represents up to 1.8446744e+19 meaning it will work until the year 2286
Because of this new packing, when setting a new pendingMinter
both the address and the timestamp will be in the same slot.
When changing pendingMinter
from zero to non-zero, that's going to save 20k gas (cost of setting value from zero to non-zero) as the slot will remain non-zero
For similar reasons isAirdropMinted
and isGenesisRewardMinted
will also keep the slot non-zero, triggering further gas savings of up to 15k gas (non-zero to non-zero SLOAD costs 5k gas)
function attach(uint256 _tokenId) external { require(msg.sender == voter); attachments[_tokenId] = attachments[_tokenId] + 1; } function detach(uint256 _tokenId) external { require(msg.sender == voter); attachments[_tokenId] = attachments[_tokenId] - 1; } function merge(uint256 _from, uint256 _to) external { require(attachments[_from] == 0 && !voted[_from], 'attached');
By using 0 as the null value instead of 1, you're triggering gas refunds and then extra costs As any time the value goes from 0 to non-zero, that's 20k gas vs 5k for a non-zero to non-zero (would only happen once)
Checking for a lack of attachment could be changed to:
uint256 cachedAttachment = attachments[_from]; require(cachedAttachment == 0 || cachedAttachment == 1)
Which can be further optimized (perhaps a < 2
), however the bulk of the savings are in not triggering refunds
addFee
- 1200 gasBeside the advised immutable
variables, epoch
which is a storage variable is read 14 times in addFee
Using a memory cached version of oldEpoch = epoch
as well as a new epoch after the increment, will reduce the number of storage reads, removing upwards of 12 * 100 = 1200 gas
filled[hashStruct]
is used in validateOrder
to calculate amountRemaining
(uint256 status, bytes32 hashStruct, uint256 amountRemaining) = validateOrder(o); require(status == 3, 'order not valid'); require(amountRemaining >= amount, 'order already filled'); filled[hashStruct] = filled[hashStruct] + amount;
To save gas later, it may be best to return it from validate order (filledAmount
)
This will allow to save 100 gas later (at the cost of 3 gas), by changing:
filled[hashStruct] = filled[hashStruct] + amount;
To
filled[hashStruct] = filledAmount + amount;
Validation function can be rewritten to pass the value, saves 100 gas per trade
Because epoch
is from storage, each iteration of the loop will costs 100 extra gas to re-load epoch
Using a supporting memory variable will save 94 gas the first time and 97 time each subsequent iteration
Refactor:
for (uint256 index = 0; index < epoch; index++) {
To:
uint256 cachedEpoch = epoch; for (uint256 index = 0; index < cachedEpoch; index++) {
Also see further below for additional minor gas savings (25 / 30 per iteration of loops)
Instances:
Additional functions also check epoch
against the provided parameter, because you'd expect the provided epochs
array to have a length above 1, you'd be better of caching the value of epoch
.
Change
for (uint256 index = 0; index < epochs.length; index++) { require(epochs[index] < epoch);
To
uint256 cachedEpoch = epoch for (uint256 index = 0; index < epochs.length; index++) { require(epochs[index] < cachedEpoch);
Instances:
Below are listed minor gas optimizations that can help make heavily used functions cheaper, consider defaulting to these any time you're writing new code as there's no "drawback" just gas savings
Each iteration of the loop will save the following
Saves 5 gas on going from i++ to ++i
Saves 20 gas via unchecked
Saves 3 gas in skipping the i = 0 default assignment (declaring uint256 i;
is sufficient)
Save another 3 gas per iteration by storing the length of the array in a memory variable (finding length of an array requires 6 gas as you're checking the offset of memory and then storing that value in memory)
uint256 length = proof.length;
This will save even more gas if the values are from storage, however the Storage savings are reported above as they save more gas
for (uint256 i = 0; i < proof.length; i++) {
Grep output below for you to mass replace
rewards/RewardDistributor.sol
142: for (uint256 index = 0; index < epochs.length; index++) { 156: for (uint256 index = 0; index < epochs.length; index++) { 182: for (uint256 index = 0; index < epochs.length; index++) { 225: for (uint256 index = 0; index < epoch; index++) { 257: for (uint256 index = 0; index < epoch; index++) { 272: for (uint256 index = 0; index < epoch; index++) {
rewards/RewardDistributor.sol
179: for (uint256 tindex = 0; tindex < tokenids.length; tindex++) {
vote-escrow/VoteEscrowDelegation.sol
171: for (uint256 index = 0; index < delegated.length; index++) { 189: for (uint256 index = 0; index < delegatednft.length; index++) {
vote-escrow/VoteEscrowDelegation.sol
199: for (uint256 i; i < _array.length; i++) {
vote-escrow/VoteEscrowCore.sol
745: for (uint256 i = 0; i < 255; ++i) { 1044: for (uint256 i = 0; i < 128; ++i) { 1115: for (uint256 i = 0; i < 128; ++i) { 1167: for (uint256 i = 0; i < 255; ++i) {
Memory means the data will be copied from calldata
, and copied to memory
You can skip the overhead by declaring the function external
and the data location to calldata
See benchmark below to prove that this will save around 1k gas per claim
function multiStakerClaim(uint256[] memory tokenids, uint256[] memory epochs) public { // Change to function multiStakerClaim(uint256[] calldata tokenids, uint256[] calldata epochs) external {
addFee
(will save at least 6 gas here)traderClaim
exchangeClaim
multiStakerClaim
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.13; import "../../lib/test.sol"; import "../../lib/Console.sol"; contract GasTest is DSTest { CalldataExternalTest c0; MemoryPublicTest c1; function setUp() public { c0 = new CalldataExternalTest(); c1 = new MemoryPublicTest(); } function testGas() public { uint256[] memory tokenids = new uint256[](5); uint256[] memory epochs = new uint256[](5); c0.multiStakerClaim(tokenids, epochs); c1.multiStakerClaim(tokenids, epochs); } } contract CalldataExternalTest { function multiStakerClaim(uint256[] calldata tokenids, uint256[] calldata epochs) external { // Do nothing } } contract MemoryPublicTest { function multiStakerClaim(uint256[] memory tokenids, uint256[] memory epochs) public { // Do nothing } }
Test result: ok. 1 passed; 0 failed; finished in 3.34ms ╭───────────────────────────────┬─────────────────┬─────┬────────┬─────┬─────────╮ │ CalldataExternalTest contract ┆ ┆ ┆ ┆ ┆ │ ╞═══════════════════════════════╪═════════════════╪═════╪════════╪═════╪═════════╡ │ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ 58511 ┆ 324 ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ multiStakerClaim ┆ 661 ┆ 661 ┆ 661 ┆ 661 ┆ 1 │ ╰───────────────────────────────┴─────────────────┴─────┴────────┴─────┴─────────╯ ╭───────────────────────────┬─────────────────┬──────┬────────┬──────┬─────────╮ │ MemoryPublicTest contract ┆ ┆ ┆ ┆ ┆ │ ╞═══════════════════════════╪═════════════════╪══════╪════════╪══════╪═════════╡ │ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ 79929 ┆ 431 ┆ ┆ ┆ ┆ │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ │ multiStakerClaim ┆ 1689 ┆ 1689 ┆ 1689 ┆ 1689 ┆ 1 │ ╰───────────────────────────┴─────────────────┴──────┴────────┴──────┴─────────╯
Because the data passed to _verifyProof
is calldata
you can change the location for it to calldata
as well, while keeping it internal to save gas that would be used for the copy of data from calldata
to memory
See:
Costly -> public
memory
│ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ verifyProof ┆ 2634 ┆ 2634 ┆ 2634 ┆ 2634 ┆ 1
Fixed -> external
calldata
│ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ verifyProof ┆ 2081 ┆ 2081 ┆ 2081 ┆ 2081 ┆ 1
toString
fully unchecked - 556 gasThe function toString
is based on code written in Solidity 0.4, which didn't use checked math
You can save around 550 gas by simply adding an unchecked block around the whole code
Original Cost:
Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls toString ┆ 2434 ┆ 2434 ┆ 2434 ┆ 2434 ┆ 1
Rewrite to
function toString(uint256 value) external pure returns (string memory) { // Inspired by OraclizeAPI's implementation - MIT license // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol unchecked { if (value == 0) { return '0'; } uint256 temp = value; uint256 digits; while (temp != 0) { digits++; temp /= 10; } bytes memory buffer = new bytes(digits); while (value != 0) { digits -= 1; buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); value /= 10; } return string(buffer); } }
Updated Gas Cost: (Tested over 50k iterations)
│ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │ toString ┆ 602 ┆ 1827 ┆ 1878 ┆ 1878 ┆ 50000