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: 40/59
Findings: 1
Award: $65.35
🌟 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
In TimeswapV2PoolFactory.sol and TimeswapV2OptionFactory.sol, it is noted that their corresponding pair
and optionPair
have not been pushed into the array, getByIndex
in the function logic of create()
. This leads to futile numberOfPairs()
that is going to always return getByIndex.length == 0
.
Suggested fix:
It is recommended pushing these useful elements, pair
and optionPair
, into their respective getByIndex
when calling create()
.
Using return
to output function results is a confusing approach in the presence of named returns.
Here is a specific instance found.
function min(uint256 value1, uint256 value2) internal pure returns (uint256 result) { return value1 < value2 ? value1 : value2; }
Suggested fix:
return
function min(uint256 value1, uint256 value2) internal pure returns (uint256 result) { result = value1 < value2 ? value1 : value2; }
or
. Remove result
function min(uint256 value1, uint256 value2) internal pure returns (uint256) { return value1 < value2 ? value1 : value2; }
In TimeswapV2PoolFactory.sol, the second if check of create()
reverts via Error.zeroAddress()
if pair
is not a zero address. This is confusing the users because the error thrown applies more appropriately the opposite way if pair == address(0)
.
if (pair != address(0)) Error.zeroAddress();
Suggested fix:
Implement something meaningful, e.g. Error.addressNotZero
or Error.duplicatePair
and update it in Error.sol.
In Pool.sol, the condition in the if statement of updateDurationWeightBeforeMaturity()
, pool.lastTimestamp < blockTimestamp
will always be true, making the check unnecessary.
function updateDurationWeightBeforeMaturity(Pool storage pool, uint96 blockTimestamp) private { if (pool.lastTimestamp < blockTimestamp) (pool.lastTimestamp, pool.shortFeeGrowth) = updateDurationWeight( pool.liquidity, pool.sqrtInterestRate, pool.shortFeeGrowth, DurationCalculation.unsafeDurationFromLastTimestampToNow(pool.lastTimestamp, blockTimestamp), blockTimestamp );
Suggested fix:
Remove the if check and proceed to executing the associated block logic.
Comments in the function NatSpec should be as accurate as possible to avoid confusing the users.
For example, as denoted in the whitepaper:
"Let I be the marginal interest rate per second of the Short per total Long."
However, the second line of sqrtInterestRate()
NatSpec says that:
"... the square root of interest rate is z/(x+y) ..."
Suggested fix:
Update the function NatSpec to correctly portray its intended message.
In TimeswapV2OptionFactory.sol, create()
will readily revert if token0 > token1
, which is not very efficient in terms of gas efficiency and user friendliness.
TimeswapV2OptionFactory.sol#L46
OptionPairLibrary.checkCorrectFormat(token0, token1);
Suggested fix:
Replace the above code line above with a sort logic that is only 1 code line long:
function create(address _token0, address _token1) external override returns (address optionPair) { if (_token0 == address(0)) Error.zeroAddress(); if (_token1 == address(0)) Error.zeroAddress(); (token0, token1) = token0_ < token1_ ? (token0_, token1_) : (token1_, token0_); optionPair = optionPairs[token0][token1]; OptionPairLibrary.checkDoesNotExist(token0, token1, optionPair); optionPair = deploy(address(this), token0, token1); optionPairs[token0][token1] = optionPair; emit Create(msg.sender, token0, token1, optionPair); }
In TimeswapV2OptionFactory.sol, OptionPairLibrary.checkDoesNotExist()
in create()
is simply making sure optionPair
is a zero address. There isn't really a need to call the library for this simple check.
TimeswapV2OptionFactory.sol#L49
OptionPairLibrary.checkDoesNotExist(token0, token1, optionPair);
Suggested fix:
Replace the above code line with the following:
if (optionPair != address(0)) Error.alreadyExisted(); // @ audit: Update this in Error.sol
Along with the sort logic implemented, OptionPair.sol
could pretty much be done away.
delete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address or a boolean to false. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x.
The delete key better conveys the intention and is also more idiomatic.
Here are the 3 specific examples.
228: pool.long0ProtocolFees = 0; 236: pool.long1ProtocolFees = 0; 244: pool.shortProtocolFees = 0;
Suggested fix:
228: delete pool.long0ProtocolFees; 236: delete pool.long1ProtocolFees; 244: delete pool.shortProtocolFees;
A zero value check on strike
is missing when initializing the pool that could lead to malfunction when assessing the mapping, pools
.
Suggested fix:
Update the second if block to:
if (rate == 0 || strike == 0) Error.cannotBeZero();
In TimeswapV2Pool.sol, transferLiquidity(()
calls hasLiquidity()
to make sure the pool liquidity is not zero. However, this similar check is going to be performed when calling pools[strike][maturity].transferLiquidity()
.
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(); pools[strike][maturity].transferLiquidity(to, liquidityAmount, blockTimestamp(0)); emit TransferLiquidity(strike, maturity, msg.sender, to, liquidityAmount); }
Suggested fix:
Remove hasLiquidity(strike, maturity)
and do only 1 needed check on pool liquidity.
Lower versions like 0.8.8 are being used in the protocol contracts. For better security, it is best practice to use the latest Solidity version, 0.8.17.
Please visit the versions security fix list in the link below for detailed info:
https://github.com/ethereum/solidity/blob/develop/Changelog.md
hardhat.config.js: 29 module.exports = { 30: solidity: { 31: compilers: [ 32: { 33: version: "0.8.8", 34: settings: { 35: optimizer: { 36: enabled: true, 37: runs: 1000000 38 }
Description: Protocol has enabled optional compiler optimizations in Solidity. There have been several optimization bugs with security implications. Moreover, optimizations are actively being developed. Solidity compiler optimizations are disabled by default, and it is unclear how many contracts in the wild actually use them.
Therefore, it is unclear how well they are being tested and exercised. High-severity security issues due to optimization bugs have occurred in the past. A high-severity bug in the emscripten-generated solc-js compiler used by Truffle and Remix persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG.
Another high-severity optimization bug resulting in incorrect bit shift results was patched in Solidity 0.5.6. More recently, another bug due to the incorrect caching of keccak256 was reported. A compiler audit of Solidity from November 2018 concluded that the optional optimizations may not be safe. It is likely that there are latent bugs related to optimization and that new bugs will be introduced due to future optimizations.
Exploit Scenario A latent or future bug in Solidity compiler optimizations—or in the Emscripten transpilation to solc-js—causes a security vulnerability in the contracts.
Recommendation: Short term, measure the gas savings from optimizations and carefully weigh them against the possibility of an optimization-related bug. Long term, monitor the development and adoption of Solidity compiler optimizations to assess their maturity.
As denoted in Solidity's Style Guide:
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
In order to help readers identify which functions they can call, and find the constructor and fallback definitions more easily, functions should be grouped according to their visibility and ordered in the following manner:
constructor, receive function (if exists), fallback function (if exists), external, public, internal, private
And, within a grouping, place the view and pure functions last.
Additionally, inside each contract, library or interface, use the following order:
type declarations, state variables, events, modifiers, functions
Where possible, consider adhering to the above guidelines for all contract instances entailed.
#0 - c4-judge
2023-02-01T22:51:58Z
Picodes marked the issue as grade-b