Platform: Code4rena
Start Date: 07/06/2022
Pot Size: $75,000 USDC
Total HM: 11
Participants: 77
Period: 7 days
Judge: gzeon
Total Solo HM: 7
Id: 124
League: ETH
Rank: 16/77
Findings: 2
Award: $248.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: berndartmueller
Also found by: 0x1f8b, 0x29A, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xkatana, 0xmint, Bronicle, Chom, Cityscape, Deivitto, Funen, GimelSec, GreyArt, IllIllI, JC, Lambda, Meera, Nethermind, Picodes, PierrickGT, Ruhum, Sm4rty, Tadashi, TerrierLover, TomJ, Trumpero, Waze, _Adam, antonttc, ayeslick, c3phas, catchup, cccz, cloudjunky, cryptphi, csanuragjain, delfin454000, dipp, ellahi, fatherOfBlocks, hake, hansfriese, hyh, joestakey, jonah1005, kenzo, minhquanym, oyc_109, sach1r0, saian, simon135, slywaters, sorrynotsorry, sseefried, unforgiven, xiaoming90, z3s, zzzitron
136.9496 USDC - $136.95
The main concerns are with the use of deprecated functions and incorrect versions of contracts.
Some of the function comments are missing function parameters or returns
Non-Critical
Instances include:
moduleIssueHook(ISetToken _setToken)
moduleRedeemHook(ISetToken _setToken)
componentIssueHook(ISetToken _setToken,uint256 _setTokenAmount,IERC20 _component,bool _isEquity)
componentRedeemHook(ISetToken _setToken,uint256 _setTokenAmount,IERC20 _component,bool _isEquity)
_deployWrappedfCash(uint16 _currencyId, uint40 _maturity)
_getWrappedfCash(uint16 _currencyId, uint40 _maturity)
redeemMaturedPositions(ISetToken _setToken)
_mintFCashPosition(ISetToken _setToken,IWrappedfCashComplete _fCashPosition,IERC20 _sendToken,uint256 _fCashAmount,uint256 _maxSendAmount)
_redeemFCashPosition(ISetToken _setToken,IWrappedfCashComplete _fCashPosition,IERC20 _receiveToken,uint256 _fCashAmount,uint256 _minReceiveAmount)
approve (ISetToken _setToken,IWrappedfCashComplete _fCashPosition,IERC20 _sendToken,uint256 _fCashAmount,uint256 _maxAssetAmount)
_mint(ISetToken _setToken,IWrappedfCashComplete _fCashPosition,uint256 _maxAssetAmount,uint256 _fCashAmount,bool _fromUnderlying)
_redeem(ISetToken _setToken,IWrappedfCashComplete _fCashPosition,uint256 _fCashAmount,bool _toUnderlying)
_isUnderlying(IWrappedfCashComplete _fCashPosition,IERC20 _paymentToken)
_getUnderlyingAndAssetTokens(IWrappedfCashComplete _fCashPosition)
_getFCashPositions(ISetToken _setToken)
_isWrappedFCash(address _fCashPosition)
_updateSetTokenPositions(ISetToken setToken,address sendToken,uint256 preTradeSendTokenBalance,address receiveToken,uint256 preTradeReceiveTokenBalance)
initialize(uint16 currencyId, uint40 maturity)
onERC1155Received(address _operator,address _from,uint256 _id,uint256 _value,bytes calldata _data)
redeem(uint256 amount, RedeemOpts memory opts)
redeemToAsset(uint256 amount,address receiver,uint32 maxImpliedRate)
redeemToUnderlying(uint256 amount,address receiver,uint32 maxImpliedRate)
_burn(address from,uint256 amount,bytes memory userData,bytes memory operatorData)
_withdrawCashToAccount(uint16 currencyId,address receiver,uint88 assetInternalCashClaim,bool toUnderlying)
_sellfCash(address receiver,uint256 fCashToSell,bool toUnderlying,uint32 maxImpliedRate)
Manual Analysis
Add a comment for these parameters
There are portions of commented code in some files.
Non-Critical
Instances include:
moduleIssueHook(ISetToken _setToken, uint256 /* _setTokenAmount */)
moduleRedeemHook(ISetToken _setToken, uint256 /* _setTokenAmount */)
Manual Analysis
Remove commented code
Events should use indexed fields
Non-Critical
Instances include:
event WrapperDeployed(uint16 currencyId, uint40 maturity, address wrapper)
Manual Analysis
Add indexed fields to this event.
Setters should emit an event so that Dapps can detect important changes to storage
Non-Critical
Instances include:
Manual Analysis
Emit an event in all setters.
Some functions are missing comments.
Non-Critical
Instances include:
function setRedeemToUnderlying
function _getMaturedValue()
function _getPresentValue()
function _redeemInternal()
function _safeNegInt88()
function _mintInternal()
function _sendTokensToReceiver()
function _safeUint88()
function _getByteCode()
function deployWrapper()
function computeAddress()
Manual Analysis
Add comments to these functions
contracts within the scope should be compiled using the same compiler version.
Non-Critical
wfCashBase.sol
, WrappedfCashFactory.sol
and wfCashLogic.sol
have the compiler version set to 0.8.11
, while NotionalTradeModule
has 0.6.10
Manual Analysis
Use the same compiler version throughout the contracts
Public functions that are not called by the contract should be declared external.
Non-Critical
Instances include:
redeemMaturedPositions(ISetToken _setToken)
getfCashId()
hasMatured()
getDecodedID()
getToken(bool useUnderlying)
Manual Analysis
Make these functions external
contracts should be compiled using a fixed compiler version. Locking the pragma helps ensure that contracts do not accidentally get deployed using a different compiler version with which they have been tested the most
Non-Critical
Instances include:
Manual Analysis
Used a fixed compiler version
When there are mappings that use the same key value, having separate fields is error prone, for instance in case of deletion or with future new fields.
Non-Critical
Instances include:
mapping(ISetToken => bool) public redeemToUnderlying;
mapping(ISetToken => bool) public allowedSetTokens;
Manual Analysis
Group the related data in a struct and use one mapping:
struct SetToken { bool redeemToUnderlying; bool allowedSetTokens; }
And it would be used as a state variable:
mapping(address => SetToken) public setTokens;
Some require statements are missing descriptive error strings, which makes it harder to debugg when the function reverts
Non-Critical
Instances include:
require(pvExternal >= 0)
require(int256(type(int88).min) <= y)
require(ac.hasDebt == 0x00 &&assets.length == 1 &&EncodeDecode.encodeERC1155Id(assets[0].currencyId,assets[0].maturity,assets[0].assetType) == fCashID)
require(x <= uint256(type(uint88).max))
Manual Analysis
Add descriptive strings to require statements
uint
is an alias for uint256
.
It is better to use uint256: it brings readability and consistency in the code, and it future proofs it in case of any changes to the alias of uint
Non-Critical
Instances include:
uint positionsLength = positions.length
uint positionsLength = positions.length
uint numFCashPositions
uint j
Manual Analysis
replace uint
with
uint256
When a division is computed, it must be ensured that the denominator is non-zero to prevent failure of the function call.
Low
Instances include:
uint256 assetInternalCashClaim = (uint256(cashBalance) * amount) / initialTotalSupply
Manual Analysis
Before doing these computations, add a non-zero check to the variables aforementioned.
The initialize function in wfCashBase.sol
does not have any access control and is front-runnable. If it is not executed in the same transaction as the constructor, a malicious user can front-run the initialize()
call, forcing the contract to be redeployed
Low
Instances include:
function initialize(uint16 currencyId, uint40 maturity) external override initializer
Manual Analysis
in WrappedfCashFactory.sol
, ensure contract.initialize()
is called in deployWrapper()
constructors should check the address written in an immutable address variable is not the zero address
Low
Instances include:
wrappedfCashFactory = _wrappedfCashFactory
weth = _weth
NotionalV2 = _notional
WETH = _weth
Manual Analysis
Add a zero address check for these function arguments.
_mintInternal()
should check that receiver
is not the zero address.
Low
Instances include:
Manual Analysis
Add a zero address check
This function is deprecated.
Low
Instances include:
assetToken.safeApprove(address(NotionalV2), type(uint256).max)
underlyingToken.safeApprove(address(NotionalV2), type(uint256).max)
Manual Analysis
safeIncreaseAllowance()
and safeDecreaseAllowance()
should be used instead.
Upgradeable contracts should have this variable to allow for new storage variables in future versions, see here.
Low
Instances include:
contract wfCashBase is ERC777Upgradeable
Manual Analysis
add __gap[50]
Given that wfCashLogic.sol
is deployed as a proxy contract, the Upgradeable variant of OpenZeppelin Contracts should be used. Otherwise, the constructor functions of wfCashLogic.sol
parent contracts - here ReentrancyGuard
- won't work for clone instances.
Low
Instances include:
contract wfCashLogic is ReentrancyGuard
Manual Analysis
use the openzeppelin-upgradeable
version of ReentrancyGuard
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xSolus, 0xf15ers, 0xkatana, 0xmint, 8olidity, Chom, Cityscape, DavidGialdi, Deivitto, ElKu, Fitraldys, Funen, GreyArt, Lambda, Meera, Picodes, PierrickGT, Sm4rty, Tadashi, TerrierLover, TomJ, Tomio, UnusualTurtle, Waze, _Adam, antonttc, asutorufos, berndartmueller, c3phas, catchup, csanuragjain, delfin454000, djxploit, ellahi, fatherOfBlocks, hake, hansfriese, hyh, joestakey, kaden, minhquanym, oyc_109, rfa, sach1r0, saian, samruna, simon135, slywaters, ynnad
112.0303 USDC - $112.03
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
Instances include:
scope: _getMaturedValue(
NotionalV2
is read twicescope: _mintInternal(
WETH
is read twicescope: onERC1155Received(
NotionalV2
is read three timesscope: _burn(
NotionalV2
is read twicescope: _mintInternal(
WETH
is read twiceManual Analysis
cache these storage variables in memory
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
Instances include:
scope: redeem()
scope: _burn()
bytes memory userData
bytes memory operatorData
Manual Analysis
Replace memory
with calldata
In the EVM, there is no opcode for >=
or <=
.
When using greater than or equal, two operations are performed: >
and =
.
Using strict comparison operators hence saves gas
Instances include:
receivedAmount >= _minReceiveAmount
sentAmount <= _maxSendAmount
getMaturity() <= block.timestamp
pvExternal >= 0
int256(type(int88).min) <= y
x <= uint256(type(uint88).max)
Manual Analysis
Replace <=
with <
, and >=
with >
. Do not forget to increment/decrement the compared variable
example:
-receivedAmount >= _minReceiveAmount; +receivedAmount > _minReceiveAmount - 1;
However, if 1
is negligible compared to the value of the variable, we can omit the increment.
-getMaturity() <= block.timestamp +getMaturity() < block.timestamp
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters.
Instances include:
wrappedfCashFactory = _wrappedfCashFactory; weth = _weth
Manual Analysis
Hardcode storage variables with their value instead of writing it during contract deployment with constructor parameters.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here
Custom errors are defined using the error statement
Instances include:
require(_setToken.isComponent(address(_sendToken)), "Send token must be an index component")
require(_setToken.isComponent(address(wrappedfCash)), "FCash to redeem must be an index component")
require(allowedSetTokens[_setToken], "Not allowed SetToken")
require(_setToken.isInitializedModule(getAndValidateAdapter(DEFAULT_ISSUANCE_MODULE_NAME))
require(_setToken.isInitializedModule(address(_debtIssuanceModule))
require(controller.isSet(address(_setToken)) || allowedSetTokens[_setToken]
require(wrappedfCashAddress.isContract()
require(sentAmount <= _maxSendAmount
require(receivedAmount >= _minReceiveAmount
require(_paymentToken == assetToken
require(cashGroup.maxMarketIndex > 0
require(DateTime.isValidMaturity(cashGroup.maxMarketIndex, maturity, block.timestamp)
require(underlyingExternal > 0
require(pvExternal >= 0)
require(int256(type(int88).min) <= y)
require(!hasMatured()
require(msg.sender == address(NotionalV2) && _id == fCashID &&int256(_value) > 0,
require(ac.hasDebt == 0x00 &&assets.length == 1 &&EncodeDecode.encodeERC1155Id(assets[0].currencyId,assets[0].maturity,assets[0].assetType) == fCashID)
require(0 < cashBalance,
require(x <= uint256(type(uint88).max))
Manual Analysis
Replace require and revert statements with custom errors.
For instance, in wfCashERC4626.sol
:
Replace
require(pvExternal >= 0)
with
if (pvExternal >= 0) { revert NegativePV(pvExternal); }
and define the custom error in the contract
error NegativePV(int256 pvExternal);
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Instances include:
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint32 minImpliedRate = 0
uint256 i = 0
uint256 i = 0
Manual Analysis
Remove explicit initialization for default values.
Prefix increments are cheaper than postfix increments.
Instances include:
i++
i++
i++
i++
numFCashPositions++
i++
j++
Manual Analysis
change variable++
to ++variable
.
Require statements including conditions with the &&
operator can be broken down in multiple require statements to save gas.
Instances include:
require(msg.sender == address(NotionalV2) && _id == fCashID &&int256(_value) > 0,
require(ac.hasDebt == 0x00 &&assets.length == 1 &&EncodeDecode.encodeERC1155Id(assets[0].currencyId,assets[0].maturity,assets[0].assetType) == fCashID)
Manual Analysis
Break down the require statements that contain &&
into multiple require statements. Further savings can be achieved by replacing require statements altogether with custom errors
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked
block will save gas. This is particularly true in for loops, as it saves some gas at each iteration.
Instances include:
i++
i++
i++
i++
numFCashPositions++
i++
j++
Manual Analysis
Place the arithmetic operations in an unchecked
block