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: 3/12
Findings: 6
Award: $11,627.67
π Selected for report: 7
π Solo Findings: 3
gpersoon
The contract TracerPerpetualSwaps inherits from SafetyWithdraw, which means the function withdrawERC20Token is possible, This allows the projectowners to withdraw the ERC20 tokens from the contract, which can be seen as a rug pull
Also the tvl variable and other administration is not updated.
Even this is well intended the project could still be call out, see for example: https://twitter.com/RugDocIO/status/1408097542202531840)
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L20 contract TracerPerpetualSwaps is ITracerPerpetualSwaps, Ownable, SafetyWithdraw {
//https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/SafetyWithdraw.sol#L7 contract SafetyWithdraw is Ownable, ISafetyWithdraw { function withdrawERC20Token(address tokenAddress, address to, uint256 amount) external override onlyOwner { IERC20(tokenAddress).transfer(to, amount); } }
Remove the code or limit the period for which it can be performed.
#0 - raymogg
2021-07-05T03:10:26Z
Duplicate of #81
π Selected for report: gpersoon
2013.0528 USDC - $2,013.05
gpersoon
The library prb-math documents that it is not audited by a security researcher. This means its more risky to rely on this library.
// https://github.com/hifi-finance/prb-math#security The contracts have not been audited by a security researcher.
Consider (crowdsourcing) an audit for prb-math
π Selected for report: gpersoon
2013.0528 USDC - $2,013.05
gpersoon
A liquidator can always claim the liquidation escrow in the following way:
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L67 function executeTrade(Types.SignedLimitOrder[] memory makers, Types.SignedLimitOrder[] memory takers) external override {
https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L394 function claimReceipt( uint256 receiptId, Perpetuals.Order[] memory orders, address traderContract) external override {
perhaps limit who can call executeTrade
#0 - raymogg
2021-07-05T04:22:52Z
Valid issue which would allow someone to get reimbursed for slippage against themselves.
The Trader contract will have whitelisted relayers added to prevent issues like this (similar to #119)
π Selected for report: gpersoon
2013.0528 USDC - $2,013.05
gpersoon
It's possible to avoid paying insurance in the following way:
The insurance rates are 0 now and no-one pays insurance. The gas costs relative to the insurance costs + the flash loan fees determine if this is an economically viable attack. Otherwise it is still a grief attack This will probably be detected pretty soon because the insurance pool will stay empty. However its difficult to prevent.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L45 function deposit(uint256 amount) external override {
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L74 function withdraw(uint256 amount) external override {
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L69
function recordTrade(uint256 tradePrice) external override onlyTracer {
..
if (startLastHour <= block.timestamp - 1 hours) {
..
updateFundingRate();
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L141 function updateFundingRate() internal { .. int256 iPoolFundingRate = insurance.getPoolFundingRate().toInt256(); .. int256 iPoolFundingRateValue = currentInsuranceFundingRateValue + iPoolFundingRate; .. setInsuranceFundingRate(iPoolFundingRate, iPoolFundingRateValue);
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L204 function getPoolFundingRate() external view override returns (uint256) { .. // If the pool is above the target, we don't pay the insurance funding rate if (poolTarget <= poolHoldings) { return 0; }
Set a timelock on withdrawing insurance
#0 - raymogg
2021-07-05T03:52:30Z
Really like this exploit idea. Currently this is possible since the Trader is not whitelisted (eg there is no whitelisted relayer address). With this added, this exploit is no longer possible as only off chain relayers can place orders with the trader.
Disagree with the severity mainly due to the fact that executing this exploit once would only cause insurance funding to not be paid for a single hour. For insurance funding to never be paid, you would have to time this transaction as the first transaction on each and every hour. This would quickly be noticed. The only affect on this would be insurance depositors miss interest payments for a few periods.
#1 - cemozerr
2021-07-18T19:58:17Z
Marking this as medium risk as a front-runner could keep doing this for not paying any funding using a bot.
gpersoon
An attacker could monitor the mempool and see an executeTrade transaction. He then could checkout the parameters and see if a better trade is possible for himself. He might even create and sign a new trade and then submit the new trade via an executeTrade transaction. If the trade is submitted with a higher fee and/or via a high priority channel like flashbots, taichi, bloxroute, archer, it might be executed first. Thus frontrunning the original executeTrade and the original executeTrade will probably fail.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L67 function executeTrade(Types.SignedLimitOrder[] memory makers, Types.SignedLimitOrder[] memory takers) external override {
Perhaps send the executeTrade transactions via a high priority channel Or commit trades first to the contract, before being able to be executed (only useful for low fee chains)
#0 - raymogg
2021-07-05T03:21:30Z
Duplicate of #123
gpersoon
The function transferOwnership of TracerPerpetualSwaps.sol contains a check for address(0), however the function transferOwnership of Liquidation.sol does not contain this check. So the ownership could be made inaccessible by accident.
//https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L572 function transferOwnership(address newOwner) public override(Ownable, ITracerPerpetualSwaps) onlyOwner { require(newOwner != address(0), "address(0) given"); super.transferOwnership(newOwner); }
//https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L445 function transferOwnership(address newOwner) public override(Ownable, ILiquidation) onlyOwner { super.transferOwnership(newOwner); }
ALso add the following check to transferOwnership of Liquidation.sol: require(newOwner != address(0), "address(0) given");
#0 - raymogg
2021-07-05T23:30:31Z
Duplicate of #136
gpersoon
The value for LIQUIDATION_GAS_COST is hardcoded in TracerPerpetualSwaps.sol. However with updates in solidity (like eip1559) this gas costs of instructions could change and this value might no longer be accurate.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L26 uint256 public constant override LIQUIDATION_GAS_COST = 63516;
Make the LIQUIDATION_GAS_COST parameter updatable.
#0 - raymogg
2021-07-08T00:32:42Z
Duplicate of #101
π Selected for report: gpersoon
671.0176 USDC - $671.02
gpersoon
There are several todos left in the code.
.\Pricing.sol: // todo by using public variables lots of these can be removed .\Trader.sol: // todo this could be succeptible to re-entrancy as .\lib\LibLiquidation.sol: // todo with the below * -1, note ints can overflow as 2^-127 is valid but 2^127 is not. .\lib\LibPrices.sol: // todo double check safety of this.
Check, fix and remove the todos before it is deployed in production
π Selected for report: gpersoon
671.0176 USDC - $671.02
gpersoon
In function calculateSlippage of LibLiquidation.sol, the value of amountToReturn is calculated by subtracting to numbers. Later on it is check if this value is negative. However amountToReturn is an unsigned integer so it can never be negative. If a negative number would be attempted to be assigned, the code will revert, because solidity 0.8 checks for this.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibLiquidation.sol#L106 function calculateSlippage( ... uint256 amountToReturn = 0; uint256 percentSlippage = 0; if (avgPrice < receipt.price && receipt.liquidationSide == Perpetuals.Side.Long) { amountToReturn = amountExpectedFor - amountSoldFor; } else if (avgPrice > receipt.price && receipt.liquidationSide == Perpetuals.Side.Short) { amountToReturn = amountSoldFor - amountExpectedFor; } if (amountToReturn <= 0) { // can never be smaller than 0, because amountToReturn is uint256 return 0; }
Double check if amountToReturn could be negative. If this is the case change the type of amountToReturn to int256 and add the appropriate type casts
#0 - raymogg
2021-07-08T03:05:56Z
Confirmed but think this is a 0 on severity. The check while a bit redundant on the less than case, is actually still needed as we do want to catch the case where amountToReturn = 0
.
#1 - cemozerr
2021-07-18T18:47:04Z
Marking this as low risk as a "Double check if amountToReturn could be negative" seems necessary for the scenarios where the amountToReturn could underflow.
gpersoon
The function averagePriceForPeriod calculates the average prices for the last 24 hours. In case no trades have been done, the variable j keeps the value of 0. Then the function meanN is called, which divides by the value of j (e.g. 0). This means the code will revert at that moment and no other code will be executed.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibPrices.sol#L57 function averagePriceForPeriod(PriceInstant[24] memory prices) internal pure returns (uint256) { ... uint256 j = 0; for (uint256 i = 0; i < 24; i++) { .. if (currPrice.trades == 0) { continue; } else { .... j++; } } return LibMath.meanN(averagePrices, j); } // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibMath.sol#L67 function meanN(uint256[] memory arr, uint256 len) internal pure returns (uint256) { return sumN(arr, len) / len; }
Check the error handling for the situation there are no trades for 24 hours.
#0 - raymogg
2021-07-08T00:58:44Z
duplicate of #140
π Selected for report: gpersoon
671.0176 USDC - $671.02
gpersoon
If you call the function withdrawFees and the "tvl" would not be enough for the fee then the code would revert. In this case the fees cannot be withdrawn. Although it is unlikely that the tvl would be wrong it is probably better to be able to withdraw the remaining fees.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L508 function withdrawFees() external override { uint256 tempFees = fees; fees = 0; tvl = tvl - tempFees;
// Withdraw from the account IERC20(tracerQuoteToken).transfer(feeReceiver, tempFees); emit FeeWithdrawn(feeReceiver, tempFees); }
Add something like: tempFees = min (fees, tvl); and change fees=0 to: fees -= tempFees;
π Selected for report: gpersoon
671.0176 USDC - $671.02
gpersoon
The function matchOrders of TracerPerpetualSwaps.sol doesn't check that the contract itself is indeed equal to order1.market and order2.market. The function executeTrade Trader.sol, which calls the matchOrders, can deal with multiple markets. Suppose there would be a mistake in executeTrade, or in a future version, the matchOrders would be done in the wrong market.
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L216 function matchOrders( Perpetuals.Order memory order1, Perpetuals.Order memory order2, uint256 fillAmount )
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L67 function executeTrade(Types.SignedLimitOrder[] memory makers, Types.SignedLimitOrder[] memory takers) external override { ... (bool success, ) = makeOrder.market.call( abi.encodePacked( ITracerPerpetualSwaps(makeOrder.market).matchOrders.selector, abi.encode(makeOrder, takeOrder, fillAmount) ) );
// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibPerpetuals.sol#L128 function canMatch( Order memory a, uint256 aFilled,Order memory b, uint256 bFilled ) internal view returns (bool) { ... bool marketsMatch = a.market == b.market;
Add something like: require ( order1.market == address(this), "Wrong market");
Note: canMatch already verifies that order1.market== order2.market