Platform: Code4rena
Start Date: 05/08/2021
Pot Size: $50,000 USDC
Total HM: 9
Participants: 16
Period: 7 days
Judge: 0xean
Total Solo HM: 4
Id: 22
League: ETH
Rank: 4/16
Findings: 6
Award: $5,945.09
🌟 Selected for report: 2
🚀 Solo Findings: 0
2106.6725 USDC - $2,106.67
cmichel
The LongShort._batchConfirmOutstandingPendingActions
function uses the batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][false]
field to determine how much short tokens to shift to long tokens.
However, this field is not cleared, instead, the long options (true
) are cleared again:
// Handle shift tokens from SHORT to LONG amountForCurrentAction_workingVariable = batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][ false ]; if (amountForCurrentAction_workingVariable > 0) { int256 paymentTokenValueChangeForShiftToLong = int256( _getAmountPaymentToken(amountForCurrentAction_workingVariable, syntheticTokenPrice_inPaymentTokens_short) ); short_changeInMarketValue_inPaymentToken -= paymentTokenValueChangeForShiftToLong; long_changeInMarketValue_inPaymentToken += paymentTokenValueChangeForShiftToLong; changeInSupply_syntheticToken_short -= int256(amountForCurrentAction_workingVariable); changeInSupply_syntheticToken_long += int256( _getEquivalentAmountSyntheticTokensOnTargetSide( amountForCurrentAction_workingVariable, syntheticTokenPrice_inPaymentTokens_short, syntheticTokenPrice_inPaymentTokens_long ) ); // @audit should be [false] batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][true] = 0; }
An attacker can shift short to long tokens once, and can then repeatedly print long tokens and empty short tokens on every _batchConfirmOutstandingPendingActions
call as this field is not cleared.
This completely breaks the market as the short and long token total supply is wrong as well as the "changeInMarketValue" variables wrongly change the short/long value of the market.
User funds will be lost as the short side keeps losing in value and the long side gains value through the mints. (An attacker can also use this information to make a profit.)
Reset the correct field by assigning batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][false] = 0;
.
#0 - JasoonS
2021-08-12T11:38:00Z
Duplicate of #5
1053.3363 USDC - $1,053.34
cmichel
The LongShort.getUsersConfirmedButNotSettledSynthBalance
function estimates the outstanding synthetic tokens using the latest synthetic token price (currentMarketUpdateIndex
), instead of the price at the next price update after the user deposited (userNextPrice_currentUpdateIndex[marketIndex][user]
).
// see getUsersConfirmedButNotSettledSynthBalance uint256 amountPaymentTokenDeposited = userNextPrice_paymentToken_depositAmount[marketIndex][isLong][user]; // @audit uses current price uint256 syntheticTokenPrice = syntheticToken_priceSnapshot[marketIndex][isLong][currentMarketUpdateIndex]; confirmedButNotSettledBalance = _getAmountSyntheticToken(amountPaymentTokenDeposited, syntheticTokenPrice); // see actual amount minted at _executeOutstandingNextPriceMints uint256 currentPaymentTokenDepositAmount = userNextPrice_paymentToken_depositAmount[marketIndex][isLong][user]; uint256 amountSyntheticTokensToTransferToUser = _getAmountSyntheticToken( currentPaymentTokenDepositAmount, syntheticToken_priceSnapshot[marketIndex][isLong] // @audit uses deposit price [userNextPrice_currentUpdateIndex[marketIndex][user]] );
The actual mint amount in _executeOutstandingNextPriceMints
however uses the price of the next price update after the user deposit.
Any contracts or frontends relying on getUsersConfirmedButNotSettledSynthBalance
will receive a wrong token balance if the account has not "claimed" yet.
Note that this is also the function that SyntheticToken.balanceOf
uses, the standard ERC20 function, which is used a lot.
This can lead to losses if this balance is relied on in any way.
Use the correct synth price in the getUsersConfirmedButNotSettledSynthBalance
view function.
#0 - JasoonS
2021-08-12T07:26:41Z
1 low risk
Duplicate #142
1053.3363 USDC - $1,053.34
cmichel
Minting/redeeming tokens only happens in the next "epoch" where an epoch is started only if the underlying prices change.
Stable underlyings that don't change the price often or have a low precision can lead to issues.
Consider starting a new epoch either at each price change or after X minutes have passed to force the protocol to process outstanding actions.
#0 - JasoonS
2021-08-12T06:16:18Z
Duplicate of #16
210.6673 USDC - $210.67
cmichel
The same market can be created twice using LongShort.createNewSyntheticMarket
with the exact same parameters.
The only difference will be the marketIndex
(and the newly created synthetic token contracts).
This will lead to a fragmentation of liquidity and is bad for the protocol.
Disallow creating the same synthetic twice.
Once could hash (_paymentToken, _oracleManager, _yieldManager)
and keep a map bytes32 => bool
of hashed tuples to created markets, set it to true
in createNewSyntheticMarket
and revert if it's already true
.
#0 - JasoonS
2021-08-12T06:22:26Z
Duplicate #10
Interesting idea of hashing those addresses. However, itt doesn't prevent the issue of _paymentToken
and _oracleManager
being the same, but only the _yieldManager
changing. Thanks.
I think that the mitigation in #48 is the best. Since it is non-critical to have two marktes use the same payment token and oracle manager. The yieldManager
is the important one!
#1 - 0xean
2021-08-26T11:49:03Z
dupe of #48
🌟 Selected for report: cmichel
780.2491 USDC - $780.25
cmichel
The LongShort._handleTotalPaymentTokenValueChangeForMarketWithYieldManager
function assumes that the YieldManager
indeed withdraws all of the desired payment tokens, but it could be that they are currently lent out at Aave.
// NB there will be issues here if not enough liquidity exists to withdraw // Boolean should be returned from yield manager and think how to appropriately handle this IYieldManager(yieldManagers[marketIndex]).removePaymentTokenFromMarket( uint256(-totalPaymentTokenValueChangeForMarket) );
Trying to withdraw these tokens will then fail.
Boolean should be returned from yield manager and think how to appropriately handle this 😁
#0 - JasoonS
2021-08-12T11:37:18Z
That happens on the user level, they can just try again later (our UI will give a good message).
It was designed like this.
#1 - 0xean
2021-08-26T01:58:11Z
based on the incorrect comment in the code I am going to leave as a 1 (per - https://docs.code4rena.com/roles/wardens/judging-criteria#estimating-risk-tl-dr)
but it does look like the system is designed to handle this scenario regardless of the boolean returned (or not being returned)
351.1121 USDC - $351.11
cmichel
The FloatCapital_v0.initialize
/FloatToken.initializeFloatToken
/LongShort.initialize
functions that initialize important contract state can be called by anyone.
The attacker can initialize the contract before the legitimate deployer, hoping that the victim continues to use the same contract. In the best case for the victim, they notice it and have to redeploy their contract costing gas.
Use the constructor to initialize non-proxied contracts.
For initializing proxy contracts deploy contracts using a factory contract that immediately calls initialize
after deployment or make sure to call it immediately after deployment and verify the transaction succeeded.
#0 - JasoonS
2021-08-12T10:31:23Z
Duplicate #82
🌟 Selected for report: cmichel
249.7566 USDC - $249.76
cmichel
The SyntheticToken
overwrites the _beforeTokenTransfer
hook and removes the pausing functionality of ERC20PresetMinterPauser
.
But the ERC20PresetMinterPauser
constructor still assigns pauser roles which leads to unnecessary gas costs.
Inherit from an ERC20PresetMinterPauser
-like contract without the pausing functionality.
This would also make the intention of the code more clear by showcasing that it does not implement the pauser interface functions pause
/unpause
(which it currently still does but they don't have any effect).
#0 - JasoonS
2021-08-12T11:36:02Z
Thanks