Contest ran 7 September 20228 September 2022

1 day contest

Canto Dex Oracle contest

Execution layer for original work.

$20,000 worth of CANTO

Total Awards

Canto Dex Oracle contest details

Canto Docs

Link to docs

Oracle Audit

Total LOC: 155

Files in scope

Not all code in the contracts below are in scope. The specific functions in scope are detailed below.

FileSLOCCoverage
Contracts (2)
src/Swap/BaseV1-core.sol 🧮 🔖 Σ47678.15%
src/Swap/BaseV1-periphery.sol 💰 📤 🧮52560.71%
Total (over 2 files):100170.24%

Oracle Overview

The oracle we define below is used in the Canto-Lending Market to determine the value of asset collateral of ($CANTO, $NOTE, $USDC $USDT, $ETH, $ATOM). It is also used to determine the value of lpTokens used as collateral. As such, the oracle is general purpose in that it is able to determine prices, from specified pairs deployed from the router, of non-LP and LP tokens.
Stable pairs follow the x^3y + y^3x = k CF curve invariant. While volatile pairs follow the regular constant-product CF, xy = k, where x and y are the reserves of the assets in the pool.

Integration Tests

Integration tests using CLM in conjunction with the oracle may be found here,

To Run Tests

All in one command:

rm -Rf 2022-09-canto || true && git clone https://github.com/code-423n4/2022-09-canto.git && cd 2022-09-canto && nvm install 16.0.0 && yarn install --lock-file && REPORT_GAS=true npx hardhat test

or:

nvm install 16.0.0

yarn install --lock-file

npx hardhat test

Oracle specific tests are found here: ./test/canto/Oracle

BaseV1-Periphery

getUnderlyingPrice (SLOC: 487 - 522): 35 LOC -

Explanation

This method is defined to adhere to the Compound PriceOracle interface. The Comptroller calls this method when calculating accountLiquidities for users who hold balances of ERC20 tokens. These tokens are priced in terms of $NOTE . Stable coins (USDC, USDT, NOTE) have prices that are statically set to 1e18.

For non-stable tokens, there are two cases,

  • The token is an lpToken
    If the token to be priced is an lpToken, the price of the lpToken is determined from getPriceLP (defined below)
  • The token is not an lpToken
    The Price of the token is determined through the reserve ratio determined from the Canto/Token pool * price from Canto/Note pool. All non-stable tokens, are priced in reference to the Canto/Token Pool.

The final price is scaled by 1e18.

Concerns

  • Is it possible for users to create non-stable pairs for stable pairs, and have the router reference them in price determination, and vice-versa for non-stable assets and stable pairs. i.e. we're trying to get USDC/NOTE stable price, but a 3rd party has deployed USDC/NOTE non-stable, and ensure that oracle is looking at correct one

External Calls

  • L495
    address(ICErc20(address(ctoken)).underlying())
    
  • L502/506
    erc20(underlying).decimals();
    

getPriceLP (SLOC: 549 - 593): 14 LOC -

Explanation

This method is used to price lpTokens. There are two cases,

  • The pair that lpToken is minted from is stable
    The Router will reference the Note/Token pool. The lpToken calculation determines the reserves of Note in the pool (unitReserves) and the reserves of the other asset in the pool (assetReserves)
  • The pair that lpToken is minted from is not stable
    The Router will reference the Canto/Token pool. The lpToken calculation determines the reserves of Canto in the pool (unitReserves) and the reserves of the other token in the pool (assetReserves)

along with the reserves, the last 8 price observations are determined, where the price is represented as follows

prices = sample(token_asset, token_asset_decimals, 8, 1)

This returns the amount of the unitToken received (Canto or Note) from swapping 10 **token.decimals() of the assetToken.

The totalSupply of lpTokens at each of the most recent 8 observations is also recorded. The lpTokenPrice is determined as follows

sum_i((token_asset_reserves[i] * price[i] + token_unit_reserves[i])/totalSupply[i])

Where token_asset_reserves, price, token_unit_reserves, and totalSupply are time weighted Averages of reserves, prices, and totalSupply from the pair. These values are determined from the most recent observations, the sample size is determined by user.

