Platform: Code4rena
Start Date: 24/06/2021
Pot Size: $80,000 USDC
Total HM: 18
Participants: 12
Period: 7 days
Judge: cemozer
Total Solo HM: 11
Id: 16
League: ETH
Rank: 2/12
Findings: 7
Award: $19,609.10
๐ Selected for report: 8
๐ Solo Findings: 3
๐ Selected for report: cmichel
6710.1761 USDC - $6,710.18
cmichel
The Liquidation
contract allows the liquidator to submit "bad" trade orders and the insurance reimburses them from the insurance fund, see Liquidation.claimReceipt
.
The function can be called with an orders
array which does not check for duplicate orders.
An attacker can abuse this to make a profit by liquidating themselves, making a small bad trade and repeatedly submitting this bad trade for slippage reimbursement.
Example:
claimReceipt(orders)
where orders
is an array that contains many duplicates of the "bad" trade, for example 100 times. The calcUnitsSold
function will return unitsSold = receipt.amountLiquidated
and a bad avgPrice
. They are now reimbursed the price difference on the full liquidation amount (instead of only on 1% of it) making an overall profitThis can be repeated until the insurance fund is drained.
The attacker has an incentive to do this attack as it's profitable and the insurance fund will be completely drained.
Disallow duplicate orders in the orders
argument of claimReceipt
. This should make the attack at least unprofitable, but it could still be a griefing attack.
A quick way to ensure that orders
does not contain duplicates is by having liquidators submit the orders in a sorted way (by order ID) and then checking in the calcUnitsSold
for
loop that the current order ID is strictly greater than the previous one.
#0 - CalabashSquash
2021-07-05T04:41:25Z
Valid issue. The recommended mitigation step would also work. :+1:
๐ Selected for report: cmichel
6710.1761 USDC - $6,710.18
cmichel
The GasOracle
uses two chainlink oracles (GAS in ETH with some decimals, USD per ETH with some decimals) and multiplies their raw return values to get the gas price in USD.
However, the scaling depends on the underlying decimals of the two oracles and could be anything. But the code assumes it's in 18 decimals.
"Returned value is USD/Gas * 10^18 for compatibility with rest of calculations"
There is a toWad
function that seems to involve scaling but it is never used.
If the scale is wrong, the gas price can be heavily inflated or under-reported.
Check chainlink.decimals()
to know the decimals of the oracle answers and scale the answers to 18 decimals such that no matter the decimals of the underlying oracles, the latestAnswer
function always returns the answer in 18 decimals.
#0 - raymogg
2021-07-05T03:09:15Z
Disagree with severity as while the statement that the underlying decimals of the oracles could be anything, we will be using production Chainlink feeds for which the decimals are known at the time of deploy.
This is still however an issue as you don't want someone using different oracles (eg non Chainlink) that have different underlying decimals and not realising that this contract will not support that.
#1 - kumar-ish
2021-07-15T13:26:58Z
Closed by accident
#2 - cemozerr
2021-07-18T22:11:59Z
Marking this a high-risk issue as it poses a big threat to users deploying their own markets
cmichel
The Chainlink API (latestAnswer
) used in the GasOracle
oracle wrappers is deprecated:
This API is deprecated. Please see API Reference for the latest Price Feed API. Chainlink Docs
It seems like the old API can return stale data. Checks similar to that of the new API using latestTimestamp
and latestRoundare
are needed.
This could lead to stale prices according to the Chainlink documentation:
Add the recommended checks:
( uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound ) = chainlink.latestRoundData(); require( timeStamp != 0, โChainlinkOracle::getLatestAnswer: round is not completeโ ); require( answeredInRound >= roundID, โChainlinkOracle::getLatestAnswer: stale dataโ ); require(price != 0, "Chainlink Malfunctionโ);
#0 - raymogg
2021-07-05T03:18:09Z
Duplicate of #145
cmichel
The ERC20.transfer()
and ERC20.transferFrom()
functions return a boolean value indicating success. This parameter should be checked for success.
The Insurance.deposit
and Insurace.withdraw
functions dp not check the return value:
// deposit collateralToken.transferFrom(msg.sender, address(this), rawTokenAmount); // withdraw collateralToken.transfer(msg.sender, rawTokenAmount);
Some tokens do not revert if the transfer failed but return false
instead.
Tokens that don't actually perform the transfer and return false
are still counted as a correct transfer.
We recommend using OpenZeppelinโs SafeERC20
versions with the safeTransfer
and safeTransferFrom
functions that handle the return value check as well as non-standard-compliant tokens.
#0 - raymogg
2021-07-05T03:24:55Z
Duplicate of #115
๐ Selected for report: cmichel
2013.0528 USDC - $2,013.05
cmichel
The TracerPerpetualSwaps.settle
function updates the user's last index to currentGlobalFundingIndex
, however a comment states:
"// Note: global rates reference the last fully established rate (hence the -1), and not the current global rate. User rates reference the last saved user rate"
The code for the else
branch also updates the last index to currentGlobalFundingIndex - 1
instead of currentGlobalFundingIndex
.
if (accountBalance.position.base == 0) { // set to the last fully established index // @audit shouldn't this be global - 1 like below? accountBalance.lastUpdatedIndex = currentGlobalFundingIndex; accountBalance.lastUpdatedGasPrice = IOracle(gasPriceOracle).latestAnswer(); }
It might be possible that first-time depositors skip having to pay the first funding rate period as the accountLastUpdatedIndex + 1 < currentGlobalFundingIndex
check will still return false
when the funding rates are updated the next time.
Check if setting it to currentGlobalFundingIndex
or to currentGlobalFundingIndex - 1
is correct.
cmichel
The Trader
contract accepts two signed orders and tries to match them.
Once they are matched and become filled, they can therefore not be matched against other orders anymore.
This allows for a griefing attack where an attacker can deny any other user from trading by observing the mempool and front-running their trades by creating their own order and match it against the counter order instead.
A trader can be denied from trading. The cost of the griefing attack is that the trader has to match the order themselves, however depending on the liquidity of the order book and the spread, they might be able to do the counter-trade again afterwards, basically just paying the fees.
It could be useful if the attacker is a liquidator and is stopping a user who is close to liquidation from becoming liquid again.
This seems hard to circumvent in the current design.
If the order book is also off-chain, the executeTrade
could also be a bot-only function.
#0 - raymogg
2021-07-05T03:21:16Z
Duplicate of #123
#1 - loudoguno
2021-08-17T23:13:17Z
reopening to reflect judges sheet and final report
cmichel
Some parameters of functions are not checked for invalid values:
TracerPerpetualSwaps.constructor
: The addresses can be checked for non-zero. Percentage values like feeRate
, _deleveragingCliff
, _insurancePoolSwitchStage
should be checked to be less than 100% (1e18
)OracleAdapter.constructor
: The addresses can be checked for non-zero.A wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas.
Validate the parameters.
#0 - raymogg
2021-07-08T00:23:04Z
This ticket covers topics that are duplicates of #136 (non zero address checks) and also #102 (percent value limits)
๐ Selected for report: cmichel
671.0176 USDC - $671.02
cmichel
There are several setter functions that do not check if the amount is less than 100%.
TracerPerpetualSwaps
: setFeeRate
, setDeleveragingCliff
, setInsurancePoolSwitchStage
Insurance
: setFeeRate
, setDeleveragingCliff
, setInsurancePoolSwitchStage
Setting values to more than 100% might lead to unintended functionality.
Ensure that the parameters are less than 100%.
181.1748 USDC - $181.17
cmichel
The TracerPerpetualSwaps.LIQUIDATION_GAS_COST
should be upgradeable as the gas costs of liquidations are likely to change with upcoming hardforks.
After a hardfork that increased the gas cost, the hardcoded LIQUIDATION_GAS_COST
might not be enough and the liquidation and min. margin balances are not accurate anymore.
Allow the owner to change LIQUIDATION_GAS_COST
.
#0 - loudoguno
2021-08-17T23:38:18Z
closing to reflect judges findings sheet as duplicate of #48
๐ Selected for report: cmichel
671.0176 USDC - $671.02
cmichel
When LibMath.abs
is called with -2^255 (type(int256).min
), it tries to multiply it by -1
but it'll fail as it exceeds the max signed 256-bit integers.
The function will fail with an implicit error that might be hard to locate.
Throw an error similar to toInt256
like int256 overflow
.
๐ Selected for report: cmichel
671.0176 USDC - $671.02
cmichel
When LibMath.sumN
function does not check if n <= arr.length
and can therefore fail if called with n > arr.length
.
The caller must always check that it's called with an argument that is less than n
which is inconvenient.
Change the condition to iterate up to min(n, arr.length)
.
๐ Selected for report: cmichel
671.0176 USDC - $671.02
cmichel
The pool holdings of Insurance
(publicCollateralAmount
and bufferCollateralAmount
) is in WAD (18 decimals) but it's used as a raw token value in drainPool
// amount is a mix of pool holdings, i.e., 18 decimals // this requires amount to be in RAW! if tracerMarginToken has > 18 decimals, it'll break, < 18 decimals will approve too much tracerMarginToken.approve(address(tracer), amount); // this requires amount to be in WAD which is correct tracer.deposit(amount);
If tracerMarginToken
has less than 18 decimals, the approval approves orders of magnitude more tokens than required for the deposit
call that follows.
If tracerMarginToken
has more than 18 decimals, the deposit
that follows would fail as fewer tokens were approved, but the protocol seems to disallow tokens in general with more than 18 decimals.
Convert the amount
to a "raw token value" and approve this one instead.
#0 - raymogg
2021-07-05T03:30:36Z
The issue is correct in pointing out that the wrong approve amount is used, however disagree with the severity.
It is common practice to approve the maximum amount of tokens for a contract to spend already. This bug simply allows more tokens to be approved (to a trusted contract in the system), than was intended. This is only exploitable if paired with another bug in the Tracer contracts. As is, no users would be affected.
#1 - cemozerr
2021-07-18T21:45:45Z
Marking this as low-risk as it would only pose a security threat coupled with another bug.