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: 9/77
Findings: 3
Award: $1,942.85
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: xiaoming90
Also found by: GreyArt, berndartmueller, jonah1005, kenzo, minhquanym
1806.2399 USDC - $1,806.24
https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L40 https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L52
In case fCash has not matured yet, convertToShares()
may return incorrect value due to division round down 2 times. It may leads to the case that user need more amount of share than expected to withdraw assets.
In wfCashERC4626.convertToShares()
function, it calculated amout of share from assets
amount. And it used a formula which round down the division (1st one).
return (assets * totalSupply()) / totalAssets();
totalAsset()
function, it also rounded down 1 more time (2nd one) when fCash has not maturedint256 pvExternal = (pvInternal * precision) / Constants.INTERNAL_TOKEN_PRECISION;
Assume underlyingToken
is USDC
with decimals = 6
so precision = 1e6
. Wrapped fCash has totalSupply() = 2000000
, has not matured and pvInternal = 2111111
. We try to get amount of share with assets = 1e9
.
Use 2nd formula
pvExternal = (pvInternal * precision) / INTERNAL_TOKEN_PRECISION = (2111111 * 1e6) / 1e8 = 21111 => totalAssets() = 21111
(assets * totalSupply()) / totalAssets() = (1e9 * 2000000) / 21111 = 94737340722
(assets * totalSupply()) / totalAssets() = (assets * totalSupply()) / ((pvInternal * precision) / INTERNAL_TOKEN_PRECISION) = (assets * totalSupply() * INTERNAL_TOKEN_PRECISION) / (pvInternal * precision) = (1e9 * 2000000 * 1e8) / (2111111 * 1e6) = 94736847091
Manual Review
Merge 2 formula into 1 calculation to improve precision.
#0 - jeffywu
2022-06-15T12:26:39Z
Good find and agree with the mitigation.
#1 - jeffywu
2022-06-16T13:02:53Z
Duplicate of #155 (I think this is the better written description of all the duplicates).
I believe this should be Med Severity due to the size of the potential effect.
#2 - jeffywu
2022-06-28T12:34:57Z
My understanding of the ERC4626 specification (and this comes via Alberto who is one of the authors of the specification), is that convertToShares and convertToAssets are intended to be oracle values that do not take into account slippage for an actual exit (that is the job of previewMint/previewWithdraw, etc). Therefore, convertToShares and convertToAssets must be resistant to flash loan manipulation (which they are here) and usable as view methods for UIs and display purposes, but should not be used by smart contracts to determine actual trading amounts.
If that is the case then I would think that the severity would be reduced for this particular report. I think that rounding in the previewMint/etc cases could be left as High.
convertToShares The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. MUST NOT be inclusive of any fees that are charged against assets in the Vault. MUST NOT show any variations depending on the caller. MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
🌟 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
89.1872 USDC - $89.19
_safeNegInt88()
in wfCashERC4626
In wfCashERC4626
contract, function _safeNegInt88()
is declared but never use. And it’s a private function, so no one can call it outside the contract either.
Remove _safeNegInt88()
function.
totalSupply()
2 times to get already cached value.In wfCachERC4626.convertToShares()
, line 53 has already cached totalSupply but line 60 call totalSupply()
again instead of using supply
variable directly. It’s inconsistent when compare to convertToAssets()
function which use supply
variable.
Use supply
instead of totalSupply()
Function NotionalTradeModule._mintFCashPosition()
is used to mint fCash position, but the natspec docs said that this function is used to redeem fCash position
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
https://swcregistry.io/docs/SWC-103
In wrapped fcash, it uses ^0.8.0
and 0.8.11
inconsistently
Use fixed solidity version
🌟 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
47.4165 USDC - $47.42
DateTime.sol
In DateTime.getTradedMarket()
function, it used 7 if
to get result which mean it’s cost at most 7 check every time this function is called.
In DateTime.getMarketIndex()
function, it loop from 1 to maxMarketIndex
and call getTradedMarket()
each time. Instead if we stored result of getTradedMarket()
in a list like below we can save gas (at most 7*7 = 49 checks)
uint256[] list = [QUARTER, 2 * QUARTER, YEAR, 2 * YEAR, … ]
Precompute and store result of getTradedMarket()
in a list