Platform: Code4rena
Start Date: 14/07/2022
Pot Size: $25,000 USDC
Total HM: 2
Participants: 63
Period: 3 days
Judge: PierrickGT
Total Solo HM: 1
Id: 147
League: ETH
Rank: 44/63
Findings: 1
Award: $23.75
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, Aymen0909, Chom, Deivitto, ElKu, JC, JohnSmith, Kaiziron, Limbooo, MadWookie, Meera, ReyAdmirado, Rohan16, Sm4rty, SooYa, TomJ, Trumpero, Waze, __141345__, ajtra, ak1, antonttc, bulej93, c3phas, cRat1st0s, csanuragjain, defsec, durianSausage, fatherOfBlocks, gogo, hake, hickuphh3, ignacio, joestakey, karanctf, kyteg, m_Rassska, pashov, rajatbeladiya, rbserver, robee, rokinot, samruna, sashik_eth, simon135, tofunmi
23.7515 USDC - $23.75
If a function modifier such as auth
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 6 instances of this issue:
83: function point(bytes32 param, address value) external auth { 95: function setLine( 96: bytes6 ilkId, 97: bytes6 baseId, 98: uint32 duration, 99: uint64 proportion, 100: uint64 initialOffer 101: ) external auth { 126: function setLimit( 127: bytes6 ilkId, 128: bytes6 baseId, 129: uint128 max, 130: ) external auth { 141: function setAnotherWitch(address value, bool isWitch) external auth { 150: function setIgnoredPair( 151: bytes6 ilkId, 152: bytes6 baseId, 153: bool ignore 154: ) external auth { 161: function setAuctioneerReward(uint128 auctioneerReward_) external auth {
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L83
One of most the expensive opcodes is SLOAD
. In order to save gas from checking the value of storage
-located variable they can be cached in memory
. That way the MLOAD
opcode is used each time we want to access the variable which is much cheaper than SLOAD
.
There are 11 instances of this issue:
180: DataTypes.Vault memory vault = cauldron.vaults(vaultId); 184: DataTypes.Series memory series = cauldron.series(vault.seriesId); 189: require(cauldron.level(vaultId) < 0, "Not undercollateralized"); 191: DataTypes.Balances memory balances = cauldron.balances(vaultId); 192: DataTypes.Debt memory debt = cauldron.debt(series.baseId, vault.ilkId); 304: cauldron.debtFromBase(auction_.seriesId, maxBaseIn) 309: baseIn = cauldron.debtToBase(auction_.seriesId, artIn); 542: DataTypes.Vault memory vault = cauldron.vaults(vaultId); 546: DataTypes.Series memory series = cauldron.series(vault.seriesId); 547: DataTypes.Balances memory balances = cauldron.balances(vaultId); 548: DataTypes.Debt memory debt = cauldron.debt( 549: series.baseId, 550: vault.ilkId 551: );
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L180
> 0
COSTS LESS GAS THAN != 0
WHEN USED ON A UINT
This change saves 3 gas per instance
There are 2 instances of this issue:
325: if (baseIn != 0) { 377: if (artIn != 0) {
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L325
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
There is 1 instances of this issue:
59: ICauldron public immutable cauldron;
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L59
There are 7 instances of this issue:
204: limits_.sum += auction_.ink; 259: limits[auction_.ilkId][auction_.baseId].sum -= auction_.ink; 430: limits_.sum -= auction_.ink; 443: auction_.ink -= inkOut.u128(); 444: auction_.art -= artIn.u128(); 450: limits_.sum -= inkOut.u128(); 598: liquidatorCut -= auctioneerCut;
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L204
"When 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."
https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html
Use a larger size then downcast where needed
There are 14 instances of this issue:
47: uint64 proportion, 48: uint64 initialOffer 63: uint128 public auctioneerReward = 0.01e18; 99: uint64 proportion, 100: uint64 initialOffer 129: uint128 max 161: function setAuctioneerReward(uint128 auctioneerReward_) external auth { 241: start: uint32(block.timestamp), 289: uint128 minInkOut, 290: uint128 maxBaseIn 303: uint128 artIn = uint128( 347: uint128 minInkOut, 348: uint128 maxArtIn, 582: elapsed = uint32(block.timestamp) - uint256(auction_.start);
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L47
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information. In order to get rid of the multiple custom errors, a single universal custom error Exception(uint c)
can be used. It would accept a single parameter that represents an error code used to make a reference to an off-chain dictionary of the errors' descriptions.
There are 17 instances of this issue:
84: require(param == "ladle", "Unrecognized"); 102: require(initialOffer <= 1e18, "InitialOffer above 100%"); 103: require(proportion <= 1e18, "Proportion above 100%"); 104: require( 105: initialOffer == 0 || initialOffer >= 0.01e18, 106: "InitialOffer below 1%" 107: ); 108: require(proportion >= 0.01e18, "Proportion below 1%"); 189: require(cauldron.level(vaultId) < 0, "Not undercollateralized"); 200: require(limits_.sum <= limits_.max, "Collateral limit reached"); 255: require(auction_.start > 0, "Vault not under auction"); 256: require(cauldron.level(vaultId) >= 0, "Undercollateralized"); 300: require(auction_.start > 0, "Vault not under auction"); 313: require(liquidatorCut >= minInkOut, "Not enough bought"); 328: require(baseJoin != IJoin(address(0)), "Join not found"); 358: require(auction_.start > 0, "Vault not under auction"); 365: require(liquidatorCut >= minInkOut, "Not enough bought"); 395: require(ilkJoin != IJoin(address(0)), "Join not found"); 416: require(auction_.start > 0, "Vault not under auction"); 437: require( 438: auction_.art - artIn >= debt.min * (10**debt.dec), 439: "Leaves dust" 440: );
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#84
x <= y
with x < y + 1
, and x >= y
with x + 1 > y
.In the EVM, there is no opcode for >=
or <=
. When using greater than or equal, two operations are performed: >
and =
. Using strict comparison operators hence saves gas
There are 9 instances of this issue:
102: require(initialOffer <= 1e18, "InitialOffer above 100%"); 103: require(proportion <= 1e18, "Proportion above 100%"); 105: initialOffer == 0 || initialOffer >= 0.01e18, 108: require(proportion >= 0.01e18, "Proportion below 1%"); 200: require(limits_.sum <= limits_.max, "Collateral limit reached"); 256: require(cauldron.level(vaultId) >= 0, "Undercollateralized"); 313: require(liquidatorCut >= minInkOut, "Not enough bought"); 365: require(liquidatorCut >= minInkOut, "Not enough bought"); 438: auction_.art - artIn >= debt.min * (10**debt.dec),
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#102