Platform: Code4rena
Start Date: 29/06/2022
Pot Size: $50,000 USDC
Total HM: 20
Participants: 133
Period: 5 days
Judge: hickuphh3
Total Solo HM: 1
Id: 142
League: ETH
Rank: 56/133
Findings: 2
Award: $75.93
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xSolus, 0xf15ers, 0xsanson, AmitN, Bnke0x0, BowTiedWardens, Chom, David_, ElKu, Funen, GalloDaSballo, GimelSec, Hawkeye, IllIllI, JC, JohnSmith, Kaiziron, Kenshin, Lambda, Limbooo, MadWookie, Metatron, MiloTruck, Nethermind, Picodes, ReyAdmirado, Sneakyninja0129, StErMi, TomJ, Treasure-Seeker, TrungOre, Waze, Yiko, _Adam, __141345__, antonttc, async, aysha, catchup, cccz, cryptphi, csanuragjain, danb, datapunk, defsec, delfin454000, dirk_y, doddle0x, durianSausage, exd0tpy, fatherOfBlocks, gogo, hake, hansfriese, horsefacts, hubble, itsmeSTYJ, joestakey, oyc_109, pedroais, peritoflores, rajatbeladiya, reassor, robee, rokinot, samruna, saneryee, sashik_eth, shenwilly, shung, simon135, sseefried, unforgiven, zer0dot, zzzitron
47.1318 USDC - $47.13
emit
statement.There is a single instance of this in PuttyV2.sol
: function acceptCounterOffer
fee
.Conventionally Basis points
is used as a common unit of measure for interest rates and other percentages in finance, which uses 2 decimals of precision rather than 1 decimal as in the project. There seems to be no reason to use a non-conventional standard in the project.
The limit value of fee
can be then set to 300, instead of the current 30, in the setFee function.
The fee formula should be changed as well as shown below:
feeAmount = (order.strike * fee) / 10000;
fillOrder()
The error message in line 298 should be same as the one in line 406.
require(floorAssetTokenIds.length == 0, "Invalid floor tokens length");
should be
require(floorAssetTokenIds.length == 0, "Invalid floor tokenIds length");
return
statement in fillOrder
function.The multiple return positionId
statements in fillOrder
function is not needed. As positionId
is already mentioned as a returned variable in the function declaration part at line 272.
public payable returns (uint256 positionId)
🌟 Selected for report: GalloDaSballo
Also found by: 0v3rf10w, 0x1f8b, 0xA5DF, 0xDjango, 0xHarry, 0xKitsune, 0xNazgul, 0xNineDec, 0xc0ffEE, 0xf15ers, 0xkatana, 0xsanson, ACai, Aymen0909, Bnke0x0, BowTiedWardens, Chom, ElKu, Fitraldys, Funen, Haruxe, Hawkeye, IllIllI, JC, JohnSmith, Kaiziron, Kenshin, Lambda, Limbooo, MadWookie, Metatron, MiloTruck, Picodes, PwnedNoMore, Randyyy, RedOneN, ReyAdmirado, Ruhum, Sm4rty, StErMi, StyxRave, TerrierLover, TomJ, Tomio, UnusualTurtle, Waze, Yiko, _Adam, __141345__, ajtra, ak1, apostle0x01, asutorufos, c3phas, cRat1st0s, catchup, codetilda, cryptphi, datapunk, defsec, delfin454000, durianSausage, exd0tpy, fatherOfBlocks, gogo, grrwahrr, hake, hansfriese, horsefacts, ignacio, jayfromthe13th, joestakey, ladboy233, m_Rassska, mektigboy, minhquanym, mrpathfindr, natzuu, oyc_109, rajatbeladiya, reassor, rfa, robee, rokinot, sach1r0, saian, sashik_eth, simon135, slywaters, swit, z3s, zeesaw, zer0dot
28.8033 USDC - $28.80
This reduces both deployment and runtime gas usage. See this link for an example.
Also many of these errors are of the same type, which means we can use the same custom error for them, reducing the gas usage even further. The require statements which use the same error message are:
File : PuttyV2.sol:
File : PuttyV2Nft.sol:
There are 27 instances in PuttyV2.sol
and 6 instances in PuttyV2Nft.sol
, where we can use custom error report instead of require statements:
File : PuttyV2.sol:
File : PuttyV2Nft.sol:
This will save gas in case the function reverts after the emit. Instances where this could be done are:
emit FilledOrder(orderHash, floorAssetTokenIds, order); could be either moved to
if elseif
block. The return statement can be taken out of the if blocks and placed at the end of the function just after the emit statement.emit ExercisedOrder(orderHash, floorAssetTokenIds, order); can be placed at the end of the function(after line 519).
emit WithdrawOrder(orderHash, order); can be placed at the end of the function(after line 457).
For arithmetic operations which are unlikely to overflow or underflow, you could put them inside an unchecked block. This is usually the case for increment of loop indices in for
and while
loops.
For example:
for (uint256 i = 0; i < orders.length; i++)
could be rewritten as:
// you can remove initializing to i as well as its redundant. // the length of the array can be cached if the arrays are long, but if its just few elements then you will // end up using more gas than before. So I have left it to the sponsor’s judgement. for (uint256 i; i < orders.length; ) { //statements… unchecked { i++; } }
There are 10 instances of this in PuttyV2.sol
.
if
blocks to a single if else
block.In File PuttyV2.sol
, these two seperate if statements
could be combined into a single if else
block. And the return
statement could be placed outside the if else
block.
The modified code will look like this:
// we can see that the strike is transferred to owner only when the value of order.isCall and isExercised have different values. // and vice versa for transferring assets from putty to owner. // Both `if blocks` are mutually exclusive. bool flag = order.isCall == isExercised; // transfer strike to owner if put is expired or call is exercised if(flag) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { unchecked { // @audit feeAmount = (order.strike * fee) / 1000; } ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); } // transfer assets from putty to owner if put is exercised or call is expired else { _transferERC20sOut(order.erc20Assets); _transferERC721sOut(order.erc721Assets); // for call options the floor token ids are saved in the long position in fillOrder(), // and for put options the floor tokens ids are saved in the short position in exercise() uint256 floorPositionId = order.isCall ? longPositionId : uint256(orderHash); _transferFloorsOut(order.floorTokens, positionFloorAssetTokenIds[floorPositionId]); } return;
This saves deployment cost plus execution cost.
There is an instance of this in fillOrder
function:
order.isCall
, order.isLong
and order.maker
are accessed multiple times inside the fillOrder
function. They could be cached like this,
address maker = order.maker; bool isLong = order.isLong; bool isCall = order.isCall;
Just the above change caused the average gas consumption for fillOrder
function to be reduced by 40(over 51 calls).
Below is a comparison of gas costs of few functions in the original code and the modified code(after the above mentioned optimizations are applied). This is without changing the require statements into custom errors. If we add that too, the gas savings will be significant.
File : PuttyV2.sol
Function Name | Original Cost(A) | Optmized Code cost(B) | Gas Saved(A-B) | % Gas Saved (A-B)/A*100 |
---|---|---|---|---|
Deployment Cost | 4774098 | 4749863 | 24235 | 0.508% |
exercise | 55920 | 55471 | 449 | 0.803% |
fillOrder | 106845 | 106475 | 370 | 0.346% |
withdraw | 27840 | 27737 | 103 | 0.370% |
Original Gas Report Optimized Code’s gas report Optimized PuttyV2.sol code