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: 24/179
Findings: 5
Award: $498.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
93.2805 USDC - $93.28
In GolomTrader
contract, the seller gets paid less than he is entitled to, if both the following points match:
As we will be proving, the seller losses amount-1
times the actual protocol fee charged by the contract, where amount
is the number of ERC1155 tokens being traded in a single transaction.
This unsent fee is stuck in the GolomTrader contract, as there is no way to directly take away any eth stuck in the contract.
The fillBid function and fillCriteriaBid function in GolomTrader.sol calls an internal function called _settleBalances. This function takes the payment from the order.signer
and sends it to various recipients such as protocol fee in eth to RewardDistributor
, eth as exchange fees, extra payments, royalties etc.
Looking at the function, we calculate the protocol fee as follows:
//Note that the fee for all the tokens are calculated here by multiplying by "amount". uint256 protocolfee = ((o.totalAmt * 50) / 10000) * amount;
Then in the next few lines, we send eth to various addresses, which are correctly implemented. The mistake lies in the section where the seller(msg.sender
) is sent his fees.
if (o.refererrAmt > 0 && referrer != address(0)) { payEther(o.refererrAmt * amount, referrer); payEther( here ---> (o.totalAmt - protocolfee - o.exchange.paymentAmt - o.prePayment.paymentAmt - o.refererrAmt) * amount - p.paymentAmt, msg.sender ); } else { payEther( here ---> (o.totalAmt - protocolfee - o.exchange.paymentAmt - o.prePayment.paymentAmt) * amount - p.paymentAmt, msg.sender ); }
See the two marked lines. The protocolfee
previously calculated is subtracted from o.totalAmt
(which was what we took from the order.signer
in line 382.
When the protocolfee was calculated, we had already multiplied it by amount
. But then this value is subtracted from o.totalAmt
and then gets multiplied by amount
again. Which reduces the eth sent to the seller, which is msg.sender in this case.
#fillBid Actual protocol fee: 0.0151 Expected Profit for maker(ETH): 0.2549 Actual ETH transferred to maker: 0.2398 Funds lost to maker and stuck forever in Trader contract(ETH): 0.0151 Funds lost divided by Actual protocol fee: 1 ✓ fillBid tested with ERC1155 token- 2 tokens Actual protocol fee: 0.03775 Expected Profit for maker(ETH): 1.01225 Actual ETH transferred to maker: 0.86125 Funds lost to maker and stuck forever in Trader contract(ETH): 0.151 Funds lost divided by Actual protocol fee: 4 ✓ fillBid tested with ERC1155 token- 5 tokens Actual protocol fee: 0.0755 Expected Profit for maker(ETH): 2.2745 Actual ETH transferred to maker: 1.595 Funds lost to maker and stuck forever in Trader contract(ETH): 0.6795 Funds lost divided by Actual protocol fee: 9 ✓ fillBid tested with ERC1155 token- 10 tokens
You can see that we did 3 tests, one with 2 tokens, another with 5 tokens and another with 10 tokens. And in each test,
( Funds Lost by the User
/ Actual Protocol Fee
) is 1, 4 and 9 respectively. which is number of tokens-1
.
The GolomTrader
contract has the weth contract address hardcoded in it. For testing it, I had to pass the weth contract address from Hardhat during run time. So I made a small change in the GolomTrader contract to accept weth address in its constructor.
constructor(address _governance, address _weth) { WETH = ERC20(_weth);
The diff between the provided GolomTrader.specs.ts file and the new one I used is here: diff file.
Hardhat, VSCode.
Correct the formulas in this section as give below:
if (o.refererrAmt > 0 && referrer != address(0)) { payEther(o.refererrAmt * amount, referrer); payEther( (o.totalAmt - o.exchange.paymentAmt - o.prePayment.paymentAmt - o.refererrAmt) * amount - protocolfee - p.paymentAmt, msg.sender ); } else { payEther( (o.totalAmt - o.exchange.paymentAmt - o.prePayment.paymentAmt) * amount - protocolfee - p.paymentAmt, msg.sender ); }
#0 - KenzoAgada
2022-08-02T06:32:02Z
Duplicate of #240
In VoteEscrowDelegation
contract, the _writeCheckpoint internal function has major logic errors which will make the delegate external function to always revert.
Suppose an user calls the delegate function for the first time for a particular toTokenId
. Since its the first time, the nCheckpoints
for that token will be zero. Which means the else
part of the condition will be executed. Here, we call the internal function _writeCheckpoint
with a value of 0
for nCheckpoints
parameter. Pasted below is the delegate function code with some extra comments:
function delegate(uint256 tokenId, uint256 toTokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power'); delegates[tokenId] = toTokenId; --->>> uint256 nCheckpoints = numCheckpoints[toTokenId]; //this will be zero for the first time. if (nCheckpoints > 0) { Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1]; checkpoint.delegatedTokenIds.push(tokenId); _writeCheckpoint(toTokenId, nCheckpoints, checkpoint.delegatedTokenIds); --->>> } else { // so control goes here. uint256[] memory array = new uint256[](1); array[0] = tokenId; --->>> _writeCheckpoint(toTokenId, nCheckpoints, array); //internal function called with 0 value for nCheckpoints parameter } emit DelegateChanged(tokenId, toTokenId, msg.sender); }
Now looking at the _writeCheckpoint
internal function:
function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); //see the line below. "nCheckpoints - 1" is used as an index. Which results in underflow panic error. -->>> Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) { oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; } else { checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); numCheckpoints[toTokenId] = nCheckpoints + 1; } }
<ins>The test in hardhat returns the following error message:</ins>
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:102) at VoteEscrow.delegate (contracts/vote-escrow/VoteEscrowDelegation.sol:87) at async HardhatNode._mineBlockWithPendingTxs (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:1772:23) at async HardhatNode.mineBlock (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:466:16) at async EthModule._sendTransactionAndReturnHash (node_modules/hardhat/src/internal/hardhat-network/provider/modules/eth.ts:1496:18) at async HardhatNetworkProvider.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:118:18) at async EthersProviderWrapper.send (node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:20)
<ins>Test script used to test the code in Hardhat:</ins>
import { ethers, waffle } from 'hardhat'; import { BigNumber, utils, Signer, constants } from 'ethers'; import chai from 'chai'; const { expect } = chai; // import typings import { VoteEscrow as VoteEscrowTypes } from '../typechain/VoteEscrow'; import { ERC20Mock as ERC20MockTypes } from '../typechain/ERC20Mock'; let testErc20: ERC20MockTypes; let veDelegation: VoteEscrowTypes; let accounts: Signer[]; describe('VoteEscrowDelegation.sol', function () { beforeEach(async function () { accounts = await ethers.getSigners(); const Lib = await ethers.getContractFactory("TokenUriHelper"); const lib = await Lib.deploy(); await lib.deployed(); const VoteEscrowArtifacts = ethers.getContractFactory('VoteEscrow',{ libraries: { TokenUriHelper : lib.address, }, }); const ERC20MockArtifacts = ethers.getContractFactory('ERC20Mock'); testErc20 = (await (await ERC20MockArtifacts).deploy()) as ERC20MockTypes; veDelegation = (await (await VoteEscrowArtifacts).deploy(testErc20.address)) as VoteEscrowTypes; }); describe('#Delegate', () => { it('adding a delegate', async () => { const receiver = await accounts[2].getAddress(); //the one who owns golom tokens await testErc20.connect(accounts[0]).mint(receiver, 10000) //receiver gets 10000 golem tokens //VoteEscrowDelegation contract is allowed to withdraw receiver's tokens on his behalf await testErc20.connect(accounts[2]).approve(veDelegation.address, 10000) // the receiver address locks 100 golom tokens for a period of 1000000. // create_lock(uint256 _value, uint256 _lock_duration) external let nftid = await veDelegation.connect(accounts[2]).create_lock( 100, 1000000, ) // then he calls the VoteEscrowDelegation contract's delegate function await veDelegation.connect(accounts[2]).delegate( 1, nftid.value, ) }); }); });
Along with this obvious error, there is one more logical error which was noticed in the _writeCheckpoint
function. If the control goes into the if
part of the statement, the function doesn't cause any state changes. As the oldCheckpoint
variable it modifies is a memory
variable. So any changes will be erased when the function is done executing.
function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); --->>> Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) { --->>> oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; } else { checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); numCheckpoints[toTokenId] = nCheckpoints + 1; } }
Hardhat, VSCode.
Correct the _writeCheckpoint function as shown below to solved the pointed out errors:
function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); if (nCheckpoints > 0) { Checkpoint storage oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; if(oldCheckpoint.fromBlock == block.number) { oldCheckpoint.delegatedTokenIds = _delegatedTokenIds; } } else { checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds); numCheckpoints[toTokenId] = nCheckpoints + 1; } }
#0 - KenzoAgada
2022-08-02T09:16:04Z
Underflow is duplicate of #630 Same-block no-storage-update is duplicate of #455 Respect for the warden for providing a test
🌟 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 |
---|---|
1 | Add a function to recover extra ether stuck in GolomTrader |
2 | Conditions inside require doesnt match function logic |
3 | Timelock mechanism could be improved to be more secure and transparent |
4 | Compare the value of startTime with block.timestamp in RewardDistributor.sol |
5 | Anyone can claim Trader/Exchange rewards on their owner's behalf(without permission) |
6 | Missing timelock for MIN_VOTING_POWER_REQUIRED in VoteEscrowDelegation |
7 | Events are not emitted for Important state changes |
8 | Once an order is cancelled it cannot be placed again(GolomTrader.sol) |
GolomTrader
</ins>Though the contract is doing everything it can, to have a zero balance in the GolomTrader
contract, there can be unseen issues with the code which might result in eth getting stuck in the contract. An example would be the bug in the _settleBalances
function where less eth is sent to the msg.sender than supposed to(I have submitted this as a separate finding marking it as a high risk issue) and the extra eth continues to stay in the contract without being able to be withdrawn.
In such cases it would be nice to have a function which can withdraw the funds and use it among the community or for a wholesome purpose.
There are two instances of this:
a) <ins>The following require statement in fillBid
function in GolomTrader.sol doesn't match the function logic</ins>
require( o.totalAmt * amount > (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt) * amount + p.paymentAmt );
The protocol fee of 0.5% is not checked within the require
statement. But at the end of function we call _settleBalances
function where the protocol fee is taken out from this totalAmt*amount.
This will cause the require
statement to pass, only to revert on a much later stage, wasting gas of the trader.
<ins>Mitigation</ins> would be to alter the require conditions as below:
require( o.totalAmt * amount > (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt + (o.totalAmt * 50) / 10000) * amount + p.paymentAmt );
b) <ins>A similar issue is found in the fillCriteriaBid
function too</ins>
require(o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt);
Here the extra payment p
and the protocol fees are not included in the conditions mentioned inside the require statement.
<ins>Mitigation would be to change the statement as follows:</ins>
require( o.totalAmt * amount > (o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt + (o.totalAmt * 50) / 10000) * amount + p.paymentAmt );
In fact all the three order types could have the same require conditions.
The timelock mechanism could have the following improvements:
<ins>a) Add an expiry timestamp to the timelock proposal</ins> This removes the possibility of an old proposal suddenly getting executed. Circumstances might have changed from the time when the proposal was made.
An example mitigation would be the setDistributor
function from the GolomTrader.sol
:
function setDistributor(address _distributor) external onlyOwner { if (address(distributor) == address(0)) { distributor = Distributor(_distributor); } else { pendingDistributor = _distributor; distributorEnableDate = block.timestamp + 1 days; distributorDisableDate = block.timestamp + 2 days; //see this. new expiry parameter added } }
And the corresponding executeSetDistributor
function is also changed as shown below:
function executeSetDistributor() external onlyOwner { require(distributorEnableDate <= block.timestamp, 'LOCKED'); require(distributorDisableDate >= block.timestamp, 'EXPIRED'); //new require statement. EXPIRED distributor = Distributor(pendingDistributor); }
In the above example the proposal needs to be activated within one day of being unlocked.
There are 3 instances of this:
<ins>b) An event should be emitted when a proposal is made and is executed</ins> This leads to a much more transparent system.
<ins>c) Older proposal can be overwritten by a new one without any warning</ins> Not sure if it was the intention of the developers, but it is something to be kept in mind.
The constructor in RewardDistributor.sol
initializes startTime
state variable with a magic number. I am assuming that this was done expecting the contract will be launched on a set date. But unexpected things can happen and its safer to apply some conditions programmatically to make sure we dont have the wrong value in there. For example:
uint256 _starttime = 1659211200; require(_starttime >= block.timestamp && _starttime <= block.timestamp + 1 days, "starttime out of range"); starttime = _starttime;
Setting the wrong value of starttime will cause the reward cycles to be calculated wrongly in the addFee function. This will avoid those complications, if the developer decides to deploy the contract earlier or later than originally intended.
In RewardDistributor.sol contract, a trader or exchange can claim their GOLOM token rewards with help of traderClaim
and exchangeClaim
public functions respectively.
These functions can be called by anyone and the contract will calculate the rewards belonging to the addr
input for the given epoch
's and send them to the particular address. Though this cannot result in loss of funds, it can be an unpleasant experience to the user that anybody can initiate his/her fund transfer without permission.
<ins>Mitigation</ins> would be to either check if the msg.sender
and addr
are the same. Or create an approval mapping to make sure that only a particular address can withdraw the funds for a particular trader/exchange address.
VoteEscrowDelegation
</ins>As an important protocol parameter I suggest that a time lock functionality is added to the changeMinVotingPower function. Point 3 above could be referred for implementing a better timelock system.
The following functions doesn't emit an event upon completion:
GolomTrader.sol
)</ins>In GolomTrader.sol
, an order can be cancelled through the following cancelOrder function.
function cancelOrder(Order calldata o) public nonReentrant { require(o.signer == msg.sender); (, bytes32 hashStruct, ) = validateOrder(o); filled[hashStruct] = o.tokenAmt + 1; emit OrderCancelled(hashStruct); }
The order is cancelled by making the filled
value by 1 more than the o.tokenAmt
.
If the user decides to place the order with the same parameters again, he/she won't be able to do it. As the validateOrder
function will return a status value of 2, instead of 3, thus reverting the transaction.
Though this can be resolved by the user by changing the order parameters by a bit, it can lead to an unpleasant user experience in certain cases.
<ins>Mitigation</ins> would be to allow the user to cancel the cancellation of an order.
 | Issue |