Concerns

  • What is the tolerance of the lpToken Pricing to large moves in volatility / reserve sizes in both volatile / not-volatile pairs. I.e - try to induce large moves in reserves sizes to force price to be over/under-estimate of actual reserve ratio.
  • Inaccuracy resulting from rounding errors.
  • StableSwap invariant for stable pairs x^3y + xy^3 that is used, may also lead to inaccuracy in getAmountOut calculations

External Calls

  • L560/564/569/574
    10 ** (erc20(token1).decimals());
    
  • L561/565/571/575
    pair.sample(token0, decimals, 8, 1) // call to BaseV1Pair
    
  • L562/566/572/576
    pair.sampleReserves(8, 1);
    

getPriceCanto (SLOC: 525 - 534): 9 LOC -

Explanation

This method is used to determine the price of assets in terms of Canto. This method calls quote on the volatile pair between Canto/Asset. This returns the amount of Canto received when swapping 10 ** token.decimals() for Canto in the Canto/Asset pair

External Calls

  • L532
    IBaseV1Pair(pair).quote(address(token), decimals, 8)
    

getPriceNote (SLOC: 537 - 546): 9 LOC -

Explanation

Returns the price of the asset in Note. This is called for the deriving the price of stable assets. All stable assets are swapped in pools between the asset and Note. This method works similarly to getPriceCanto . All stable pools obey the curve invariant x^3y + y^3x = k as such the calculations for amountOut are different than in Canto/Asset pairs.

Concerns

  • Tolerance of stableswap invariant used to large moves in reserves of either asset.

External Calls

  • L544
    IBaseV1Pair(pair).quote(address(token), decimals, 8)
    

BaseV1-Core

_update (SLOC: 137 - 153): 16 LOC -

Explanation

Adds the current values of the reserves and totalSupply times the timeDiff between the last observations to the supply and reserve accumulators. After updateFrequency difference in blockTimestamps between the last recorded block timestamp and the current block timestamp, the reserveCumulative and totalSupplyCumulative values are written to state. Each time that any of these values changes, the accumulators are adjusted as follows,

reserve0Cumulative += timeDiff * reserve0
reserve1Cumulative += timeDiff * reserve1
totalSupplyCumulataive += timeDiff * totalSupply

Where timeDiff is the difference in block timestamps between the current block timestamp, and the last time time that update was called. This enables time weighted average s to be calculated between observations as follows,

(observation[i].reserve0Cumulative - observation[i-1].resrve0Cumulative)/timeElapsed

Reserves (SLOC: 224 - 237) 13 LOC -

Explanation

This method returns the totalSum of the last n recorded reserves for either pair, this value can then be averaged by the number of requested values, to obtain a time weighted average over the number of periods requested.

sampleReserves (SLOC: 237 - 259): 22 LOC -

Explanation

This method returns the TW value of the reserves of either asset between observations. The user is able to determine the windowSize, and the number of windows to return TW values for, it is calculated as follows,

timeElapsed = observation[window * (i)].timeStamp - observation[window*(i - 1)].timeStamp
reserve0[i] = (observation[window * (i)].reserve0Cumulative - observation[window*(i - 1)].reserve0Cumulative) / timeElapsed
reserve1[i] = (observation[window * (i)].reserve1Cumulative - observation[window*(i - 1)].reserve1Cumulative) / timeElapsed

totalSupplyAvg (SLOC: 260 - 269): 9 LOC -

Explanation

This serves the same purpose as reserves however, the values summed are returned from sampleSupply (defined below)

sampleSupply (SLOC: 271 - 289): 18 LOC -

Explanation

This method returns the time weighted value of the reserves of either asset between observations. The user is able to determine the windowSize, and the number of windows to return time weighted values for, it is calculated as follows,

timeElapsed = observation[window * (i)].timeStamp - observation[window*(i - 1)].timeStamp
supply[i] = (observation[window * (i)].totalSupplyCumulative - observation[window*(i - 1)].totalSupplyCumulative) / timeElapsed