Platform: Code4rena
Start Date: 20/01/2023
Pot Size: $90,500 USDC
Total HM: 10
Participants: 59
Period: 7 days
Judge: Picodes
Total Solo HM: 4
Id: 206
League: ETH
Rank: 28/59
Findings: 2
Award: $113.89
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: 0x1f8b, 0xAgro, 0xGusMcCrae, 0xSmartContract, Awesome, Breeje, DadeKuma, Diana, IllIllI, Josiah, Moksha, RaymondFam, Rolezn, SaeedAlipoor01988, Udsen, Viktor_Cortess, brgltd, btk, chaduke, cryptonue, ddimitrov22, delfin454000, descharre, fatherOfBlocks, georgits, hansfriese, lukris02, luxartvinsec, martin, matrix_0wl, mookimgo, oberon, popular00, shark, tnevler
65.3481 USDC - $65.35
packages\v2-token\src\TimeswapV2Token.sol
5: import "forge-std/console.sol"; 109: console.log("reaches right before mint in timeswapv2Tokne::mint"); 170: console.log()
Most of the functions in the project have 1-word names like get, create, check, etc. Without Natspec it makes understanding of the code harder.
For example:
\packages\v2-option\test\src\TimeswapV2OptionFactory.sol
33: function get(address token0, address token1) external view override returns (address optionPair) { 47: function create(address token0, address token1) external override returns (address optionPair) {
Avoid floating pragmas for non-library contracts. While floating pragmas make sense for libraries to allow them to be included with multiple different versions of applications, it may be a security risk for application implementations. A known vulnerable compiler version may accidentally be selected or security tools might fall back to an older compiler version ending up checking a different EVM compilation that is ultimately deployed on the blockchain. It is recommended to pin to a concrete compiler version.
packages\v2-token\src\interfaces\IERC1155Enumerable.sol => pragma solidity ^0.8.0; packages\v2-token\src\base\ERC1155Enumerable.sol => pragma solidity ^0.8.0; packages\v2-token\src\TimeswapV2LiquidityToken.sol => pragma solidity ^0.8.8; packages\v2-token\src\TimeswapV2Token.sol => pragma solidity ^0.8.8; packages\v2-token\src\structs\CallbackParam.sol => pragma solidity ^0.8.8; packages\v2-token\src\structs\FeesPosition.sol => pragma solidity ^0.8.8; packages\v2-token\src\structs\Param.sol => pragma solidity ^0.8.8; packages\v2-token\src\structs\Position.sol => pragma solidity ^0.8.8;
Parameters used in constructor are used to initialize the state variable, error in these can lead to the redeployment of the contract.
packages\v2-token\src\TimeswapV2LiquidityToken.sol
37: optionFactory = chosenOptionFactory; 38: poolFactory = chosenPoolFactory;
packages\v2-token\src\TimeswapV2Token.sol
42 : optionFactory = chosenOptionFactory;
\packages\v2-pool\src\base\OwnableTwoSteps.sol
18: constructor(address chosenOwner) { owner = chosenOwner; }
\packages\v2-token\src\TimeswapV2Token.sol
41: constructor(address chosenOptionFactory) ERC1155("Timeswap V2 address") {
\packages\v2-token\src\TimeswapV2LiquidityToken.sol
36: constructor(address chosenOptionFactory, address chosenPoolFactory) ERC1155("Timeswap V2 uint160 address") {
\packages\v2-pool\src\TimeswapV2Pool.sol
77: constructor() NoDelegateCall() { (poolFactory, optionPair, transactionFee, protocolFee) = ITimeswapV2PoolDeployer(msg.sender).parameter(); }
\packages\v2-pool\src\TimeswapV2Pool.sol
240: function **mint**(TimeswapV2PoolMintParam calldata param) external override returns (uint160 liquidityAmount, uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { 245: function mint( TimeswapV2PoolMintParam calldata param, uint96 durationForward ) external override returns (uint160 liquidityAmount, uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return mint(param, true, durationForward); } 252: function mint( TimeswapV2PoolMintParam calldata param, bool isQuote, uint96 durationForward ) 305: function burn(TimeswapV2PoolBurnParam calldata param) external override returns (uint160 liquidityAmount, uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return burn(param, false, 0); } 310: function burn( TimeswapV2PoolBurnParam calldata param, uint96 durationForward ) external override returns (uint160 liquidityAmount, uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return burn(param, true, durationForward); } 317: function burn( TimeswapV2PoolBurnParam calldata param, bool isQuote, uint96 durationForward ) 365: function deleverage(TimeswapV2PoolDeleverageParam calldata param) external override returns (uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return deleverage(param, false, 0); } 370: function deleverage( TimeswapV2PoolDeleverageParam calldata param, uint96 durationForward ) external override returns (uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return deleverage(param, true, durationForward); } 377: function deleverage( TimeswapV2PoolDeleverageParam calldata param, bool isQuote, uint96 durationForward ) 421: function leverage(TimeswapV2PoolLeverageParam calldata param) external override returns (uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return leverage(param, false, 0); } /// @inheritdoc ITimeswapV2Pool function leverage(TimeswapV2PoolLeverageParam calldata param, uint96 durationForward) external override returns (uint256 long0Amount, uint256 long1Amount, uint256 shortAmount, bytes memory data) { return leverage(param, true, durationForward); } function leverage( TimeswapV2PoolLeverageParam calldata param, bool isQuote, uint96 durationForward )
For better readability and analysis it is better to avoid nested if blocks. Here is an example:
\packages\v2-token\src\TimeswapV2LiquidityToken.sol
237: - if (from != to) { + require(from != to,””);
#0 - c4-judge
2023-02-01T23:47:35Z
Picodes marked the issue as grade-b
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xackermann, Aymen0909, Beepidibop, IllIllI, Iurii3, Rageur, RaymondFam, ReyAdmirado, Rolezn, SaeedAlipoor01988, Udsen, Viktor_Cortess, W0RR1O, W_Max, atharvasama, c3phas, chaduke, descharre, fatherOfBlocks, kaden, matrix_0wl, shark
48.5424 USDC - $48.54
1) packages\v2-option\typechain\test\src\structs\Param.sol
103 : function check(TimeswapV2OptionMintParam memory param, uint96 blockTimestamp) internal pure { if (param.strike == 0) Error.zeroInput(); if (param.maturity > type(uint96).max) Error.incorrectMaturity(param.maturity); if (param.maturity <= blockTimestamp) Error.alreadyMatured(param.maturity, blockTimestamp); if (param.shortTo == address(0)) Error.zeroAddress(); if (param.long0To == address(0)) Error.zeroAddress(); if (param.long1To == address(0)) Error.zeroAddress(); - TransactionLibrary.check(param.transaction); + if (uint256(param.transaction) >= 2) revert InvalidTransaction(); if (param.amount0 == 0 && param.amount1 == 0) Error.zeroInput(); }
We can replace this line: 110: TransactionLibrary.check(param.transaction);
With a simple if statement:
if (uint256(param.transaction) >= 2) revert InvalidTransaction();
Results of the test: Before: (gas: 816) After: (gas: 787)
Similar situation to the other 3 check()
functions in ParamLibrary.
2) packages\v2-option\typechain\test\src\TimeswapV2OptionFactory.sol
46: function create(address token0, address token1) external override returns (address optionPair) { if (token0 == address(0)) Error.zeroAddress(); if (token1 == address(0)) Error.zeroAddress(); - OptionPairLibrary.checkCorrectFormat(token0, token1); + if (token0 >= token1) revert Error.InvalidOptionPair(token0, token1); optionPair = optionPairs[token0][token1]; - OptionPairLibrary.checkDoesNotExist(token0, token1, optionPair); + if (optionPair != address(0)) revert Error.OptionPairAlreadyExisted(token0, token1, optionPair); optionPair = deploy(address(this), token0, token1); optionPairs[token0][token1] = optionPair; emit Create(msg.sender, token0, token1, optionPair); } 33: function get(address token0, address token1) external view override returns (address optionPair) { - OptionPairLibrary.checkCorrectFormat(token0, token1); + if (token0 >= token1) revert Error.InvalidOptionPair(token0, token1); optionPair = optionPairs[token0][token1]; }
Gas before: AVG 1553 Gas after: AVG 1519
packages\v2-library\src\BytesLib.sol
12: function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory)
When you use the "delete" keyword, the EVM will remove the storage slot associated with the variable, and shift all of the storage slots that come after it. This process consumes more gas than a simple assignment operation such as "variableName = 0", which only requires updating the value of the variable to zero.
packages\v2-pool\src\base\OwnableTwoSteps.sol
39: delete pendingOwner;
packages\v2-option\src\TimeswapV2OptionDeployer.sol
38: delete parameter;
\packages\v2-pool\src\TimeswapV2PoolDeployer.soll
30: delete parameter;
packages\v2-pool\src\libraries\ConstantProduct.sol
149: if (isAdd) longAmount -= fees;
150: else shortAmount -= fees;
180: shortAmount -= fees;
212: longAmount -= fees;
244: amount -= fees;
295: denominator2 += longAmount;
In the original code, the contract checks the value of both token0 and token1 separately, executing the Error.zeroAddress() function twice if either of them is equal to address(0). This means that the contract will consume more gas to execute the comparison and the function call twice.
packages\v2-option\src\TimeswapV2OptionFactory.sol
44: if (token0 == address(0)) Error.zeroAddress();
45: if (token1 == address(0)) Error.zeroAddress();
In we unify several if statements, the contract checks the value of both token0 and token1 together using the OR logical operator. If either of them is equal to address(0), the Error.zeroAddress() function is called once. This means that the contract will consume less gas to execute the comparison and the function call once.
if (token0 == address(0) || token1 == address(0) ) Error.zeroAddress();
packages\v2-option\src\structs\Param.sol
107: if (param.shortTo == address(0)) Error.zeroAddress(); if (param.long0To == address(0)) Error.zeroAddress(); if (param.long1To == address(0)) Error.zeroAddress(); 134: if (param.tokenTo == address(0)) Error.zeroAddress(); if (param.longTo == address(0)) Error.zeroAddress(); 131: if (param.strike == 0) Error.zeroInput(); 137: if (param.amount == 0) Error.zeroInput(); 147: if (param.token0To == address(0)) Error.zeroAddress(); 148: if (param.token1To == address(0)) Error.zeroAddress(); 144: if (param.strike == 0) Error.zeroInput(); 150: if (param.amount == 0) Error.zeroInput();
\packages\v2-token\src\TimeswapV2LiquidityToken.sol
76: if (from == address(0)) Error.zeroAddress(); if (to == address(0)) Error.zeroAddress();
In general, it is cheaper to read a uint8 variable than a uint96 variable because uint8 requires less storage than uint96
\packages\v2-pool\src\libraries\ReentrancyGuard.sol
12: uint96 internal constant NOT_INTERACTED = 0; //@GAS UINT96 - 32 from TimeswapV2LiquidityToken /// @dev The initial and ending state of balanceTarget in the Option struct. uint96 internal constant NOT_ENTERED = 1; /// @dev The state where the contract is currently being interacted with. uint96 internal constant ENTERED = 2; function check(uint96 reentrancyGuard) internal pure { if (reentrancyGuard == NOT_INTERACTED) revert NotInteracted(); if (reentrancyGuard == ENTERED) revert NoReentrantCall(); }
DEPLOYMENT COST BEFORE: 49099, AFTER: 45499 CHECK FUNCTION. BEFORE: 326, AFTER: 290
Error.sol file consists of errors and functions with the same names and these functions inside. For example:
error ZeroInput(); function zeroInput() internal pure { revert ZeroInput(); }
Direct use of errors without functions saves some gas. For example:
\packages\v2-pool\src\TimeswapV2Pool.sol
152: function transferLiquidity(uint256 strike, uint256 maturity, address to, uint160 liquidityAmount) external override { hasLiquidity(strike, maturity); if (blockTimestamp(0) > maturity) Error.alreadyMatured(maturity, blockTimestamp(0)); if (to == address(0)) Error.zeroAddress(); - if (liquidityAmount == 0) Error.zeroInput(); //Calling a function with error inside + if (liquidityAmount == 0) revert Error.ZeroInput(); // Direct call to the same error pools[strike][maturity].transferLiquidity(to, liquidityAmount, blockTimestamp(0)); emit TransferLiquidity(strike, maturity, msg.sender, to, liquidityAmount); }
Similar situation with libraries that hold only functions with one if statement and an error inside:
\packages\v2-pool\src\libraries\PoolPair.sol
15: function checkNotZeroAddress(address poolPair) internal pure { if (poolPair == address(0)) revert ZeroSwapAddress(); }
#0 - c4-judge
2023-02-02T12:29:46Z
Picodes marked the issue as grade-b