---|---|
1 | Events should be emitted at the end |
2 | constant's should be defined rather than using magic numbers |
3 | Spelling mistakes in Natspec comments |
4 | Missing Natspec comments |
5 | require() statements should have descriptive reason strings |
There are two instances of this.
In fillBid
and fillCriteriaBid
functions, the event is emitted before calling the _settleBalances
function. To save any extra gas consumption on a reversal, its recommended to emit events at the end of the function, unless there is a specific reason not to do so.
a) fillBid function:
emit OrderFilled(msg.sender, o.signer, 1, hashStruct, o.totalAmt * amount); _settleBalances(o, amount, referrer, p);
emit OrderFilled(msg.sender, o.signer, 2, hashStruct, o.totalAmt * amount); _settleBalances(o, amount, referrer, p);
<ins>Mitigation</ins> would be to change the order of the above statements. That is to place the emit statement after calling the _settleBalances
function.
constant
's should be defined rather than using magic numbers</ins>There are 6 instances of this spread out in 4 files.
File: contracts/governance/GolomToken.sol // mintAirdrop function 44: _mint(_airdrop, 150_000_000 * 1e18); // mintGenesisReward function 52: _mint(_rewardDistributor, 62_500_000 * 1e18);
File: contracts/vote-escrow/VoteEscrowDelegation.sol // _writeCheckpoint function 99: require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); // mintGenesisReward function 52: _mint(_rewardDistributor, 62_500_000 * 1e18);
File: contracts/rewards/RewardDistributor.sol // addFee function 100: if (rewardToken.totalSupply() > 1000000000 * 10**18) {
File: contracts/core/GolomTrader.sol // the protocol fee of "50" is used at several places 381: uint256 protocolfee = ((o.totalAmt * 50) / 10000) * amount;
There are 11 instances of this spread out in 3 files:
File: contracts/rewards/RewardDistributor.sol 95: facilated 168: there
File: contracts/core/GolomTrader.sol 201: wanna, succesful 278: wanna, succesful 333: wanna, succesful 370: succesfully, succesful
File: contracts/vote-escrow/VoteEscrowDelegation.sol 227: Exeute
There are 11 instances of this spread out in 2 files:
File: contracts/rewards/RewardDistributor.sol 70: event NewEpoch(uint256 indexed epochNo, uint256 tokenMinted, uint256 rewardStaker, uint256 previousEpochFee); 141: function traderClaim(address addr, uint256[] memory epochs) public { 155: function exchangeClaim(address addr, uint256[] memory epochs) public { //missing natspec comments on return values. 215: function stakerRewards(uint256 tokenid) public view returns ( 254: function traderRewards(address addr) public view returns ( 269: function exchangeRewards(address addr) public view returns (
File: contracts/core/GolomTrader.sol 79: event NonceIncremented(address indexed maker, uint256 newNonce); 81: event OrderFilled( address indexed maker, address indexed taker, uint256 indexed orderType, bytes32 orderHash, uint256 price ); 89: event OrderCancelled(bytes32 indexed orderHash); 164: function validateOrder(Order calldata o) 334: function fillCriteriaBid(
require()
statements should have descriptive reason strings</ins>There are 17 instances of this spread out in 3 files:
File: contracts/rewards/RewardDistributor.sol 88 : require(msg.sender == trader); 144: require(epochs[index] < epoch); 158: require(epochs[index] < epoch);
File: contracts/core/GolomTrader.sol 211: require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm'); //mgmtm??? 220: require(msg.sender == o.reservedAddress); 285: require( o.totalAmt * amount > (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); 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));
🌟 Selected for report: JohnSmith
Also found by: 0x1f8b, 0xA5DF, 0xDjango, 0xKitsune, 0xLovesleep, 0xNazgul, 0xSmartContract, 0xmatt, 0xsam, Aymen0909, Bnke0x0, CRYP70, Chandr, Chinmay, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, Funen, GalloDaSballo, Green, IllIllI, JC, Jmaxmanblue, Junnon, Kaiziron, Kenshin, Krow10, Maxime, Migue, MiloTruck, Noah3o6, NoamYakov, Randyyy, RedOneN, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StyxRave, TomJ, Tomio, _Adam, __141345__, ajtra, ak1, apostle0x01, asutorufos, async, benbaessler, brgltd, c3phas, cRat1st0s, carlitox477, delfin454000, djxploit, durianSausage, ellahi, erictee, fatherOfBlocks, gerdusx, gogo, hyh, jayfromthe13th, jayphbee, joestakey, kaden, kenzo, kyteg, ladboy233, lucacez, m_Rassska, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, rfa, robee, rokinot, sach1r0, saian, samruna, sashik_eth, simon135, supernova, tofunmi, zuhaibmohd
21.3211 USDC - $21.32
 | Issue |
---|---|
1 | Cache calculations, which are used more than once |
2 | for loops can be gas optimized |
3 | Use custom errors rather than require() strings to save gas |
An example would be the use of the below formula:
(o.totalAmt * 50) / 10000
which is used several times in the fillAsk
function in GolomTrader.sol
. We could cache it as shown below:
uint256 protocolFee = (o.totalAmt * 50) / 10000; // and then use the above variable in all the subsequent equations. // for example instead of payEther(((o.totalAmt * 50) / 10000) * amount, address(distributor)); payEther(protocolFee * amount, address(distributor)); //we can use this
There are 5 places where we could replace this:
Line 211: require( o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt + (o.totalAmt * 50) / 10000, 'amt not matching' ); Line 242: payEther(((o.totalAmt * 50) / 10000) * amount, address(distributor)); Line 252: payEther( (o.totalAmt - (o.totalAmt * 50) / 10000 - o.exchange.paymentAmt - o.prePayment.paymentAmt - o.refererrAmt) * amount, o.signer ); } else { Line 254: payEther( (o.totalAmt - (o.totalAmt * 50) / 10000 - o.exchange.paymentAmt - o.prePayment.paymentAmt) * amount, o.signer ); } Line 269: distributor.addFee([o.signer, o.exchange.paymentAddress], ((o.totalAmt * 50) / 10000) * amount);
<ins>Hardhat Gas report:</ins> | | Deployment cost of GolomTrader | fillAsk function | -- | -- | -- Before the change: | 2013842 | 241406| After the change : | 1992605 | 240923| Saved gas fees : | 21237 | 483|
Similarly in fillBid
function, the following pairs of statements can be combined into one, as shown below:
ERC721 nftcontract = ERC721(o.collection); nftcontract.transferFrom(msg.sender, o.signer, o.tokenId); //becomes ERC721(o.collection).transferFrom(msg.sender, o.signer, o.tokenId); ERC1155 nftcontract = ERC1155(o.collection); nftcontract.safeTransferFrom(msg.sender, o.signer, o.tokenId, amount, ''); //becomes ERC1155(o.collection).safeTransferFrom(msg.sender, o.signer, o.tokenId, amount, '');
The same statements were changed in the fillCriteriaBid function too generate the below report. <ins>Hardhat Gas report:</ins>*
Deployment cost of GolomTrader | |
---|---|
Before the change: | 1992605 |
After the change : | 1986178 |
Saved gas fees : | 6427 |
*Due to non-availability of test code, gas savings for fillBid
and fillCriteriaBid
functions were not calculated.
<ins>RewardDistributor</ins>
In RewardDistributor
contract, in the addFee function, the following state variables could be cached: epoch
(accessed 14 times), rewardToken.totalSupply()
(accessed 4 times) and rewardToken.balanceOf(address(ve))
(accessed 2 times).
Storage read costs 100 gas while memory read is only 3 gas, saving us 97 gas approximately per access. So with the following changes, we could save at least (13+3+1)*97 = <ins>1649</ins> gas.
index c475948..d152df4 100755 --- a/original_addFee.sol +++ b/modified_addFee.sol @@ -1,41 +1,44 @@ function addFee(address[2] memory addr, uint256 fee) public onlyTrader { //console.log(block.timestamp,epoch,fee); - if (rewardToken.totalSupply() > 1000000000 * 10**18) { + uint256 tokenSupply = rewardToken.totalSupply(); // changed in 4 places + if (tokenSupply > 1000000000 * 10**18) { // if supply is greater then a billion dont mint anything, dont add trades return; } // if 24 hours have passed since last epoch change + uint256 epochCached = epoch; //changed in 1 places if (block.timestamp > startTime + (epoch) * secsInDay) { // this assumes atleast 1 trade is done daily?????? // logic to decide how much token to emit // emission = daily * (1 - (balance of locker/ total supply)) full if 0 locked and 0 if all locked // uint256 tokenToEmit = dailyEmission * rewardToken.balanceOf()/ // emissions is decided by epoch begiining locked/circulating , and amount each nft gets also decided at epoch begining - uint256 tokenToEmit = (dailyEmission * (rewardToken.totalSupply() - rewardToken.balanceOf(address(ve)))) / - rewardToken.totalSupply(); - uint256 stakerReward = (tokenToEmit * rewardToken.balanceOf(address(ve))) / rewardToken.totalSupply(); + uint256 veBalance = rewardToken.balanceOf(address(ve)); // changed in 2 places + uint256 tokenToEmit = (dailyEmission * (tokenSupply - veBalance)) / + tokenSupply; + uint256 stakerReward = (tokenToEmit * veBalance) / tokenSupply; // deposit previous epoch fee to weth for distribution to stakers - uint256 previousEpochFee = epochTotalFee[epoch]; - epoch = epoch + 1; - rewardStaker[epoch] = stakerReward; - rewardTrader[epoch] = ((tokenToEmit - stakerReward) * 67) / 100; - rewardExchange[epoch] = ((tokenToEmit - stakerReward) * 33) / 100; + uint256 previousEpochFee = epochTotalFee[epochCached]; + epoch = ++epochCached; + rewardStaker[epochCached] = stakerReward; + rewardTrader[epochCached] = ((tokenToEmit - stakerReward) * 67) / 100; + rewardExchange[epochCached] = ((tokenToEmit - stakerReward) * 33) / 100; rewardToken.mint(address(this), tokenToEmit); - epochBeginTime[epoch] = block.number; + epochBeginTime[epochCached] = block.number; if (previousEpochFee > 0) { - if (epoch == 1){ + if (epochCached == 1){ epochTotalFee[0] = address(this).balance; // staking and trading rewards start at epoch 1, for epoch 0 all contract ETH balance is converted to staker rewards rewards. weth.deposit{value: address(this).balance}(); }else{ weth.deposit{value: previousEpochFee}(); } } - emit NewEpoch(epoch, tokenToEmit, stakerReward, previousEpochFee); + emit NewEpoch(epochCached, tokenToEmit, stakerReward, previousEpochFee); } - feesTrader[addr[0]][epoch] = feesTrader[addr[0]][epoch] + fee; - feesExchange[addr[1]][epoch] = feesExchange[addr[1]][epoch] + fee; - epochTotalFee[epoch] = epochTotalFee[epoch] + fee; + feesTrader[addr[0]][epochCached] = feesTrader[addr[0]][epochCached] + fee; + feesExchange[addr[1]][epochCached] = feesExchange[addr[1]][epochCached] + fee; + epochTotalFee[epochCached] = epochTotalFee[epochCached] + fee; return; }
See the example below:
for (uint256 i = 0; i < myarray.length; i++) { //code here } //can be optimized to uint256 arrayLength = myarray.length; for (uint256 i; i < arrayLength; ) { //code here unchecked{ ++i; } }
We can optimize the for loop in three ways:
unchecked
{} for increment of loop indices.An example implementation of this in remix shows that every iteration can save around 75 gas.
There are 11 instances of this:
File: contracts/core/GolomTrader.sol Line 415: for (uint256 i = 0; i < proof.length; i++) File: contracts/rewards/RewardDistributor.sol Line 143: for (uint256 index = 0; index < epochs.length; index++) Line 157: for (uint256 index = 0; index < epochs.length; index++) Line 180: for (uint256 tindex = 0; tindex < tokenids.length; tindex++) Line 183: for (uint256 index = 0; index < epochs.length; index++) Line 226: for (uint256 index = 0; index < epochs.length; index++) Line 258: for (uint256 index = 0; index < epoch; index++) Line 273: for (uint256 index = 0; index < epoch; index++) File: contracts/vote-escrow/VoteEscrowDelegation.sol Line 171: for (uint256 index = 0; index < delegated.length; index++) Line 189: for (uint256 index = 0; index < delegatednft.length; index++) Line 199: for (uint256 i; i < _array.length; i++)
Assuming an average of 5 iterations per loop:
Approximate gas saved = 11 instances
* 5 iterations per loop
* 75 gas in each iteration
= <ins>4125</ins> gas.
See the example implementation to see, how both the deployment and execution gas costs are saved by using custom errors.
The reference code shared above shows that:
I changed 3 require strings into custom errors in the GolomTrader contract, just to test it out. And it showed each change saved around 7000 to 7500 gas. Which could be used to determine the approximate deployment gas savings in our case. And each function with at least one require string will save us a maximum of 55 gas on reversal, when custom errors are used.
The instances below shows where we could apply these gas savings:
File: contracts/core/GolomTrader.sol // 10 require statements spread among 5 functions. // Deployment cost savings: 10 instances * 7000 = 70,000 gas saved. // Gas saved on reversals : 5 functions * 55 = 275 gas saved. Line 177: require(signaturesigner == o.signer, 'invalid signature'); Line 211: require( o.totalAmt >= o.exchange.paymentAmt + o.prePayment.paymentAmt + o.refererrAmt + (o.totalAmt * 50) / 10000, 'amt not matching' ); Line 217: require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm'); Line 222: require(o.orderType == 0, 'invalid orderType'); Line 226: require(status == 3, 'order not valid'); Line 227: require(amountRemaining >= amount, 'order already filled'); Line 235: require(amount == 1, 'only 1 erc721 at 1 time'); Line 299: require(amount == 1, 'only 1 erc721 at 1 time'); Line 359: require(amount == 1, 'only 1 erc721 at 1 time'); Line 455: require(distributorEnableDate <= block.timestamp, 'not allowed'); File: contracts/rewards/RewardDistributor.sol // 7 require statements spread among 4 functions. // Deployment cost savings: 7 instances * 7000 = 49,000 gas saved. // Gas saved on reversals : 4 functions * 55 = (at least) 220 gas saved. Line 173: require(address(ve) != address(0), ' VE not added yet'); Line 181: require(tokenowner == ve.ownerOf(tokenids[tindex]), 'Can only claim for a single Address together'); Line 184: require(epochs[index] < epoch, 'cant claim for future epochs'); Line 185: require(claimed[tokenids[tindex]][epochs[index]] == 0, 'cant claim if already claimed'); Line 220: require(address(ve) != address(0), ' VE not added yet'); Line 292: require(traderEnableDate <= block.timestamp, 'RewardDistributor: time not over yet'); Line 309: require(voteEscrowEnableDate <= block.timestamp, 'RewardDistributor: time not over yet'); File: contracts/vote-escrow/VoteEscrowDelegation.sol // 7 require statements spread among 3 functions. // Deployment cost savings: 7 instances * 7000 = 49,000 gas saved. // Gas saved on reversals : 3 instances * 55 = (at least) 165 gas saved. Line 072: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); Line 073: require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power'); Line 099: require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more'); Line 130: require(blockNumber < block.number, 'VEDelegation: not yet determined'); Line 186: require(blockNumber < block.number, 'VEDelegation: not yet determined'); Line 211: require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); Line 239: require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached');
So total deployment gas saved = 70000 + 49000 + 49000 = 168000. Dynamic gas saved = 275 + 220 + 165 = 660.
 | Optimization | Deployment Gas saved | Dynamic Gas Saved |
---|---|---|---|
1 | Cache frequently used calculations in fillAsk function | 21237 | 483 |
2 | Removing variables which are used only once from fillBid and fillCriteriaBid function | 6427 | N/A |
3 | Caching state variables in addFee function in RewardDistributor | N/A | 1649 |
4 | Gas optimizing "for" loops | N/A | 4125 |
5 | Use custom errors rather than require() strings | 168000 | 660 |
Total Gas Saved | 195664‬ | â€6917 |