Tracer contest - gpersoon's results

Build and trade with Tracer’s Perpetual Swaps and gain leveraged exposure to any market in the world.

General Information

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

Tracer

Findings Distribution

Researcher Performance

Rank: 3/12

Findings: 6

Award: $11,627.67

🌟 Selected for report: 7

πŸš€ Solo Findings: 3

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: gpersoon, pauliax

Labels

bug
duplicate
3 (High Risk)

Awards

1811.7475 USDC - $1,811.75

External Links

Handle

gpersoon

Vulnerability details

Impact

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)

Proof of Concept

// 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); } }

Tools Used

Remove the code or limit the period for which it can be performed.

#0 - raymogg

2021-07-05T03:10:26Z

Duplicate of #81

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

2013.0528 USDC - $2,013.05

External Links

Handle

gpersoon

Vulnerability details

Impact

The library prb-math documents that it is not audited by a security researcher. This means its more risky to rely on this library.

Proof of Concept

// https://github.com/hifi-finance/prb-math#security The contracts have not been audited by a security researcher.

Tools Used

Consider (crowdsourcing) an audit for prb-math

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

2013.0528 USDC - $2,013.05

External Links

Handle

gpersoon

Vulnerability details

Impact

A liquidator can always claim the liquidation escrow in the following way:

  • create a second account
  • setup a complimentary trade in that second account, which will result in a large slippage when executed
  • call executeTrade (which everyone can call), to execute a trade between his own two accounts with a large slippage
  • the slippage doesn't hurt because the liquidator owns both accounts
  • call claimReceipt with the receiptId of the executed order, within the required period (e.g. 15 minutes)

Proof of Concept

// 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 {

Tools Used

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)

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
2 (Med Risk)
sponsor confirmed
disagree with severity

Awards

2013.0528 USDC - $2,013.05

External Links

Handle

gpersoon

Vulnerability details

Impact

It's possible to avoid paying insurance in the following way:

  • once per hour (at the right moment), do the following: ----using a flash loan, or with a large amount of tokens, call deposit of Insurance.sol to make sure that the pool is sufficiently filled (poolHoldings > poolTarget) ----call the function executeTrade of Trader.sol with a minimal trade (possibly of value 0, see finding "executeTrade with same trades") ----executeTrade calls matchOrders, which calls recordTrade ----recordTrade calls updateFundingRate(); (once per hour, so you have to be sure you do it in time before other trades trigger this) ----updateFundingRate calls getPoolFundingRate ----getPoolFundingRate determines the insurance rate, but because the insurance pool is sufficiently full (due to the flash loan), the rate is 0 ----updateFundingRate stores the 0 rate via setInsuranceFundingRate (which is used later on to calculate the amounts for the insurances) ----withdraw from the Insurance and pay back the flash loan

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.

Proof of Concept

// 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; }

Tools Used

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.

Findings Information

🌟 Selected for report: cmichel

Also found by: gpersoon, tensors

Labels

bug
duplicate
2 (Med Risk)

Awards

543.5243 USDC - $543.52

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

// 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 {

Tools Used

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

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: JMukesh, cmichel, gpersoon, pauliax, shw

Labels

bug
duplicate
1 (Low Risk)

Awards

66.0382 USDC - $66.04

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

//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); }

Tools Used

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

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: cmichel, gpersoon

Labels

bug
duplicate
1 (Low Risk)

Awards

181.1748 USDC - $181.17

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

// https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L26 uint256 public constant override LIQUIDATION_GAS_COST = 63516;

Tools Used

Make the LIQUIDATION_GAS_COST parameter updatable.

#0 - raymogg

2021-07-08T00:32:42Z

Duplicate of #101

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

671.0176 USDC - $671.02

External Links

Handle

gpersoon

Vulnerability details

Impact

There are several todos left in the code.

Proof of Concept

.\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.

Tools Used

Check, fix and remove the todos before it is deployed in production

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
1 (Low Risk)
sponsor confirmed
disagree with severity

Awards

671.0176 USDC - $671.02

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

// 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; }

Tools Used

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.

Findings Information

🌟 Selected for report: shw

Also found by: gpersoon

Labels

bug
duplicate
1 (Low Risk)

Awards

301.9579 USDC - $301.96

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

// 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; }

Tools Used

Check the error handling for the situation there are no trades for 24 hours.

#0 - raymogg

2021-07-08T00:58:44Z

duplicate of #140

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

671.0176 USDC - $671.02

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

// 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); }

Tools Used

Add something like: tempFees = min (fees, tvl); and change fees=0 to: fees -= tempFees;

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

671.0176 USDC - $671.02

External Links

Handle

gpersoon

Vulnerability details

Impact

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.

Proof of Concept

// 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;

Tools Used

Add something like: require ( order1.market == address(this), "Wrong market");

Note: canMatch already verifies that order1.market== order2.market

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter