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: 15/63
Findings: 2
Award: $65.04
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: hickuphh3
Also found by: 0x29A, 0x52, 0xNazgul, Chom, Deivitto, ElKu, Funen, IllIllI, Meera, ReyAdmirado, SooYa, TomJ, Trumpero, Waze, __141345__, ak1, asutorufos, c3phas, cRat1st0s, csanuragjain, delfin454000, exd0tpy, fatherOfBlocks, hake, hansfriese, horsefacts, hyh, karanctf, kenzo, kyteg, ladboy233, pashov, peritoflores, rajatbeladiya, rbserver, reassor, rokinot, simon135, wastewa
39.1057 USDC - $39.11
 
I recommend adding check of 0-address for immutable addresses. Not doing so might lead to non-functional contract when it is updated to 0-address accidentally.
Add 0-address check for above immutable address.
 
It is best practice to define magic numbers to constant rather than just using as a magic number. This improves code readability and maintainability.
./Witch.sol:102: require(initialOffer <= 1e18, "InitialOffer above 100%"); ./Witch.sol:103: require(proportion <= 1e18, "Proportion above 100%"); ./Witch.sol:162: if (auctioneerReward_ > 1e18) { ./Witch.sol:163: revert AuctioneerRewardTooHigh(1e18, auctioneerReward_); ./Witch.sol:587: proportionNow = 1e18; ./Witch.sol:591: uint256(1e18 - initialProportion).wmul(elapsed.wdiv(duration));
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L102-L103 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L162-L163 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L587 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L591
./Witch.sol:105: initialOffer == 0 || initialOffer >= 0.01e18, ./Witch.sol:108: require(proportion >= 0.01e18, "Proportion below 1%");
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L105 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L108
./Witch.sol:233: if (art < debt.min * (10**debt.dec)) art = balances.art; ./Witch.sol:438: auction_.art - artIn >= debt.min * (10**debt.dec),
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L233 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L438
Define magic numbers to constant.
 
Each event should have 3 indexed fields if there are 3 or more fields.
./Witch.sol:34-38: event Bought(bytes12 indexed vaultId, address indexed buyer, uint256 ink, uint256 art); ./Witch.sol:43-49: event LineSet(bytes6 indexed ilkId, bytes6 indexed baseId, uint32 duration, uint64 proportion, uint64 initialOffer); ./Witch.sol:50: event LimitSet(bytes6 indexed ilkId, bytes6 indexed baseId, uint128 max); ./Witch.sol:51: event AnotherWitchSet(address indexed value, bool isWitch); ./Witch.sol:52-56: event IgnoredPairSet(bytes6 indexed ilkId, bytes6 indexed baseId, bool ignore); ./Witch.sol:57: event AuctioneerRewardSet(uint128 auctioneerReward);
Use all 3 index fields when possible.
 
Some typo was found in the following.
213: /// Useful as a method so it can be overriden by specialised witches that may need to do extra accounting or notify 3rd parties 267: /// Useful as a method so it can be overriden by specialised witches that may need to do extra accounting or notify 3rd parties 462: /// Useful as a method so it can be overriden by specialised witches that may need to do extra accounting or notify 3rd parties
385: /// @dev transfers funds from the ilkJoin to the liquidator (and potentially the auctioneer if they're differente people)
520: /// @dev quoutes hoy much ink a liquidator is expected to get if it repays an `artIn` amount
Change to correct spelling as mentioned in above PoC
 
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces. However there were places where this NatSpec was incomplete.
no @param for vaultId https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L212-L214
no @param for vault, series, to, balances, debt and no @return https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L220-L229
no @param for vaultId and owner https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L266-L268
no @param for auction_, to, liquidatorCut, auctioneerCut https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L385-L390
no @param for vaultId, auction_, inkOut, artIn https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L407-L413
no @param for vaultId, buyer, ink, art https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L461-L467
no @ param for auction_, to, artIn and @return for liquidatorCut, auctioneerCut https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L561-L566
Complete the missing NatSpec as shown in above PoC
 
#0 - alcueca
2022-07-22T14:18:28Z
Indexed fields and typo suggestions welcome, but just read the README next time.
🌟 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
25.9264 USDC - $25.93
 
SLOADs cost 100 gas where MLOADs/MSTOREs cost only 3 gas. Whenever function reads storage value more than once, it should be cached to save gas.
Cache variable cauldron of auction() in Witch.sol 5 SLOADs to 1 SLOAD, 1 MSTORE and 5 MLOAD https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L180 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L184 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L189 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L191-L192
Cache variable cauldron of payBase() in Witch.sol 2 SLOADs to 1 SLOAD, 1 MSTORE and 2 MLOAD https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L304 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L309
Cache variable cauldron of calcPayout() in Witch.sol 4 SLOADs to 1 SLOAD, 1 MSTORE and 4 MLOAD https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L542 https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L546-L548
When certain state variable is read more than once, cache it to local variable to save gas.
 
Certain variables is defined even though they are used only once. Remove these unnecessary variables to save gas. For cases where it will reduce the readability, one can use comments to help describe what the code is doing.
Originally: 594: uint256 inkAtEnd = uint256(artIn).wdiv(auction_.art).wmul(auction_.ink); 595: liquidatorCut = inkAtEnd.wmul(proportionNow); Change to: 595: liquidatorCut = uint256(artIn).wdiv(auction_.art).wmul(auction_.ink).wmul(proportionNow);
https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L594-L595
Don't define variable that is used only once. Details are listed on above PoC.
 
Since below require checks are used more than once, I recommend making these to a modifier or a function.
./Witch.sol:255: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:300: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:358: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:416: require(auction_.start > 0, "Vault not under auction");
./Witch.sol:313: require(liquidatorCut >= minInkOut, "Not enough bought"); ./Witch.sol:366: require(liquidatorCut >= minInkOut, "Not enough bought");
I recommend making duplicate require statement into modifier or a function.
 
Certain function is defined even though it is called only once. Inline it instead to where it is called to avoid usage of extra gas.
I recommend to not define above functions and instead inline it to place it is called.
 
It is cheaper gas to use calldata than memory if the function parameter is read only. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. More details on following link. link: https://docs.soliditylang.org/en/v0.8.15/types.html#data-location
./Witch.sol:224: DataTypes.Vault memory vault, ./Witch.sol:225: DataTypes.Series memory series, ./Witch.sol:227: DataTypes.Balances memory balances, ./Witch.sol:228: DataTypes.Debt memory debt ./Witch.sol:387: DataTypes.Auction memory auction_, ./Witch.sol:411: DataTypes.Auction memory auction_, ./Witch.sol:563: DataTypes.Auction memory auction_,
Change memory to calldata
 
Since EVM operates on 32 bytes at a time, it acctually cost more gas to use elements smaller than 32 bytes. Reference: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html
./Witch.sol:28: error AuctioneerRewardTooHigh(uint128 max, uint128 actual); ./Witch.sol:46: uint32 duration, ./Witch.sol:47: uint64 proportion, ./Witch.sol:48: uint64 initialOffer ./Witch.sol:50: event LimitSet(bytes6 indexed ilkId, bytes6 indexed baseId, uint128 max); ./Witch.sol:57: event AuctioneerRewardSet(uint128 auctioneerReward); ./Witch.sol:63: uint128 public auctioneerReward = 0.01e18; ./Witch.sol:98: uint32 duration, ./Witch.sol:99: uint64 proportion, ./Witch.sol:100: uint64 initialOffer ./Witch.sol:129: uint128 max ./Witch.sol:161: function setAuctioneerReward(uint128 auctioneerReward_) external auth { ./Witch.sol:232: uint128 art = uint256(balances.art).wmul(line.proportion).u128(); ./Witch.sol:234: uint128 ink = (art == balances.art) ./Witch.sol:289: uint128 minInkOut, ./Witch.sol:290: uint128 maxBaseIn ./Witch.sol:303: uint128 artIn = uint128( ./Witch.sol:347: uint128 minInkOut, ./Witch.sol:348: uint128 maxArtIn
I suggest using uint256 instead of anything smaller.
 
!= 0 costs less gas when optimizer is enabled and is used for unsigned integers in require statement.
./Witch.sol:255: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:300: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:358: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:416: require(auction_.start > 0, "Vault not under auction");
I suggest changing > 0 to != 0
 
Custom errors from Solidity 0.8.4 are cheaper than revert strings. Details are explained here: https://blog.soliditylang.org/2021/04/21/custom-errors/
./Witch.sol:84: require(param == "ladle", "Unrecognized"); ./Witch.sol:102: require(initialOffer <= 1e18, "InitialOffer above 100%"); ./Witch.sol:103: require(proportion <= 1e18, "Proportion above 100%"); ./Witch.sol:104-107: require(initialOffer == 0 || initialOffer >= 0.01e18, "InitialOffer below 1%"); ./Witch.sol:108: require(proportion >= 0.01e18, "Proportion below 1%"); ./Witch.sol:189: require(cauldron.level(vaultId) < 0, "Not undercollateralized"); ./Witch.sol:200: require(limits_.sum <= limits_.max, "Collateral limit reached"); ./Witch.sol:255: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:256: require(cauldron.level(vaultId) >= 0, "Undercollateralized"); ./Witch.sol:300: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:313: require(liquidatorCut >= minInkOut, "Not enough bought"); ./Witch.sol:328: require(baseJoin != IJoin(address(0)), "Join not found"); ./Witch.sol:358: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:365: require(liquidatorCut >= minInkOut, "Not enough bought"); ./Witch.sol:395: require(ilkJoin != IJoin(address(0)), "Join not found"); ./Witch.sol:416: require(auction_.start > 0, "Vault not under auction"); ./Witch.sol:437-440: require(auction_.art - artIn >= debt.min * (10**debt.dec), "Leaves dust");
I suggest implementing custom errors to save gas.