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: 21/179
Findings: 6
Award: $554.22
🌟 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
_writeCheckpoint()
and removeDelegation()
revert because of underflowWhen nCheckpoints
is zero we get an undreflow error because we are trying to substract from nCheckpoints
and since it is of type uint256
, it can't be less than zero.
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L101
File: contracts/vote-escrow/VoteEscrowDelegation.sol 101: Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; 213: Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1];
It is a test, where we try to call delegate(uint256, uint256)
:
it('delegateVotesReverts', async () => { let tenGolomTokens = "10000000000000000000" //10 * 1e18 let oneGolomTokens = "1000000000000000000" //1 * 1e18 let user = accounts[6] let user2 = accounts[7] let userAddress = await user.getAddress() let user2Address = await user2.getAddress() let rewardDistributor1 = accounts[8] await golomToken.connect(governance).setMinter(await rewardDistributor1.getAddress()); await golomToken.connect(governance).executeSetMinter(); await golomToken.connect(rewardDistributor1).mint(userAddress, tenGolomTokens) await golomToken.connect(rewardDistributor1).mint(user2Address, tenGolomTokens) await golomToken.connect(user).approve(voteEscrow.address, oneGolomTokens) await golomToken.connect(user2).approve(voteEscrow.address, oneGolomTokens) let tx = await voteEscrow.connect(user).create_lock(oneGolomTokens, 1000000) let rc = await tx.wait() let event = rc.events?.find(event => event?.event === 'Transfer'); let [from, to, tokenId] = event?.args || []; tx = await voteEscrow.connect(user2).create_lock(oneGolomTokens, 1000000) rc = await tx.wait() event = rc.events?.find(event => event?.event === 'Transfer'); let [from2, to2, tokenId2] = event?.args || []; tx = await voteEscrow.connect(user2).delegate(tokenId2, tokenId) rc = await tx.wait() });
Full test code at https://gist.github.com/johnsmith1623/8f826454986492bbd5b6b6774807b397 We get an error:
Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block) at VoteEscrow._writeCheckpoint (contracts/vote-escrow/VoteEscrowDelegation.sol:101) at VoteEscrow.delegate (contracts/vote-escrow/VoteEscrowDelegation.sol:85)
Check if value is not zero before substracting from it.
#0 - KenzoAgada
2022-08-02T09:07:15Z
Duplicate of #630 (the issue with _writeCheckpoint()
)
The issue with removeDelegation
- needs to be determined if separate issue. Warden hardly mentions it and didn't describe the full impact which another warden identified - undelegated tokens can not be transferred.
#1 - dmvt
2022-10-04T14:53:12Z
IMO removeDelegation is not possible as a result of not being able to delegate in the first place and so doesn't qualify as a separate issue.
Attacker can delegate same tokenId many times in same block and have bigger voting weight
File: contracts/vote-escrow/VoteEscrowDelegation.sol 71: function delegate(uint256 tokenId, uint256 toTokenId) external { 72: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); 73: require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power'); 74: 75: delegates[tokenId] = toTokenId; 76: uint256 nCheckpoints = numCheckpoints[toTokenId]; 77: 78: if (nCheckpoints > 0) { 79: Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1]; 80: checkpoint.delegatedTokenIds.push(tokenId);//@audit what if pushed multiple times same block 81: _writeCheckpoint(toTokenId, nCheckpoints, checkpoint.delegatedTokenIds);
Function _writeCheckpoint(uint256,uint256,uint256[])
with its if check does not prevent anything
101: Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; 102: 103: if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) { 104: oldCheckpoint.delegatedTokenIds = _delegatedTokenIds;//@audit does nothing
It is a test, where we delegate multiple times in one block delegate(uint256, uint256)
:
describe("delegation", () => { it('delegateVotesAttack', async () => { let tenGolomTokens = "10000000000000000000" //10 * 1e18 let oneGolomTokens = "1000000000000000000" //1 * 1e18 let attacker = accounts[6] let attacker2 = accounts[7] let attackerAddress = await attacker.getAddress() let attacker2Address = await attacker2.getAddress() let rewardDistributor1 = accounts[8] await golomToken.connect(governance).setMinter(await rewardDistributor1.getAddress()); await golomToken.connect(governance).executeSetMinter(); await golomToken.connect(rewardDistributor1).mint(attackerAddress, tenGolomTokens) await golomToken.connect(rewardDistributor1).mint(attacker2Address, tenGolomTokens) await golomToken.connect(attacker).approve(voteEscrow.address, oneGolomTokens) await golomToken.connect(attacker2).approve(voteEscrow.address, oneGolomTokens) let tx = await voteEscrow.connect(attacker).create_lock(oneGolomTokens, 1000000) let rc = await tx.wait() let event = rc.events?.find(event => event?.event === 'Transfer'); let [from, to, tokenId] = event?.args || []; tx = await voteEscrow.connect(attacker2).create_lock(oneGolomTokens, 1000000) rc = await tx.wait() event = rc.events?.find(event => event?.event === 'Transfer'); let [from2, to2, tokenId2] = event?.args || []; voteEscrow.connect(attacker2).delegate(tokenId2, tokenId) voteEscrow.connect(attacker2).delegate(tokenId2, tokenId) tx = await voteEscrow.connect(attacker2).delegate(tokenId2, tokenId) rc = await tx.wait() let votes = await voteEscrow.connect(attacker).getVotes(tokenId) console.log("votes: " + votes) });
Full test code at https://gist.github.com/johnsmith1623/8f826454986492bbd5b6b6774807b397 You may have to fix underflow error before trying to test it. User should not be able to increase voting weight at same checkpoint in such way
Check if it is not there already before pushing it.
#0 - KenzoAgada
2022-08-02T10:55:18Z
Duplicate of #455 as the warden mentions "same block"
🌟 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
Usage of deprecated transfer
function can revert.
File: contracts/core/GolomTrader.sol 154: payable(payAddress).transfer(payAmt); // royalty transfer to royaltyaddress
The original transfer
used to send eth uses a fixed stipend 2300 gas. This was used to prevent reentrancy. However this limit your protocol to interact with others contracts that need more than that to process the transaction.
A good article about that: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/.
Used call instead. For example
(bool success, ) = payable(payAddress).call{value: payAmt}(""); require(success, "Transfer failed.");
#0 - KenzoAgada
2022-08-03T14:04:39Z
Duplicate of #343
🌟 Selected for report: sseefried
Also found by: 0x4non, IllIllI, Jmaxmanblue, JohnSmith, Lambda, arcoun, berndartmueller, cccz, csanuragjain, minhquanym, rbserver, rotcivegaf
By standard "Return of other than the magic value MUST result in the transaction being reverted." But value is not schecked and there is no revert if wrong value returned. Other contract creators rely on this standard and can expect that caller will revert transaction if they don't return the magic value.
File: contracts/vote-escrow/VoteEscrowCore.sol 588: /// If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if 589: /// the return value is not `bytes4(keccak256("onERC721Received(address,address,uint,bytes)"))`. 604: try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4) {} catch ( 605: bytes memory reason 606: ) {
Add check for returned value like this:
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) {
#0 - KenzoAgada
2022-08-04T02:01:34Z
Duplicate of #577
🌟 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
Issue | Instances | |
---|---|---|
1 | Timelock is too short | 3 |
2 | Zero-address checks are missing | 17 |
3 | Use of floating pragma | 1 |
4 | Events not emitted for important state changes | 11 |
5 | Return values of transfer()/transferFrom() not checked | 6 |
6 | Unused/empty receive() and fallback() functions | 4 |
7 | abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() | 1 |
8 | Use of assert() instead of require() | 13 |
9 | Shadowing State Variables | 2 |
10 | Hardcoded contract address | 1 |
11 | Misleading comment in function comments | 2 |
12 | _writeCheckpoint() stores array to memory | 1 |
13 | Dependencies are outdated | 1 |
14 | Add a timelock to critical functions | 1 |
15 | Set sane maximum for input parameter | 1 |
16 | Non-exploitable(yet) reentrancy | 1 |
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious Manager making a frontrunning/sandwich attack on the fees). Timelock should be at least three days to cover weekends. A lot of people do not work on weekends, and will not be able to react to changes in time.
File: contracts/governance/GolomToken.sol 58: function setMinter(address _minter) external onlyOwner { 59: pendingMinter = _minter; 60: minterEnableDate = block.timestamp + 1 days; 61: }
File: contracts/rewards/RewardDistributor.sol 286: traderEnableDate = block.timestamp + 1 days; 302: voteEscrowEnableDate = block.timestamp + 1 days;
File: contracts/core/GolomTrader.sol 449: distributorEnableDate = block.timestamp + 1 days;
Add more time: *****EnableDate = block.timestamp + 3 days
Zero-address checks are a best practice for input validation of critical address parameters. While the codebase applies this in some cases, there are many places where this is missing in constructors and setters. Accidental use of zero-addresses may result in exceptions, burn fees/tokens, or force redeployment of contracts.
File: contracts/governance/GolomToken.sol 29: _transferOwnership(_governance); // set the new owner 59: pendingMinter = _minter; 67: minter = pendingMinter; 70: minter = pendingMinter;
File: contracts/rewards/RewardDistributor.sol 80: weth = ERC20(_weth); 81: trader = _trader; 82: rewardToken = ERC20(_token); 83: _transferOwnership(_governance); // set the new owner 287: pendingTrader = _trader; 293: trader = pendingTrader; 300: ve = VE(pendingVoteEscrow); 303: pendingVoteEscrow = _voteEscrow; 310: ve = VE(pendingVoteEscrow);
File: contracts/core/GolomTrader.sol 94: _transferOwnership(_governance); 446: distributor = Distributor(_distributor); 448: pendingDistributor = _distributor; 456: distributor = Distributor(pendingDistributor);
Add zero-address checks, e.g.:
require(_trader != address(0), "Zero-address");
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. https://swcregistry.io/docs/SWC-103
File: contracts/governance/GolomToken.sol 2: pragma solidity ^0.8.11;
Lock the pragma version to the same version as used in the other contracts and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.
Pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or EthPM package. Otherwise, the developer would need to manually update the pragma in order to compile it locally.
When changing state variables events are not emitted. Emitting events allows monitoring activities with off-chain monitoring tools.
File: contracts/governance/GolomToken.sol 59: pendingMinter = _minter; 67: minter = pendingMinter; 70: minter = pendingMinter;
File: contracts/rewards/RewardDistributor.sol 287: pendingTrader = _trader; 293: trader = pendingTrader; 300: ve = VE(pendingVoteEscrow); 303: pendingVoteEscrow = _voteEscrow; 310: ve = VE(pendingVoteEscrow);
File: contracts/core/GolomTrader.sol 446: distributor = Distributor(_distributor); 448: pendingDistributor = _distributor; 456: distributor = Distributor(pendingDistributor);
File: contracts/vote-escrow/VoteEscrowDelegation.sol 261: MIN_VOTING_POWER_REQUIRED = _newMinVotingPower;
Emit events for state variable changes.
Not all IERC20 implementations revert() when there’s a failure in transfer()/transferFrom(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually making a payment
File: contracts/rewards/RewardDistributor.sol 151: rewardToken.transfer(addr, reward); 165: rewardToken.transfer(addr, reward); 208: rewardToken.transfer(tokenowner, reward); 209: weth.transfer(tokenowner, rewardEth);
File: contracts/core/GolomTrader.sol 154: payable(payAddress).transfer(payAmt); // royalty transfer to royaltyaddress 382: WETH.transferFrom(o.signer, address(this), o.totalAmt * amount);
Check return value
receive()
and fallback()
functionsIf the intention is for the Ether to be used, the function should call another function, otherwise it should revert.
File: contracts/rewards/RewardDistributor.sol 313: fallback() external payable {} 315: receive() external payable {}
File: contracts/core/GolomTrader.sol 459: fallback() external payable {} 461: receive() external payable {}
abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such as keccak256()
Use abi.encode()
instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456)
=> 0x123456
=> abi.encodePacked(0x1,0x23456)
, but abi.encode(0x123,0x456)
=> 0x0...1230...456
). "Unless there is a compelling reason, abi.encode
should be preferred". If there is only one argument to abi.encodePacked()
it can often be cast to bytes()
or bytes32()
instead.
File: contracts/core/GolomTrader.sol 175: bytes32 hash = keccak256(abi.encodePacked('\x19\x01', EIP712_DOMAIN_TYPEHASH, hashStruct));
Use abi.encode()
assert()
instead of require()
Contracts use assert() instead of require() in multiple places. This causes a Panic error on failure and prevents the use of error strings. Prior to solc 0.8.0, assert() used the invalid opcode which used up all the remaining gas while require() used the revert opcode which refunded the gas and therefore the importance of using require() instead of assert() was greater. However, after 0.8.0, assert() uses revert opcode just like require() but creates a Panic(uint256) error instead of Error(string) created by require(). Nevertheless, Solidity’s documentation says:
“Assert should only be used to test for internal errors, and to check invariants. Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix. Language analysis tools can evaluate your contract to identify the conditions and function calls which will cause a Panic.”
whereas
“The require function either creates an error without any data or an error of type Error(string). It should be used to ensure valid conditions that cannot be detected until execution time. This includes conditions on inputs or return values from calls to external contracts.”
File: contracts/vote-escrow/VoteEscrowCore.sol 493: assert(idToOwner[_tokenId] == address(0)); 506: assert(idToOwner[_tokenId] == _from); 519: assert(idToOwner[_tokenId] == _owner); 666: assert(_operator != msg.sender); 679: assert(_to != address(0)); 861: assert(IERC20(token).transferFrom(from, address(this), _value)); 977: assert(_isApprovedOrOwner(msg.sender, _tokenId)); 981: assert(_value > 0); // dev: need non-zero value 991: assert(_isApprovedOrOwner(msg.sender, _tokenId)); 1007: assert(_isApprovedOrOwner(msg.sender, _tokenId)); 1023: assert(IERC20(token).transfer(msg.sender, value)); 1110: assert(_block <= block.number); 1206: assert(_block <= block.number);
Use Custom Errors or at least use require()
with informative error strings instead of assert()
.
https://swcregistry.io/docs/SWC-119 In future it could go unnoticed and subsequently lead to security issues.
File: contracts/vote-escrow/VoteEscrowDelegation.sol 71: function delegate(uint256 tokenId, uint256 toTokenId) external { 79: Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1]; 116: function _getCurrentDelegated(uint256 tokenId) internal view returns (uint256[] memory) { 168: function getVotes(uint256 tokenId) external view returns (uint256) { 185: function getPriorVotes(uint256 tokenId, uint256 blockNumber) public view returns (uint256) { 210: function removeDelegation(uint256 tokenId) external { 213: Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1];
tokenId
and checkpoint
are already defined in parent contract:
File: contracts/vote-escrow/VoteEscrowCore.sol 323: uint256 internal tokenId; 915: function checkpoint() external {
Rename the local variables that shadow another component.
On different blockchains same address may contain contract with different logic.
File: contracts/core/GolomTrader.sol 45: ERC20 WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
Make it immutable and set it in constructor()
;
The function comments imply that if passed address is zero address, then function "throws".
In reality it just returns 0
.
File: contracts/vote-escrow/VoteEscrowCore.sol 395: /// @dev Returns the number of NFTs owned by `_owner`. 396: /// Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. 397: /// @param _owner Address for whom to query the balance. 398: function _balance(address _owner) internal view returns (uint256) { 399: return ownerToNFTokenCount[_owner]; 400: } 401: 402: /// @dev Returns the number of NFTs owned by `_owner`. 403: /// Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. 404: /// @param _owner Address for whom to query the balance. 405: function balanceOf(address _owner) external view returns (uint256) { 406: return _balance(_owner); 407: } 588: /// If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if 589: /// the return value is not `bytes4(keccak256("onERC721Received(address,address,uint,bytes)"))`. //@audit no check, does not throw if wrong value returned 604: try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4) {} catch ( 605: bytes memory reason 606: ) {
Add zero address check and revert.
_writeCheckpoint()
stores array to memoryArray _delegatedTokenIds
is stored to memory
struct, not to storage
, probably intended to store it in storage.
File: contracts/vote-escrow/VoteEscrowDelegation.sol 094: function _writeCheckpoint( 095: uint256 toTokenId, 096: uint256 nCheckpoints, 097: uint256[] memory _delegatedTokenIds 098: ) internal { 099: require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); 100: uint index = nCheckpoints == 0 ? 0 : nCheckpoints- 1; 101: Checkpoint memory oldCheckpoint = checkpoints[toTokenId][index]; 102: 103: if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) { 104: oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; 105: } else { 106: checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); 107: numCheckpoints[toTokenId] = nCheckpoints + 1; 108: } 109: }
change oldCheckpoint from being memory
variable to be storage
Checkpoint storage oldCheckpoint = checkpoints[toTokenId][index];
Some libraries are outdated and contain various vulnerabilities
npm audit
shows
44 vulnerabilities (8 moderate, 35 high, 1 critical)
@openzeppelin/contracts 4.0.0 - 4.7.0 Severity: high OpenZeppelin Contracts's SignatureChecker may revert on invalid EIP-1271 signers - https://github.com/advisories/GHSA-4g63-c64m-25w9 OpenZeppelin Contracts's ERC165Checker may revert instead of returning false - https://github.com/advisories/GHSA-qh9x-gcfh-pcrw //@audit and many more in result report below
Update dependencies;
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious Manager making a frontrunning/sandwich attack on the fees).
File: contracts/vote-escrow/VoteEscrowDelegation.sol 260: function changeMinVotingPower(uint256 _newMinVotingPower) external onlyOwner { 261: MIN_VOTING_POWER_REQUIRED = _newMinVotingPower; 262: }
Add a timelock.
There should be an upper limit to minimum voting power
File: contracts/vote-escrow/VoteEscrowDelegation.sol 260: function changeMinVotingPower(uint256 _newMinVotingPower) external onlyOwner { 261: MIN_VOTING_POWER_REQUIRED = _newMinVotingPower; 262: }
Add a check for input parameter, so it can not be unreasonably high.
There is no reentrancy guard in RewardDistributor.addFee(address[2],uint256)
function, and if the trader
changed with reentrancy guard missing, it will cause exploitation of this function.
File: contracts/rewards/RewardDistributor.sol 98: function addFee(address[2] memory addr, uint256 fee) public onlyTrader {
Add reentrancy guard
Issue | Instances | |
---|---|---|
1 | Function is missing NatSpec | 34 |
2 | Contract/interface/library declaraiton is missing NatSpec | 8 |
3 | NatSpec is incomplete | 13 |
4 | Event is missing indexed fields | 4 |
5 | Remove commented out code | 1 |
6 | No need to explicitly initialize variables with default values | 35 |
7 | require() /revert() statements should have descriptive reason strings | 33 |
8 | Large multiples of ten should use scientific notation (e.g. 1e6 ) rather than decimal literals (e.g. 1000000000 or 10**18 ), for readability and obsolete calculations. | 2 |
9 | constant s should be defined rather than using magic numbers | 26 |
10 | Numeric values having to do with time should use time units for readability | 3 |
11 | Use a more recent version of solidity | 6 |
12 | States/flags should use Enum s rather than plainuint | 3 |
13 | File does not contain an SPDX Identifier | 1 |
14 | Incorrectly positioned comment | 2 |
15 | constant naming conventions not followed | 9 |
16 | Variable naming conventions not followed | 1 |
17 | Obsolete return; at the function end | 1 |
Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L32-L40
File: contracts/rewards/RewardDistributor.sol 32: function ownerOf(uint256 tokenId) external view returns (address owner); 34: function totalSupplyAtT(uint256 t) external view returns (uint256); 36: function balanceOfNFTAt(uint256 _tokenId, uint256 _t) external view returns (uint256); 38: function totalSupplyAt(uint256 _block) external view returns (uint256); 40: function balanceOfAtNFT(uint256 _tokenId, uint256 _block) external view returns (uint256); 74: constructor( 141: function traderClaim(address addr, uint256[] memory epochs) public { 155: function exchangeClaim(address addr, uint256[] memory epochs) public {
File: contracts/core/GolomTrader.sol 112: function hashPayment(Payment calldata p) private pure returns (bytes32) { 123: function _hashOrder(Order calldata o) private pure returns (bytes32) { 127: function _hashOrderinternal(Order calldata o, uint256[2] memory extra) private pure returns (bytes32) { 151: function payEther(uint256 payAmt, address payAddress) internal { 312: function cancelOrder(Order calldata o) public nonReentrant { 323: function incrementNonce() external nonReentrant {
File: contracts/vote-escrow/VoteEscrowDelegation.sol 13: function balanceOf(uint256) external view returns (uint256); 15: function balanceOfAtNFT(uint256, uint256) external view returns (uint256); 17: function ownerOf(uint256) external view returns (address); 52: constructor(address _token) {
File: contracts/vote-escrow/VoteEscrowCore.sol 445: function isApprovedOrOwner(address _spender, uint256 _tokenId) external view returns (bool) { 571: function _isContract(address account) internal view returns (bool) { 868: function setVoter(address _voter) external { 873: function voting(uint256 _tokenId) external { 878: function abstain(uint256 _tokenId) external { 883: function attach(uint256 _tokenId) external { 888: function detach(uint256 _tokenId) external { 893: function merge(uint256 _from, uint256 _to) external { 910: function block_number() external view returns (uint256) { 1093: function balanceOfNFT(uint256 _tokenId) external view returns (uint256) { 1098: function balanceOfNFTAt(uint256 _tokenId, uint256 _t) external view returns (uint256) { 1156: function balanceOfAtNFT(uint256 _tokenId, uint256 _block) external view returns (uint256) { 1198: function totalSupply() external view returns (uint256) { 1226: function _burn(uint256 _tokenId) internal {
File: contracts/vote-escrow/TokenUriHelper.sol 66: function _tokenURI( 67: uint256 _tokenId, 68: uint256 _balanceOf, 69: uint256 _locked_end, 70: uint256 _value 71: ) public pure returns (string memory output) { 128: function toString(uint256 value) internal pure returns (string memory) {
Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L31
File: contracts/rewards/RewardDistributor.sol 31: interface VE { 43: contract RewardDistributor is Ownable {
File: contracts/core/GolomTrader.sol 36: interface Distributor { 40: contract GolomTrader is Ownable, ReentrancyGuard {
File: contracts/vote-escrow/VoteEscrowDelegation.sol 12: interface IVoteEscrow { 20: contract VoteEscrow is VoteEscrowCore, Ownable {
File: contracts/vote-escrow/VoteEscrowCore.sol 275: contract VoteEscrowCore is IERC721, IERC721Metadata {
File: contracts/vote-escrow/TokenUriHelper.sol 65: library TokenUriHelper {
Check @audit
comments for details
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L328
File: contracts/core/GolomTrader.sol //@audit NatSpec tags @param tokenId, @param proof are missing 328: /// @dev function to fill a signed order of ordertype 2 also has a payment param in case the taker wants 329: /// to send ether to that address on filling the order, Match an criteria order, ensuring that the supplied proof demonstrates inclusion of the tokenId in the associated merkle root, if root is 0 then any token can be used to fill the order 330: /// @param o the Order struct to be filled must be orderType 2 331: /// @param amount the amount of times the order is to be filled(useful for ERC1155) 332: /// @param referrer referrer of the order 333: /// @param p any extra payment that the taker of this order wanna send on succesful execution of order 334: function fillCriteriaBid( 335: Order calldata o, 336: uint256 amount, 337: uint256 tokenId, 338: bytes32[] calldata proof, 339: address referrer, 340: Payment calldata p 341: ) public nonReentrant {
File: contracts/vote-escrow/VoteEscrowDelegation.sol //@audit NatSpec tags @param toTokenId, param nCheckpoints, @param _delegatedTokenIds are missing 91: /** 92: * @notice Writes the checkpoint to store current NFTs in the specific block 93: */ 94: function _writeCheckpoint( 95: uint256 toTokenId, 96: uint256 nCheckpoints, 97: uint256[] memory _delegatedTokenIds 98: ) internal { //@audit NatSpec tags @param _from, @param _to, @param _tokenId, @param _sender are missing 227: /// @dev Exeute transfer of a NFT. 228: /// Throws unless `msg.sender` is the current owner, an authorized operator, or the approved 229: /// address for this NFT. (NOTE: `msg.sender` not allowed in internal function so pass `_sender`.) 230: /// Throws if `_to` is the zero address. 231: /// Throws if `_from` is not the current owner. 232: /// Throws if `_tokenId` is not a valid NFT. 233: function _transferFrom( 234: address _from, 235: address _to, 236: uint256 _tokenId, 237: address _sender 238: ) internal override {
File: contracts/vote-escrow/VoteEscrowCore.sol //@audit NatSpec tags @param _owner, @param _tokenIndex, @return are missing 428: /// @dev Get token by index 429: function tokenOfOwnerByIndex(address _owner, uint256 _tokenIndex) external view returns (uint256) { 430: return ownerToNFTokenIdList[_owner][_tokenIndex]; 431: } //@audit NatSpec tags @param _to, @param _tokenId are missing 489: /// @dev Add a NFT to a given address 490: /// Throws if `_tokenId` is owned by someone. 491: function _addTokenTo(address _to, uint256 _tokenId) internal { //@audit NatSpec tags @param _from, @param _tokenId are missing 502: /// @dev Remove a NFT from a given address 503: /// Throws if `_from` is not the current owner. 504: function _removeTokenFrom(address _from, uint256 _tokenId) internal { //@audit NatSpec tags @param _owner, @param _tokenId are missing 515: /// @dev Clear an approval of a given address 516: /// Throws if `_owner` is not the current owner. 517: function _clearApproval(address _owner, uint256 _tokenId) internal { //@audit NatSpec tags @param _from, @param _to, @param _tokenId, @param _sender are missing 526: /// @dev Exeute transfer of a NFT. 527: /// Throws unless `msg.sender` is the current owner, an authorized operator, or the approved 528: /// address for this NFT. (NOTE: `msg.sender` not allowed in internal function so pass `_sender`.) 529: /// Throws if `_to` is the zero address. 530: /// Throws if `_from` is not the current owner. 531: /// Throws if `_tokenId` is not a valid NFT. 532: function _transferFrom( 533: address _from, 534: address _to, 535: uint256 _tokenId, 536: address _sender 537: ) internal virtual { //@audit NatSpec tag @param _tokenId is missing 974: /// @notice Deposit `_value` additional tokens for `_tokenId` without modifying the unlock time 975: /// @param _value Amount of tokens to deposit and add to the lock 976: function increase_amount(uint256 _tokenId, uint256 _value) external nonreentrant { //@audit NatSpec tag @param _tokenId is missing 988: /// @notice Extend the unlock time for `_tokenId` 989: /// @param _lock_duration New number of seconds until tokens unlock 990: function increase_unlock_time(uint256 _tokenId, uint256 _lock_duration) external nonreentrant { //@audit NatSpec tag @param t is missing 1189: /// @notice Calculate total voting power 1190: /// @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility 1191: /// @return Total voting power 1192: function totalSupplyAtT(uint256 t) public view returns (uint256) {
File: contracts/vote-escrow/TokenUriHelper.sol //@audit NatSpec tag @param data is missing 11: /// @notice Encodes some bytes to the base64 representation 12: function encode(bytes memory data) internal pure returns (string memory) {
Each event
should use three indexed
fields if there are three or more fields
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L70
File: contracts/rewards/RewardDistributor.sol 70: event NewEpoch(uint256 indexed epochNo, uint256 tokenMinted, uint256 rewardStaker, uint256 previousEpochFee);
File: contracts/vote-escrow/VoteEscrowDelegation.sol 29: event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance);
File: contracts/vote-escrow/VoteEscrowCore.sol 284: event Deposit( 285: address indexed provider, 286: uint256 tokenId, 287: uint256 value, 288: uint256 indexed locktime, 289: DepositType deposit_type, 290: uint256 ts 291: ); 292: event Withdraw(address indexed provider, uint256 tokenId, uint256 value, uint256 ts);
Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L171
File: contracts/vote-escrow/VoteEscrowDelegation.sol 218: // /// @notice Remove delegation by user 219: // function removeDelegationByOwner(uint256 delegatedTokenId, uint256 ownerTokenId) external { 220: // require(ownerOf(ownerTokenId) == msg.sender, 'VEDelegation: Not allowed'); 221: // uint256 nCheckpoints = numCheckpoints[delegatedTokenId]; 222: // Checkpoint storage checkpoint = checkpoints[delegatedTokenId][nCheckpoints - 1]; 223: // removeElement(checkpoint.delegatedTokenIds, delegatedTokenId); 224: // _writeCheckpoint(ownerTokenId, nCheckpoints, checkpoint.delegatedTokenIds); 225: // }
If a variable is not set/initialized, it is assumed to have the default value (0
for uint
, false
for bool
, address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern.
As an example: for (uint256 i = 0; i < numIterations; ++i) {
should be replaced with for (uint256 i; i < numIterations; ++i) {
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L142
File: contracts/rewards/RewardDistributor.sol 142: uint256 reward = 0; 143: for (uint256 index = 0; index < epochs.length; index++) { 156: uint256 reward = 0; 157: for (uint256 index = 0; index < epochs.length; index++) { 175: uint256 reward = 0; 176: uint256 rewardEth = 0; 180: for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { 183: for (uint256 index = 0; index < epochs.length; index++) { 222: uint256 reward = 0; 223: uint256 rewardEth = 0; 226: for (uint256 index = 0; index < epoch; index++) { 257: uint256 reward = 0; 258: for (uint256 index = 0; index < epoch; index++) { 272: uint256 reward = 0; 273: for (uint256 index = 0; index < epoch; index++) {
File: contracts/core/GolomTrader.sol 415: for (uint256 i = 0; i < proof.length; i++) {
File: contracts/vote-escrow/VoteEscrowDelegation.sol 147: uint256 lower = 0; 170: uint256 votes = 0; 171: for (uint256 index = 0; index < delegated.length; index++) { 188: uint256 votes = 0; 189: for (uint256 index = 0; index < delegatednft.length; index++) {
File: contracts/vote-escrow/VoteEscrowCore.sol 697: int128 old_dslope = 0; 698: int128 new_dslope = 0; 735: uint256 block_slope = 0; // dblock/dt 745: for (uint256 i = 0; i < 255; ++i) { 749: int128 d_slope = 0; 1042: uint256 _min = 0; 1044: for (uint256 i = 0; i < 128; ++i) { 1113: uint256 _min = 0; 1115: for (uint256 i = 0; i < 128; ++i) { 1133: uint256 d_block = 0; 1134: uint256 d_t = 0; 1167: for (uint256 i = 0; i < 255; ++i) { 1169: int128 d_slope = 0; 1211: uint256 dt = 0;
require()
/revert()
statements should have descriptive reason stringsInstances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L88
File: contracts/rewards/RewardDistributor.sol 88: require(msg.sender == trader); 144: require(epochs[index] < epoch); 158: require(epochs[index] < epoch); 217: require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm'); //@audit Saruman | golom.io: "No hidden meaning just random string" 220: require(msg.sender == o.reservedAddress); 285: require( 286: o.totalAmt * amount > 287: (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt) * amount + p.paymentAmt 288: ); 291: require(msg.sender == o.reservedAddress); 293: require(o.orderType == 1); 295: require(status == 3); 296: require(amountRemaining >= amount); 313: require(o.signer == msg.sender); 342: require(o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt); 345: require(msg.sender == o.reservedAddress); 347: require(o.orderType == 2); 349: require(status == 3); 350: require(amountRemaining >= amount);
File: contracts/vote-escrow/VoteEscrowDelegation.sol 245: require(_isApprovedOrOwner(_sender, _tokenId));
File: contracts/vote-escrow/VoteEscrowCore.sol 360: require(_entered_state == _not_entered); 540: require(_isApprovedOrOwner(_sender, _tokenId)); 646: require(owner != address(0)); 648: require(_approved != owner); 652: require(senderIsOwner || senderIsApprovedForAll); 869: require(msg.sender == voter); 874: require(msg.sender == voter); 879: require(msg.sender == voter); 884: require(msg.sender == voter); 889: require(msg.sender == voter); 895: require(_from != _to); 896: require(_isApprovedOrOwner(msg.sender, _from)); 897: require(_isApprovedOrOwner(msg.sender, _to)); 927: require(_value > 0); // dev: need non-zero value 944: require(_value > 0); // dev: need non-zero value
1e6
) rather than decimal literals (e.g. 1000000000
or 10**18
), for readability and obsolete calculations.There are 2 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L48
File: contracts/rewards/RewardDistributor.sol 48: uint256 constant dailyEmission = 600000 * 10**18; 100: if (rewardToken.totalSupply() > 1000000000 * 10**18) {
constant
s should be defined rather than using magic numbersInstances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L44
File: contracts/governance/GolomToken.sol 44: _mint(_airdrop, 150_000_000 * 1e18); 52: _mint(_rewardDistributor, 62_500_000 * 1e18);
File: contracts/rewards/RewardDistributor.sol 100: if (rewardToken.totalSupply() > 1000000000 * 10**18) { 120: rewardTrader[epoch] = ((tokenToEmit - stakerReward) * 67) / 100; 121: rewardExchange[epoch] = ((tokenToEmit - stakerReward) * 33) / 100; 179: return (0, hashStruct, 0); 183: return (1, hashStruct, 0); 187: return (2, hashStruct, 0); 191: return (2, hashStruct, 0); 193: return (3, hashStruct, o.tokenAmt - filled[hashStruct]); 212: o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt + (o.totalAmt * 50) / 10000, 226: require(status == 3, 'order not valid'); 242: payEther(((o.totalAmt * 50) / 10000) * amount, address(distributor)); 254: (o.totalAmt * 50) / 255: 10000 - 263: (o.totalAmt - (o.totalAmt * 50) / 10000 - o.exchange.paymentAmt - o.prePayment.paymentAmt) * amount, 269: distributor.addFee([o.signer, o.exchange.paymentAddress], ((o.totalAmt * 50) / 10000) * amount); 381: uint256 protocolfee = ((o.totalAmt * 50) / 10000) * amount;
File: contracts/vote-escrow/VoteEscrowCore.sol 611: revert(add(32, reason), mload(reason)) 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) {
File: contracts/vote-escrow/TokenUriHelper.sol 17: uint256 encodedLen = 4 * ((len + 2) / 3); 20: bytes memory result = new bytes(encodedLen + 32); 139: temp /= 10; 145: value /= 10;
There are units for seconds, minutes, hours, days, and weeks Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L57
File: contracts/rewards/RewardDistributor.sol 57: uint256 constant secsInDay = 24 * 60 * 60;
File: contracts/vote-escrow/VoteEscrowCore.sol 296: uint256 internal constant MAXTIME = 4 * 365 * 86400; 297: int128 internal constant iMAXTIME = 4 * 365 * 86400;
Use a solidity version of at least 0.8.12 to get string.concat() instead of abi.encodePacked(<str>,<str>)
Use a solidity version of at least 0.8.13 to get the ability to use using for
with a list of free functions
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L2
File: contracts/governance/GolomToken.sol 2: pragma solidity ^0.8.11;
File: contracts/core/GolomTrader.sol 3: pragma solidity 0.8.11;
File: contracts/vote-escrow/TokenUriHelper.sol 3: pragma solidity 0.8.11;
File: contracts/vote-escrow/VoteEscrowDelegation.sol 2: pragma solidity 0.8.11;
File: contracts/vote-escrow/VoteEscrowCore.sol 2: pragma solidity 0.8.11;
File: contracts/rewards/RewardDistributor.sol 2: pragma solidity 0.8.11;
Enum
s rather than plainuint
File: contracts/core/GolomTrader.sol 51: uint256 orderType; // 0 if selling nft for eth , 1 if offering weth for nft,2 if offering weth for collection with special criteria root
File: contracts/vote-escrow/VoteEscrowCore.sol 356: uint8 internal constant _not_entered = 1; 357: uint8 internal constant _entered = 2;
Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/TokenUriHelper.sol#L1
File: contracts/vote-escrow/TokenUriHelper.sol 1: /// [MIT License]
Some comments are misplaced. Check @audit
for details.
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L152
File: contracts/core/GolomTrader.sol 152: if (payAmt > 0) { 153: // if royalty has to be paid @audit comment should be above if check 342: require(o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt); 343: // require eth amt is sufficient @audit this comment should be before require check above;
File: contracts/vote-escrow/VoteEscrowCore.sol 538: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); 539: // Check requirements @audit this comment should be before require check above;
constant
naming conventions not followedConstants should be written in uppercase characters separated by underscores. Constant names may also contain digits if appropriate, but not as the first character. Some constants do not follow it; Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L48
File: contracts/rewards/RewardDistributor.sol 48: uint256 constant dailyEmission = 600000 * 10**18; 57: uint256 constant secsInDay = 24 * 60 * 60;
File: contracts/vote-escrow/VoteEscrowCore.sol 297: int128 internal constant iMAXTIME = 4 * 365 * 86400; 317: string public constant name = 'veNFT'; 318: string public constant symbol = 'veNFT'; 319: string public constant version = '1.0.0'; 320: uint8 public constant decimals = 18; 356: uint8 internal constant _not_entered = 1; 357: uint8 internal constant _entered = 2;
variable MIN_VOTING_POWER_REQUIRED
should not be uppercased, it looks like it is a constant
but it is not
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L50
File: contracts/vote-escrow/VoteEscrowDelegation.sol 50: uint256 public MIN_VOTING_POWER_REQUIRED = 0; 261: MIN_VOTING_POWER_REQUIRED = _newMinVotingPower;
return;
at the function endreturn;
is obsolete at the end of a function
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L137
File: contracts/rewards/RewardDistributor.sol 137: return; 138: }
🌟 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
123.0542 USDC - $123.05
Issue | Instances | |
---|---|---|
1 | ++variable costs less gas than variable++ | 12 |
2 | Access mappings directly rather than using accessor functions | 6 |
3 | Cheaper input valdiations should come before expensive operations | 3 |
4 | Help the optimizer by saving a storage variable's reference | 1 |
5 | The result of external function calls should be cached rather than re-calling the function | 4 |
6 | Amounts should be checked for 0 before calling a transfer | 10 |
7 | public functions to external | 8 |
8 | Using calldata instead of memory for read-only arguments in external functions saves gas | 4 |
9 | Default value initialization | 2 |
10 | Multiplication/division by 2 should use bit shifting | 3 |
11 | Splitting require() statements that use && saves gas | 4 |
12 | abi.encode() is less efficient than abi.encodePacked() | 4 |
13 | struct Order : can be more tighly packed | 1 |
14 | State variables can be packed into fewer storage slots | 8 |
15 | x += y costs more gas than x = x + y for state variables | 2 |
16 | internal and private functions only called once can be inlined to save gas | 10 |
17 | Unchecked arithmetic | 50 |
18 | Using bool s for storage incurs overhead | 5 |
19 | Using > 0 costs more gas than != 0 when used on a uint in a require() and assert statement | 3 |
20 | Usage of uints /ints smaller than 32 bytes (256 bits) incurs overhead | 11 |
21 | Use custom errors rather than revert() /require() strings to save gas | 76 |
22 | Multiple address mappings can be combined into a single mapping of an address to a struct , where appropriate | 5 |
23 | Functions guaranteed to revert when called by normal users can be marked payable | 12 |
24 | Do not calculate constants | 4 |
25 | <array>.length should not be looked up in every loop of a for-loop | 8 |
26 | Copying struct to memory can be more expensive than just reading from storagexxx | 3 |
27 | require() /revert() strings longer than 32 bytes cost extra gas | 8 |
28 | Remove or replace unused variables | 2 |
29 | State variables only set in the constructor should be declared immutable | 3 |
30 | State variables with values known at compile time should be constants | 2 |
31 | State variables should be cached in stack variables rather than re-reading them from storage | 14 |
32 | Using private rather than public for constants, saves gas | 4 |
33 | Remove unreachable code | 1 |
34 | No need to evaluate all expressions to know if one of them is true | 2 |
35 | No need to read tokenId second time | 1 |
36 | last_point value rewritten right after initialization | 1 |
37 | Wasted gas on copying a struct | 1 |
38 | Remove duplicate code | 1 |
39 | No need for mapping supportedInterfaces to exist | 1 |
40 | Same calculation twice | 1 |
41 | Obsolete constants | 2 |
42 | Variable end can be of type uint128 | 1 |
Prefix increments are cheaper than postfix increments. Saves 6 gas PER LOOP There are 12 instances of this issue:
File: contracts/rewards/RewardDistributor.sol 143: for (uint256 index = 0; index < epochs.length; index++) { 157: for (uint256 index = 0; index < epochs.length; index++) { 180: for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { 183: for (uint256 index = 0; index < epochs.length; index++) { 226: for (uint256 index = 0; index < epoch; index++) { 258: for (uint256 index = 0; index < epoch; index++) { 273: for (uint256 index = 0; index < epoch; index++) {
File: contracts/core/GolomTrader.sol 415: for (uint256 i = 0; i < proof.length; i++) {
File: contracts/vote-escrow/VoteEscrowDelegation.sol 171: for (uint256 index = 0; index < delegated.length; index++) { 189: for (uint256 index = 0; index < delegatednft.length; index++) { 199: for (uint256 i; i < _array.length; i++) {
File: contracts/vote-escrow/TokenUriHelper.sol 138: digits++;
Change i++
to ++i
Saves having to do two JUMP
instructions, along with stack setup
There are 6 instances of this issue:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L72
File: contracts/vote-escrow/VoteEscrowDelegation.sol 72: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); 211: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
File: contracts/vote-escrow/VoteEscrowCore.sol 1229: address owner = ownerOf(_tokenId); 406: return _balance(_owner); 453: uint256 current_count = _balance(_to); 464: uint256 current_count = _balance(_from) - 1;
Istead of ownerOf(tokenId)
use idToOwner[tokenId]
Check @audit comment for details There are 3 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L925
File: contracts/vote-escrow/VoteEscrowCore.sol 925: LockedBalance memory _locked = locked[_tokenId]; 926: //@audit this check should be before we read from storage 927: require(_value > 0); // dev: need non-zero value 942: uint256 unlock_time = ((block.timestamp + _lock_duration) / WEEK) * WEEK; // Locktime is rounded down to weeks 943: //@audit this check should be before we do unlock_time evalutaion 944: require(_value > 0); // dev: need non-zero value 977: assert(_isApprovedOrOwner(msg.sender, _tokenId)); 978: 979: LockedBalance memory _locked = locked[_tokenId]; 980: //@audit this check should be first, before line 977 check 981: assert(_value > 0); // dev: need non-zero value
To help the optimizer, declare a storage
type variable and use it instead of repeatedly fetching the reference in a map or an array.
The effect can be quite significant.
As an example, instead of repeatedly calling someMap[someIndex]
, save its reference like this: SomeStruct storage someStruct = someMap[someIndex]
and use it.
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L138
File: contracts/vote-escrow/VoteEscrowDelegation.sol 138: if (checkpoints[nftId][nCheckpoints - 1].fromBlock <= blockNumber) { 139: return checkpoints[nftId][nCheckpoints - 1].delegatedTokenIds;
Declare storage
variable and use it:
Checkpoint storage checkpoint = checkpoints[nftId][nCheckpoints - 1]; if (checkpoint.fromBlock <= blockNumber) { return checkpoint.delegatedTokenIds; }
There is no need to call another contract functions multiple times to get same value, returned value should be cached in a variable; The instances below point to the second+ call of the function within a single function: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L100
File: contracts/rewards/RewardDistributor.sol 100: if (rewardToken.totalSupply() > 1000000000 * 10**18) { 112: uint256 tokenToEmit = (dailyEmission * (rewardToken.totalSupply() - rewardToken.balanceOf(address(ve)))) / 113: rewardToken.totalSupply(); 114: uint256 stakerReward = (tokenToEmit * rewardToken.balanceOf(address(ve))) / rewardToken.totalSupply(); 239: (rewardStaker[index] * ve.balanceOfAtNFT(tokenid, epochBeginTime[index])) / 240: ve.totalSupplyAt(epochBeginTime[index]); 244: ve.balanceOfAtNFT(tokenid, epochBeginTime[index])) / 245: ve.totalSupplyAt(epochBeginTime[index]);
rewardToken.totalSupply()
should be cached.
ve.balanceOfAtNFT(tokenid, epochBeginTime[index]))
should be cached.
ve.totalSupplyAt(epochBeginTime[index])
should be cached.
0
before calling a transferChecking non-zero transfer values can avoid an expensive external call and save gas. While this is done at some places, it's not consistently done in the solution. I suggest adding a non-zero-value check here: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L151
File: contracts/rewards/RewardDistributor.sol 151: rewardToken.transfer(addr, reward); 165: rewardToken.transfer(addr, reward); 208: rewardToken.transfer(tokenowner, reward); 209: weth.transfer(tokenowner, rewardEth);
File: contracts/core/GolomTrader.sol 238: ERC1155(o.collection).safeTransferFrom(o.signer, receiver, o.tokenId, amount, ''); 304: nftcontract.safeTransferFrom(msg.sender, o.signer, o.tokenId, amount, ''); 364: nftcontract.safeTransferFrom(msg.sender, o.signer, tokenId, amount, ''); 382: WETH.transferFrom(o.signer, address(this), o.totalAmt * amount); 383: WETH.withdraw(o.totalAmt * amount);
File: contracts/vote-escrow/VoteEscrowCore.sol 1023: assert(IERC20(token).transfer(msg.sender, value));
public
functions to external
External call cost is less expensive than of public functions.
Contracts are allowed to override their parents’ functions and change the visibility from external
to public
.
The following functions could be set external
to save gas and improve code quality:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L98
File: contracts/rewards/RewardDistributor.sol 98: function addFee(address[2] memory addr, uint256 fee) public onlyTrader { 141: function traderClaim(address addr, uint256[] memory epochs) public { 155: function exchangeClaim(address addr, uint256[] memory epochs) public { 172: function multiStakerClaim(uint256[] memory tokenids, uint256[] memory epochs) public {
File: contracts/core/GolomTrader.sol 203: function fillAsk( 204: Order calldata o, 205: uint256 amount, 206: address referrer, 207: Payment calldata p, 208: address receiver 209: ) public payable nonReentrant { 279: function fillBid( 280: Order calldata o, 281: uint256 amount, 282: address referrer, 283: Payment calldata p 284: ) public nonReentrant { 312: function cancelOrder(Order calldata o) public nonReentrant { 334: function fillCriteriaBid( 335: Order calldata o, 336: uint256 amount, 337: uint256 tokenId, 338: bytes32[] calldata proof, 339: address referrer, 340: Payment calldata p 341: ) public nonReentrant {
calldata
instead of memory
for read-only arguments in external
functions saves gasIf you choose to make suggested above public
functions as external
, to continue gas optimizaton we can use calldata
function arguments instead of memory
.
When a function with a memory
array is called externally, the abi.decode()
step has to use a for-loop to copy each index of the calldata
to the memory
index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length
). Using calldata
directly, obliviates the need for such a loop in the contract code and runtime execution. Structs have the same overhead as an array of length one.
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L98
File: contracts/rewards/RewardDistributor.sol 98: function addFee(address[2] memory addr, uint256 fee) public onlyTrader { 141: function traderClaim(address addr, uint256[] memory epochs) public { 155: function exchangeClaim(address addr, uint256[] memory epochs) public { 172: function multiStakerClaim(uint256[] memory tokenids, uint256[] memory epochs) public {
If a variable is not set/initialized, it is assumed to have the default value (0
, false
, 0x0
etc depending on the data type).
Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L45
File: contracts/rewards/RewardDistributor.sol 45: uint256 public epoch = 0;
File: contracts/vote-escrow/VoteEscrowDelegation.sol uint256 public MIN_VOTING_POWER_REQUIRED = 0;
In case of RewardDistributor
hardhat-gas-reporter
results
before fix:
Deployments | |||
---|---|---|---|
Contract | min | max | avg |
GolomTrader | 2379507 | 2379543 | 2379537 |
after fix: | |||
Deployments | |||
- | :-: | :-: | :-: |
Contract | min | max | avg |
GolomTrader | 2377233 | 2377269 | 2377263 |
Remove explicit initialization for default values.
<x> * 2
is equivalent to <x> << 1
and <x> / 2
is the same as <x> >> 1
. The MUL
and DIV
opcodes cost 5 gas, whereas SHL
and SHR
only cost 3 gas
There are 3 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L150
File: contracts/vote-escrow/VoteEscrowDelegation.sol 150: uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
File: contracts/vote-escrow/VoteEscrowCore.sol 1049: uint256 _mid = (_min + _max + 1) / 2; 1120: uint256 _mid = (_min + _max + 1) / 2;
require()
statements that use &&
saves gasInstead of using the &&
operator in a single require statement to check multiple conditions, I suggest using multiple require statements with 1 condition per require statement (saving 3 gas per &
).
See this issue which describes the fact that there is a larger deployment gas cost, but with enough runtime calls, the change ends up being cheaper
There are 4 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L239
File: contracts/vote-escrow/VoteEscrowDelegation.sol 239: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached');
File: contracts/vote-escrow/VoteEscrowCore.sol 538: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); 894: require(attachments[_from] == 0 && !voted[_from], 'attached'); 1008: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached');
abi.encode()
is less efficient than abi.encodePacked()
Consider changing it if possible. There are 4 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L102
File: contracts/core/GolomTrader.sol 102: abi.encode( 115: abi.encode( 130: abi.encode( 414: bytes32 computedHash = keccak256(abi.encode(leaf));
struct Order
: can be more tighly packedEach slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings before: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L47
File: contracts/core/GolomTrader.sol 47: struct Order { address collection; // NFT contract address uint256 tokenId; // order for which tokenId of the collection address signer; // maker of order address uint256 orderType; // 0 if selling nft for eth , 1 if offering weth for nft,2 if offering weth for collection with special criteria root 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 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 uint256 deadline; // timestamp till order is valid epoch timestamp in secs uint8 v; bytes32 r; bytes32 s; }
Methods | |||||
---|---|---|---|---|---|
Contract | Method | min | max | avg | #calls |
GolomTrader | fillAsk | 238167 | 241974 | 241425 | 7 |
GolomTrader | setDistributor | 46281 | 70449 | 47432 | 21 |
Deployments | |
---|---|
Contract | avg |
GolomTrader | 2029204 |
after:
struct Order { address collection; // NFT contract address uint256 tokenId; // order for which tokenId of the collection address signer; // maker of order address uint256 orderType; // 0 if selling nft for eth , 1 if offering weth for nft,2 if offering weth for collection with special criteria root 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 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 uint256 deadline; // timestamp till order is valid epoch timestamp in secs bool isERC721; // standard of the collection , if 721 then true , if 1155 then false uint8 v; bytes32 r; bytes32 s; }
Methods | |||||
---|---|---|---|---|---|
Contract | Method | min | max | avg | #calls |
GolomTrader | fillAsk | 238153 | 241948 | 241394 | 7 |
GolomTrader | setDistributor | 46259 | 70427 | 47410 | 21 |
Deployments | |
---|---|
Contract | avg |
GolomTrader | 2013782 |
These state variables can be packed together to use less storage slots: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L297
File: contracts/vote-escrow/VoteEscrowCore.sol 297: int128 internal constant iMAXTIME = 4 * 365 * 86400; 320: uint8 public constant decimals = 18; 347: bytes4 internal constant ERC165_INTERFACE_ID = 0x01ffc9a7; 350: bytes4 internal constant ERC721_INTERFACE_ID = 0x80ac58cd; 353: bytes4 internal constant ERC721_METADATA_INTERFACE_ID = 0x5b5e139f; 356: uint8 internal constant _not_entered = 1; 357: uint8 internal constant _entered = 2; 358: uint8 internal _entered_state = 1;
x += y
costs more gas than x = x + y
for state variablesx += y
costs more than x = x + y
same as x -= y
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L499
File: contracts/vote-escrow/VoteEscrowCore.sol 499: ownerToNFTokenCount[_to] += 1; 512: ownerToNFTokenCount[_from] -= 1;
Replace x += y
and x -= y
with x = x + y
and x = x - y
.
internal
and private
functions only called once can be inlined to save gasNot inlining costs 20 to 40 gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
There are 10 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L116
File: contracts/vote-escrow/VoteEscrowDelegation.sol 116: function _getCurrentDelegated(uint256 tokenId) internal view returns (uint256[] memory) { 129: function _getPriorDelegated(uint256 nftId, uint256 blockNumber) internal view returns (uint256[] memory) { 198: function removeElement(uint256[] storage _array, uint256 _element) internal {
File: contracts/vote-escrow/VoteEscrowCore.sol 452: function _addTokenToOwnerList(address _to, uint256 _tokenId) internal { 462: function _removeTokenFromOwnerList(address _from, uint256 _tokenId) internal { 571: function _isContract(address account) internal view returns (bool) { 677: function _mint(address _to, uint256 _tokenId) internal returns (bool) { 1107: function _balanceOfAtNFT(uint256 _tokenId, uint256 _block) internal view returns (uint256) {
File: contracts/governance/GolomToken.sol 123: function _hashOrder(Order calldata o) private pure returns (bytes32) { 127: function _hashOrderinternal(Order calldata o, uint256[2] memory extra) private pure returns (bytes32) {
The default “checked” behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected. if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked block will save gas. Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L60
File: contracts/governance/GolomToken.sol minterEnableDate = block.timestamp + 1 days;
File: contracts/rewards/RewardDistributor.sol 106: if (block.timestamp > startTime + (epoch) * secsInDay) { 112: uint256 tokenToEmit = (dailyEmission * (rewardToken.totalSupply() - rewardToken.balanceOf(address(ve)))) / 113: rewardToken.totalSupply(); 114: uint256 stakerReward = (tokenToEmit * rewardToken.balanceOf(address(ve))) / rewardToken.totalSupply(); 118: epoch = epoch + 1; 143: for (uint256 index = 0; index < epochs.length; index++) { 157: for (uint256 index = 0; index < epochs.length; index++) { 180: for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { 183: for (uint256 index = 0; index < epochs.length; index++) { 258: for (uint256 index = 0; index < epoch; index++) { 273: for (uint256 index = 0; index < epoch; index++) { 286: traderEnableDate = block.timestamp + 1 days; 302: voteEscrowEnableDate = block.timestamp + 1 days;
File: contracts/core/GolomTrader.sol 324: uint256 newNonce = ++nonces[msg.sender]; 415: for (uint256 i = 0; i < proof.length; i++) { 449: distributorEnableDate = block.timestamp + 1 days;
File: contracts/vote-escrow/VoteEscrowDelegation.sol 107: numCheckpoints[toTokenId] = nCheckpoints + 1; 119: return nCheckpoints > 0 ? checkpoints[tokenId][nCheckpoints - 1].delegatedTokenIds : myArray; 138: if (checkpoints[nftId][nCheckpoints - 1].fromBlock <= blockNumber) { 139: return checkpoints[nftId][nCheckpoints - 1].delegatedTokenIds; 148: uint256 upper = nCheckpoints - 1; 150: uint256 center = upper - (upper - lower) / 2; 157: upper = center - 1; 171: for (uint256 index = 0; index < delegated.length; index++) { 172: votes = votes + this.balanceOfNFT(delegated[index]); 189: for (uint256 index = 0; index < delegatednft.length; index++) { 190: votes = votes + this.balanceOfAtNFT(delegatednft[index], blockNumber); 199: for (uint256 i; i < _array.length; i++) { 201: _array[i] = _array[_array.length - 1];
File: contracts/vote-escrow/VoteEscrowCore.sol 499: ownerToNFTokenCount[_to] += 1; 512: ownerToNFTokenCount[_from] -= 1; 745: for (uint256 i = 0; i < 255; ++i) { 748: t_i += WEEK; 768: _epoch += 1; 994: uint256 unlock_time = ((block.timestamp + _lock_duration) / WEEK) * WEEK; // Locktime is rounded down to weeks 999: require(unlock_time <= block.timestamp + MAXTIME, 'Voting lock can be 4 years max'); 1044: for (uint256 i = 0; i < 128; ++i) { 1049: uint256 _mid = (_min + _max + 1) / 2; 1053: _max = _mid - 1; 1115: for (uint256 i = 0; i < 128; ++i) { 1120: uint256 _mid = (_min + _max + 1) / 2; 1124: _max = _mid - 1; 1167: for (uint256 i = 0; i < 255; ++i) { 1213: Point memory point_next = point_history[target_epoch + 1];
File: contracts/vote-escrow/TokenUriHelper.sol 138: digits++; 139: temp /= 10; 143: digits -= 1; 144: buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); 145: value /= 10;
Place the arithmetic operations in an unchecked
block
for (uint i; i < length;) { ... unchecked{ ++i; } }
bool
s for storage incurs overhead// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27
Use uint256(1)
and uint256(2)
for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past
There are 5 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L20
File: contracts/governance/GolomToken.sol 20: bool public isAirdropMinted; 21: bool public isGenesisRewardMinted;
File: contracts/vote-escrow/VoteEscrowCore.sol 314: mapping(uint256 => bool) public voted; 341: mapping(address => mapping(address => bool)) internal ownerToOperators; 344: mapping(bytes4 => bool) internal supportedInterfaces;
> 0
costs more gas than != 0
when used on a uint
in a require()
and assert
statement> 0
is less efficient than != 0
for unsigned integers.
!= 0
costs less gas compared to > 0
for unsigned integers in require
and assert
statements with the optimizer enabled (6 gas)
There are 3 instances of this issue:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L927
File: contracts/vote-escrow/VoteEscrowCore.sol 927: require(_value > 0); // dev: need non-zero value 944: require(_value > 0); // dev: need non-zero value 981: assert(_value > 0); // dev: need non-zero value
Replace > 0
with != 0
Or update soldity compiler to >=0.8.13
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadWhen using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. It is only beneficial to use reduced-size arguments if you are dealing with storage values because the compiler will pack multiple elements into one storage slot, and thus, combine multiple reads or writes into a single operation. When dealing with function arguments or memory values, there is no inherent benefit because the compiler does not pack these values.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L271
File: contracts/vote-escrow/VoteEscrowCore.sol 271: int128 amount; //@audit no storage slot saved 297: int128 internal constant iMAXTIME = 4 * 365 * 86400; 311: mapping(uint256 => int128) public slope_changes; // time -> signed slope change 320: uint8 public constant decimals = 18; 356: uint8 internal constant _not_entered = 1; 357: uint8 internal constant _entered = 2; 358: uint8 internal _entered_state = 1; 697: int128 old_dslope = 0; 698: int128 new_dslope = 0; 749: int128 d_slope = 0; 1169: int128 d_slope = 0;
revert()
/require()
strings to save gasCustom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit by avoiding having to allocate and store the revert string. Not defining the strings also saves deployment gas.
There are 76 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L24
File: contracts/governance/GolomToken.sol 24: require(msg.sender == minter, 'GolomToken: only reward distributor can enable'); 43: require(!isAirdropMinted, 'already minted'); 51: require(!isGenesisRewardMinted, 'already minted'); 69: require(minterEnableDate <= block.timestamp, 'GolomToken: wait for timelock');
File: contracts/rewards/RewardDistributor.sol 88: require(msg.sender == trader); 144: require(epochs[index] < epoch); 158: require(epochs[index] < epoch); 173: require(address(ve) != address(0), ' VE not added yet'); 181: require(tokenowner == ve.ownerOf(tokenids[tindex]), 'Can only claim for a single Address together'); 184: require(epochs[index] < epoch, 'cant claim for future epochs'); 185: require(claimed[tokenids[tindex]][epochs[index]] == 0, 'cant claim if already claimed'); 220: require(address(ve) != address(0), ' VE not added yet'); 292: require(traderEnableDate <= block.timestamp, 'RewardDistributor: time not over yet'); 309: require(voteEscrowEnableDate <= block.timestamp, 'RewardDistributor: time not over yet');
File: contracts/core/GolomTrader.sol 177: require(signaturesigner == o.signer, 'invalid signature'); 211: require( 212: o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt + (o.totalAmt * 50) / 10000, 213: 'amt not matching' 214: ); 217: require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm'); 220: require(msg.sender == o.reservedAddress); 222: require(o.orderType == 0, 'invalid orderType'); 226: require(status == 3, 'order not valid'); 227: require(amountRemaining >= amount, 'order already filled'); 235: require(amount == 1, 'only 1 erc721 at 1 time'); 285: require( 286: o.totalAmt * amount > 287: (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt) * amount + p.paymentAmt 291: require(msg.sender == o.reservedAddress); 293: require(o.orderType == 1); 295: require(status == 3); 296: require(amountRemaining >= amount); 299: require(amount == 1, 'only 1 erc721 at 1 time'); 313: require(o.signer == msg.sender); 342: require(o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt); 345: require(msg.sender == o.reservedAddress); 347: require(o.orderType == 2); 349: require(status == 3); 350: require(amountRemaining >= amount); 359: require(amount == 1, 'only 1 erc721 at 1 time'); 455: require(distributorEnableDate <= block.timestamp, 'not allowed');
File: contracts/vote-escrow/VoteEscrowDelegation.sol 72: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); 73: require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power'); 99: require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); 130: require(blockNumber < block.number, 'VEDelegation: not yet determined'); 186: require(blockNumber < block.number, 'VEDelegation: not yet determined'); 211: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); 239: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); 245: require(_isApprovedOrOwner(_sender, _tokenId));
File: contracts/vote-escrow/VoteEscrowCore.sol 360: require(_entered_state == _not_entered); 538: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); 540: require(_isApprovedOrOwner(_sender, _tokenId)); 646: require(owner != address(0)); 648: require(_approved != owner); 652: require(senderIsOwner || senderIsApprovedForAll); 869: require(msg.sender == voter); 874: require(msg.sender == voter); 879: require(msg.sender == voter); 884: require(msg.sender == voter); 889: require(msg.sender == voter); 894: require(attachments[_from] == 0 && !voted[_from], 'attached'); 895: require(_from != _to); 896: require(_isApprovedOrOwner(msg.sender, _from)); 897: require(_isApprovedOrOwner(msg.sender, _to)); 927: require(_value > 0); // dev: need non-zero value 928: require(_locked.amount > 0, 'No existing lock found'); 929: require(_locked.end > block.timestamp, 'Cannot add to expired lock. Withdraw'); 944: require(_value > 0); // dev: need non-zero value 945: require(unlock_time > block.timestamp, 'Can only lock until time in the future'); 946: require(unlock_time <= block.timestamp + MAXTIME, 'Voting lock can be 4 years max'); 982: require(_locked.amount > 0, 'No existing lock found'); 983: require(_locked.end > block.timestamp, 'Cannot add to expired lock. Withdraw'); 996: require(_locked.end > block.timestamp, 'Lock expired'); 997: require(_locked.amount > 0, 'Nothing is locked'); 998: require(unlock_time > _locked.end, 'Can only increase lock duration'); 999: require(unlock_time <= block.timestamp + MAXTIME, 'Voting lock can be 4 years max'); 1008: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached'); 1011: require(block.timestamp >= _locked.end, "The lock didn't expire"); 1082: require(idToOwner[_tokenId] != address(0), 'Query for nonexistent token'); 1227: require(_isApprovedOrOwner(msg.sender, _tokenId), 'caller is not owner nor approved');
Custom errors are defined using the error
statement
Replace require
statements with custom errors.
address
mappings can be combined into a single mapping
of an address
to a struct
, where appropriateSaves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
There are 5 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L58
File: contracts/rewards/RewardDistributor.sol 58: mapping(address => mapping(uint256 => uint256)) public feesTrader; // fees accumulated by address of trader per epoch 59: mapping(address => mapping(uint256 => uint256)) public feesExchange; // fees accumulated by exchange of trader per epoch
File: contracts/vote-escrow/VoteEscrowCore.sol 331: /// @dev Mapping from owner address to count of his tokens. 332: mapping(address => uint256) internal ownerToNFTokenCount; 334: /// @dev Mapping from owner address to mapping of index to tokenIds 335: mapping(address => mapping(uint256 => uint256)) internal ownerToNFTokenIdList; 340: /// @dev Mapping from owner address to mapping of operator addresses. 341: mapping(address => mapping(address => bool)) internal ownerToOperators;
payable
If a function modifier such as onlyOwner
is used, the function will revert if a normal user tries to pay the function. Marking the function as payable
will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are
CALLVALUE
(2),DUP1
(3),ISZERO
(3),PUSH2
(3),JUMPI
(10),PUSH1
(3),DUP1
(3),REVERT
(0),JUMPDEST
(1),POP
(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost
There are 12 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L36
File: contracts/governance/GolomToken.sol 36: function mint(address _account, uint256 _amount) external onlyMinter { 42: function mintAirdrop(address _airdrop) external onlyOwner { 50: function mintGenesisReward(address _rewardDistributor) external onlyOwner { 58: function setMinter(address _minter) external onlyOwner { 65: function executeSetMinter() external onlyOwner {
File: contracts/rewards/RewardDistributor.sol 285: function changeTrader(address _trader) external onlyOwner { 291: function executeChangeTrader() external onlyOwner { 298: function addVoteEscrow(address _voteEscrow) external onlyOwner { 308: function executeAddVoteEscrow() external onlyOwner {
File: contracts/core/GolomTrader.sol 444: function setDistributor(address _distributor) external onlyOwner { 454: function executeSetDistributor() external onlyOwner {
File: contracts/vote-escrow/VoteEscrowDelegation.sol 260: function changeMinVotingPower(uint256 _newMinVotingPower) external onlyOwner {
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas. Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L48
File: contracts/rewards/RewardDistributor.sol 48: uint256 constant dailyEmission = 600000 * 10**18; 57: uint256 constant secsInDay = 24 * 60 * 60;
File: contracts/vote-escrow/VoteEscrowCore.sol 296: uint256 internal constant MAXTIME = 4 * 365 * 86400; 297: int128 internal constant iMAXTIME = 4 * 365 * 86400;
<array>.length
should not be looked up in every loop of a for-loopThe overheads outlined below are PER LOOP, excluding the first loop
MLOAD
(3 gas)CALLDATALOAD
(3 gas)Caching the length changes each of these to a DUP<N>
(3 gas), and gets rid of the extra DUP<N>
needed to store the stack offset
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L143
File: contracts/rewards/RewardDistributor.sol 143: for (uint256 index = 0; index < epochs.length; index++) { 157: for (uint256 index = 0; index < epochs.length; index++) { 180: for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { 183: for (uint256 index = 0; index < epochs.length; index++) {
File: contracts/core/GolomTrader.sol 415: for (uint256 i = 0; i < proof.length; i++) {
File: contracts/vote-escrow/VoteEscrowDelegation.sol 171: for (uint256 index = 0; index < delegated.length; index++) { 189: for (uint256 index = 0; index < delegatednft.length; index++) { 199: for (uint256 i; i < _array.length; i++) {
I suggest using the storage
keyword instead of the memory
one, as the copy in memory is wasting some MSTOREs and MLOADs.
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L925
File: contracts/vote-escrow/VoteEscrowCore.sol 1083: LockedBalance memory _locked = locked[_tokenId]; 1136: Point memory point_1 = point_history[_epoch + 1];
File: contracts/vote-escrow/VoteEscrowDelegation.sol 101: Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1];
should use storage
, because each struct field is read only once
require()
/revert()
strings longer than 32 bytes cost extra gasEach extra chunk of byetes past the original 32 iincurs an MSTORE which costs 3 gas There are 8 instances of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/governance/GolomToken.sol#L24
File: contracts/governance/GolomToken.sol 24: require(msg.sender == minter, 'GolomToken: only reward distributor can enable');
File: contracts/rewards/RewardDistributor.sol 181: require(tokenowner == ve.ownerOf(tokenids[tindex]), 'Can only claim for a single Address together'); 292: require(traderEnableDate <= block.timestamp, 'RewardDistributor: time not over yet'); 309: require(voteEscrowEnableDate <= block.timestamp, 'RewardDistributor: time not over yet');
File: contracts/vote-escrow/VoteEscrowDelegation.sol 73: require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power');
File: contracts/vote-escrow/VoteEscrowCore.sol 929: require(_locked.end > block.timestamp, 'Cannot add to expired lock. Withdraw'); 945: require(unlock_time > block.timestamp, 'Can only lock until time in the future'); 983: require(_locked.end > block.timestamp, 'Cannot add to expired lock. Withdraw');
Remove or replace unused variables to save deployment gas. Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L4
File: contracts/vote-escrow/VoteEscrowCore.sol 319: string public constant version = '1.0.0'; 320: uint8 public constant decimals = 18;
immutable
Avoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32
(3 gas). If getters are still desired, '_' can be added to the variable name and the getter can be added manually
There is 3 instance of this issue: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L143
File: contracts/rewards/RewardDistributor.sol 68: ERC20 public rewardToken; 69: ERC20 public weth;
File: contracts/vote-escrow/VoteEscrowCore.sol 300: address public token;
Variables with values known at compile time and that do not change at runtime should be declared as constant
There is 2 instance of this issue:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L46
File: contracts/rewards/RewardDistributor.sol 46: uint256 public startTime; // timestamp at which the contracts need to be activated
File: contracts/core/GolomTrader.sol 45: ERC20 WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
Caching will replace each Gwarmaccess (100 gas) with a much cheaper stack read.
Less obvious fixes/optimizations include having local storage variables of mappings within state variable mappings or mappings within state variable structs, having local storage variables of structs within mappings, having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L98-L138
rewardToken
accessed 7 times;
epoch
accessed 15 times;
ve
accessed 2 times;
epochTotalFee[epoch]
accessed 2 times;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L144
epoch
accessed N times, each loop iteration, where N = epochs.length
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L158
epoch
accessed N times, each loop iteration, where N = epochs.length
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L172-L210
ve
accessed 8 times
epochBeginTime[epochs[index]]
accessed 4 times
epoch
accessed N times, each loop iteration, where N = epochs.length
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L242-L269
distributor
accessed two times;
copy distributor
to stack variable:
Distributor _distributor = distributor
;
use _distributor
instead of distributor
;
hardhat-gas-reporter
results
before fix:
Methods | |||||
---|---|---|---|---|---|
Contract | Method | min | max | avg | #calls |
GolomTrader | fillAsk | 238153 | 241948 | 241401 | 7 |
after fix: | |||||
Methods | |||||
- | :- | :-: | - | :- | :-: |
Contract | Method | min | max | avg | #calls |
GolomTrader | fillAsk | 238052 | 241847 | 241300 | 7 |
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L382-L383
WETH
accessed two times;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L384-L402
distributor
accessed two times;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L138-L139
checkpoints[nftId][nCheckpoints - 1]
accessed two times;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L213-L214
checkpoint.delegatedTokenIds
accessed two times;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L644-L650
idToOwner[_tokenId]
accessed two times;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L948-L949
tokenId
accessed two times;
private
rather than public
for constants, saves gasIf needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table Instances include: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L317
File: contracts/vote-escrow/VoteEscrowCore.sol 317: string public constant name = 'veNFT'; 318: string public constant symbol = 'veNFT'; 319: string public constant version = '1.0.0'; 320: uint8 public constant decimals = 18;
There is a peace of code that does not need to exist: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L177
File: contracts/core/GolomTrader.sol 177: require(signaturesigner == o.signer, 'invalid signature'); 178: if (signaturesigner != o.signer) { 179: return (0, hashStruct, 0); //@audit unreachable code 180: }
Check inside if
is pointless, because signaturesigner != o.signer
can not be true
, when if it is, then transaction is already reverted because of line 177.
hardhat-gas-reporter
shows deployment gas difference from 2013842
to 2001108
.
When we have a code expressionA || expressionB
if expressionA
is true then expressionB
will not be evaluated and gas saved;
Instances include:
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L650
File: contracts/vote-escrow/VoteEscrowCore.sol 650: bool senderIsOwner = (idToOwner[_tokenId] == msg.sender); 651: bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender]; 652: require(senderIsOwner || senderIsApprovedForAll);
Variables bool senderIsOwner
and bool senderIsApprovedForAll
just add more work,
it should be:
require((idToOwner[_tokenId] == msg.sender) || (ownerToOperators[owner])[msg.sender];
so if (idToOwner[_tokenId] == msg.sender)
is true
we will not do SLOAD to get (ownerToOperators[owner])[msg.sender]
;
saving runtime and deployment gas;
same thing here: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L439
File: contracts/vote-escrow/VoteEscrowCore.sol 439: bool spenderIsOwner = owner == _spender; 440: bool spenderIsApproved = _spender == idToApprovals[_tokenId]; 441: bool spenderIsApprovedForAll = (ownerToOperators[owner])[_spender]; 442: return spenderIsOwner || spenderIsApproved || spenderIsApprovedForAll;
But this one is a view
function, so only deployment gas saved.
tokenId
second timeFile: contracts/vote-escrow/VoteEscrowCore.sol 948: ++tokenId; 949: uint256 _tokenId = tokenId;
Change it to uint256 _tokenId = ++tokenId;
and 92 gas is saved this way
last_point
value rewritten right after initializationIf _epoch > 0
, then last_point
is rewritten, and initialization on L726 becomes waste of gas;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#726
File: contracts/vote-escrow/VoteEscrowCore.sol 726: Point memory last_point = Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number}); 727: if (_epoch > 0) { 728: last_point = point_history[_epoch]; 729: }
Point memory last_point = _epoch > 0 ? last_point = point_history[_epoch] : Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number});
File: contracts/vote-escrow/VoteEscrowCore.sol 840: LockedBalance memory _locked = locked_balance; 1164: function _supply_at(Point memory point, uint256 t) internal view returns (uint256) { 1165: Point memory last_point = point;
locked_balance
is of same type and never used after this. Same thing with point
;
There is no need in copy of it, but gas for making a copy is spent;
There is a peace of code that is duplicated in bo blocks of if
else
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#467
467: if (current_count == current_index) { // update ownerToNFTokenIdList ownerToNFTokenIdList[_from][current_count] = 0; // update tokenToOwnerIndex tokenToOwnerIndex[_tokenId] = 0; } else { uint256 lastTokenId = ownerToNFTokenIdList[_from][current_count]; // Add // update ownerToNFTokenIdList ownerToNFTokenIdList[_from][current_index] = lastTokenId; // update tokenToOwnerIndex tokenToOwnerIndex[lastTokenId] = current_index; // Delete // update ownerToNFTokenIdList ownerToNFTokenIdList[_from][current_count] = 0; // update tokenToOwnerIndex tokenToOwnerIndex[_tokenId] = 0; }
To reduce deployment gas should be:
if (current_count != current_index) { uint256 lastTokenId = ownerToNFTokenIdList[_from][current_count]; // Add // update ownerToNFTokenIdList ownerToNFTokenIdList[_from][current_index] = lastTokenId; // update tokenToOwnerIndex tokenToOwnerIndex[lastTokenId] = current_index; } // update ownerToNFTokenIdList ownerToNFTokenIdList[_from][current_count] = 0; // update tokenToOwnerIndex tokenToOwnerIndex[_tokenId] = 0;
supportedInterfaces
to existThis mapping takes some space and every time supportsInterface(bytes4)
is called we do SLOAD
to get data from this mapping.
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L368
File: contracts/vote-escrow/VoteEscrowCore.sol 368: function supportsInterface(bytes4 _interfaceID) external view returns (bool) { 369: return supportedInterfaces[_interfaceID]; 370: }
I suggest we remove the mapping and change the function supportsInterface(bytes4)
to:
368: function supportsInterface(bytes4 _interfaceID) external virtual view returns (bool) { 369: return _interfaceID == ERC165_INTERFACE_ID || _interfaceID == ERC721_INTERFACE_ID || _interfaceID == ERC721_METADATA_INTERFACE_ID 370: }
This way we don't need the mapping supportedInterfaces
and no SLOAD
s done;
If inherited contracts support other interfaces or don't support these just override this function and add/remove conditions;
supply_before - value
calculated twice, also it is checked arithmetic operation, so cheaper to store result in a stack variable than calculate it twice;
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L1015-L1029
File: contracts/vote-escrow/VoteEscrowCore.sol 1015: uint256 supply_before = supply; 1016: supply = supply_before - value; 1029: emit Supply(supply_before, supply_before - value);
We could store result to stack variable and use it instead:
1016: uint256 supply_now = supply = supply_before - value; 1029: emit Supply(supply_before, supply_now);
These values can be accessed via kewords, there is no benefit in having these constants: https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/rewards/RewardDistributor.sol#L57
File: contracts/rewards/RewardDistributor.sol 57: uint256 constant secsInDay = 24 * 60 * 60;
We can use 1 days
instead of secsInDay
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L4
File: contracts/vote-escrow/VoteEscrowCore.sol 295: uint256 internal constant WEEK = 1 weeks;
We can use 1 weeks
instead of WEEK
end
can be of type uint128
Variable end
can be of type uint128
to take less place and save one storage slot.
A lot of protocols even use uint64
for dates to reduce gas usage.
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowCore.sol#L270-L273
File: contracts/vote-escrow/VoteEscrowCore.sol 270: struct LockedBalance { 271: int128 amount; 272: uint256 end; //@audit can be uint128 to save storage slot 273: }
it can be
270: struct LockedBalance { 271: int128 amount; 272: uint128 end; 273: }