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: 75/133
Findings: 2
Award: $68.31
š 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.1336 USDC - $47.13
In solidity division can be zero; 9 / 10 = 0
. However, all values in (order.strike * fee) / 1000
where (order.strike * fee)
< 1000 will be unsettled for fee payment and would just transfer a zero value to the owner of the contract. (999 * 1) / 1000 = 0
where 1 is the fee of 0.01%.
Imagine that one day the ether has gone above $1,000,000 or the ERC20(order.baseAsset)
used has less decimals by nature with high value, the owner of the contract would not be able to get any profit which the fee calculation required to do.
In PuttyV2.sol#withdraw()#L499
the division happened and transfering the fee amount immediately:
src/PuttyV2.sol: 497 uint256 feeAmount = 0; 498 if (fee > 0) { 499: feeAmount = (order.strike * fee) / 1000; 500 ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); 501 }
Use FixedPoint Arithmetic library or scale up amount before division to a minimum upon divider value, and scale down after the division. At least, if this not in your concern; skip the transfer if feeAmount == 0
.
When using transfer with WETH we need to assert the call so we make sure the transfer process completed successfully.
src/PuttyV2.sol: 335 IWETH(weth).deposit{value: msg.value}(); 336: IWETH(weth).transfer(order.maker, msg.value);
Use assertion to revert on failure
assert(IWETH(weth).transfer(order.maker, msg.value));
#0 - HickupHH3
2022-07-15T14:54:15Z
[low-1] isn't upgraded because it lacks the key step of linking 0 fee to bricking withdrawals
š 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
21.1794 USDC - $21.18
Order
structure in function argument could be calldata
.> 0
is less efficient than != 0
for unsigned integers.uint256
variables to 0
is redundant.External
function consume less gas than public
.Order
structure in function argument could be calldata
From the Solidity docs: Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Reading directly from calldata
using calldataload
instead of going via memory saves the gas from the intermediate memory operations that carry the values.
Function affected:
PuttyV2.sol#fillOrder()
PuttyV2.sol#exercise()
PuttyV2.sol#withdraw()
PuttyV2.sol#cancel()
PuttyV2.sol#batchFillOrder()
used as array.PuttyV2.sol#acceptCounterOffer()
originalOrder
could be calldata too.PuttyV2.sol#hashOppositeOrder()
PuttyV2.sol#hashOrder()
The manipulation of the order structure happen only once when calculates PuttyV2.sol#hashOppositeOrder()
and this can be solved using this
keyword allows us to pass memory params to a function that accepts calldata params.
Gas diff table:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāā¬āāāāāāāāāā® ā src/PuttyV2.sol:PuttyV2 contract ā ā ā ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāŖāāāāāāāāāāāāāāāāāāŖāāāāāāāāāŖāāāāāāāāāŖāāāāāāāāāŖāāāāāāāāāā” ā Deployment Cost ā Deployment Size ā ā ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā 4774098 ā 25226 ā ā ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā Function Name ā min ā avg ā median ā max ā # calls ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā cancel ā 3075 ā 26509 ā 34321 ā 34321 ā 4 ā ā cancel ā 703 ā 26005 ā 34440 ā 34440 ā 4 ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā exercise ā 5759 ā 55920 ā 68002 ā 133534 ā 18 ā ā exercise ā 4731 ā 57637 ā 69551 ā 140619 ā 18 ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā fillOrder ā 10014 ā 106845 ā 115883 ā 203777 ā 51 ā ā fillOrder ā 9121 ā 112948 ā 123603 ā 212253 ā 51 ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā hashOrder ā 5206 ā 5484 ā 5206 ā 7782 ā 62 ā ā hashOrder ā 4178 ā 4592 ā 4178 ā 7011 ā 114 ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā¼āāāāāāāāā⤠ā withdraw ā 3075 ā 27840 ā 24545 ā 71168 ā 10 ā ā withdraw ā 697 ā 31196 ā 28491 ā 78172 ā 10 ā ā°āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāā“āāāāāāāāā“āāāāāāāāā“āāāāāāāāā“āāāāāāāāāāÆ
> 0
is less efficient than != 0
for unsigned integersIf you enable the optimizer at 10k AND you're in a require statement, this will save gas. I suggest changing > 0
with != 0
here:
src/PuttyV2.sol: 292 // check base asset exists 293: require(order.baseAsset.code.length > 0, "baseAsset is not contract"); 294 326 // handle the case where the user uses native ETH instead of WETH to pay the premium 327: if (weth == order.baseAsset && msg.value > 0) { 328 // check enough ETH was sent to cover the premium 350 // handle the case where the taker uses native ETH instead of WETH to deposit the strike 351: if (weth == order.baseAsset && msg.value > 0) { 352 // check enough ETH was sent to cover the strike 426 // handle the case where the taker uses native ETH instead of WETH to pay the strike 427: if (weth == order.baseAsset && msg.value > 0) { 428 // check enough ETH was sent to cover the strike 497 uint256 feeAmount = 0; 498: if (fee > 0) { 499 feeAmount = (order.strike * fee) / 1000; 597 598: require(token.code.length > 0, "ERC20: Token is not contract"); 599: require(tokenAmount > 0, "ERC20: Amount too small"); 600
uint256
variables to 0
is redundantDefault value is zero no need to assign 0
src/PuttyV2.sol: 496 // send the fee to the admin/DAO if fee is greater than 0% 497: uint256 feeAmount = 0; 498 if (fee > 0) {
++i
consumes 5 less gas than i++
Unchecked{ ++i; }
consumes 49 less gas each iterationi = 0
src/PuttyV2.sol: 555 556: for (uint256 i = 0; i < orders.length; i++) { 557 positionIds[i] = fillOrder(orders[i], signatures[i], floorAssetTokenIds[i]); 593 function _transferERC20sIn(ERC20Asset[] memory assets, address from) internal { 594: for (uint256 i = 0; i < assets.length; i++) { 595 address token = assets[i].token; 610 function _transferERC721sIn(ERC721Asset[] memory assets, address from) internal { 611: for (uint256 i = 0; i < assets.length; i++) { 612 ERC721(assets[i].token).safeTransferFrom(from, address(this), assets[i].tokenId); 626 ) internal { 627: for (uint256 i = 0; i < floorTokens.length; i++) { 628 ERC721(floorTokens[i]).safeTransferFrom(from, address(this), floorTokenIds[i]); 636 function _transferERC20sOut(ERC20Asset[] memory assets) internal { 637: for (uint256 i = 0; i < assets.length; i++) { 638 ERC20(assets[i].token).safeTransfer(msg.sender, assets[i].tokenAmount); 646 function _transferERC721sOut(ERC721Asset[] memory assets) internal { 647: for (uint256 i = 0; i < assets.length; i++) { 648 ERC721(assets[i].token).safeTransferFrom(address(this), msg.sender, assets[i].tokenId); 657 function _transferFloorsOut(address[] memory floorTokens, uint256[] memory floorTokenIds) internal { 658: for (uint256 i = 0; i < floorTokens.length; i++) { 659 ERC721(floorTokens[i]).safeTransferFrom(address(this), msg.sender, floorTokenIds[i]); 669 function isWhitelisted(address[] memory whitelist, address target) public pure returns (bool) { 670: for (uint256 i = 0; i < whitelist.length; i++) { 671 if (target == whitelist[i]) return true; 727 function encodeERC20Assets(ERC20Asset[] memory arr) public pure returns (bytes memory encoded) { 728: for (uint256 i = 0; i < arr.length; i++) { 729 encoded = abi.encodePacked( 741 function encodeERC721Assets(ERC721Asset[] memory arr) public pure returns (bytes memory encoded) { 742: for (uint256 i = 0; i < arr.length; i++) { 743 encoded = abi.encodePacked(
External
function consume less gas than public
As for best practices, you should use external if you expect that the function will only ever be called externally, and use public if you need to call the function internally. Functions not used internally: