infrastructure-upgrade/lib/v3-core/audits/tob/README.md
T-Hax 735546619e
init
Signed-off-by: T-Hax <>
2023-04-08 18:46:18 +00:00

7.6 KiB

The following contains the properties written by Trail of Bits.

End to End testing with Echidna

We've implemented end-to-end properties for the Uniswap V3 Core pool contract.

Installation

In order to run this, you need to install echidna 1.7.0.

Run

Assuming you're in the root of the repo

echidna-test contracts/crytic/echidna/E2E_swap.sol --config contracts/crytic/echidna/E2E_swap.config.yaml --contract E2E_swap

echidna-test contracts/crytic/echidna/E2E_mint_burn.sol --config contracts/crytic/echidna/E2E_mint_burn.config.yaml --contract E2E_mint_burn

echidna-test contracts/crytic/echidna/Other.sol --config contracts/crytic/echidna/Other.config.yaml --contract Other

Random but valid pool initialisation, created positions, and priceLimits

To help Echidna to get good coverage we've used multiple helper functions to:

  • create random but valid pool initialization params (fee, tickSpacing, initial price) before doing swaps or mint/burn link link
  • create a random number of random but valid positions before testing swaps link
  • create a random but valid priceLimit when doing swap link
  • create random but valid position params when doing mint link

By doing the above Echidna will be able to test the actual properties we want to test instead of bashing it's head against using an invalid priceLimit or invalid position params. The above also allows the creation of a dynamic number of random positions before executing swaps instead of using a static list.

To achieve the above random but valid outcomes we use the uint128 _amount of swap/mint/burn as a seed to create randomness in the helper functions. This also means that retrieving the exact used params is not very straightforward. However, through a combination of hardhat console.sol and writing a small js unit test it's possible to retrieve the exact used params of every (list of) call(s).

Adjust hardhat.config.ts

The Echidna contracts cost too much gas to deploy due to all the calls inside the constructor.

Adjust the hardhat.config.ts to:

hardhat: {
    allowUnlimitedContractSize: true,
    gas: 950000000,
    blockGasLimit: 950000000,
    gasPrice: 1,
},

E2E_swap: retrieving the pool initialisation params and created positions

The pool initialisation and created positions is deterministic and there is a view function that will return whatever a specific _amount (used as _seed) will create.

Write a js unit test:

console.log(await E2E_swap.viewRandomInit('<the _amount of the first swap call>'))

E2E_swap: retrieving the used priceLimit of a swap

The priceLimit depends on the state of the pool contract, it is therefore easiest to retrieve by using hardhat's console.sol.

Uncomment the following lines:

  • at the top of E2E_swap.sol: // import 'hardhat/console.sol';
  • inside the swap functions: // console.log('sqrtPriceLimitX96 = %s', sqrtPriceLimitX96);

Instead of just one swap call, imagine Echidna reports two swap calls, and the second one causes an assertion failure.

// to get pool params + created positions
console.log(await E2E_swap.viewRandomInit('<the _amount of the first swap call>'))

// execute the swap, which will create the above and log the used priceLimit to the console
await E2E_swap.test_swap_exactOut_oneForZero('<the _amount of the first swap call>')

// execute the swap, logs the used priceLimit to the console
await E2E_swap.test_swap_exactIn_oneForZero('<the _amount of the second swap call>')

E2E_mint_burn: retrieving the pool initialisation params

The pool initialisation params creation is deterministic and there is a view function that will return whatever a specific _amount (used as _seed) will create.

Write a js unit test:

console.log(await E2E_mint_burn.viewInitRandomPoolParams('<the _amount of the first mint call>'))

E2E_mint_burn: retrieving a mint's created random position

const poolInitParams = await E2E_mint_burn.viewInitRandomPoolParams('<the _amount of the first mint call>')

const positionParams = await E2E_mint_burn.viewMintRandomNewPosition(
  '<the _amount of the first mint call>',
  poolInitParams.tickSpacing,
  poolInitParams.tickCount,
  poolInitParams.maxTick
)

console.log(positionParams)

E2E_mint_burn: retrieving a burn's used position

Uncomment the following lines:

  • at the top of E2E_mint_burn.sol: // import 'hardhat/console.sol';
  • inside the particular burn function: // console.log('burn posIdx = %s', posIdx);
  • if this is a partial burn, also want to see the burned amount. inside the test_burn_partial function: // console.log('burn amount = %s', burnAmount);

Then execute the burn in a js test.

// show pool init params
const poolInitParams = await E2E_mint_burn.viewInitRandomPoolParams('<the _amount of the first mint call>')
console.log(positionParams)

// show pool mint position params
const positionParams = await E2E_mint_burn.viewMintRandomNewPosition(
  '<the _amount of the first mint call>',
  poolInitParams.tickSpacing,
  poolInitParams.tickCount,
  poolInitParams.maxTick
)
console.log(positionParams)

// execute the first mint
await E2E_mint_burn.test_mint('<the _amount of the first mint call>')

// execute the burn
await E2E_mint_burn.test_burn_partial('<the _amount of the first test_burn_partial call>')
// this should log the index of the position that was burned to the console
// as well as the amount that was burned.
// together with the above output this should make it clear which exact position
// was burned and how much

Verification with Manticore

The verification was performed with the experimental branch dev-evm-experiments, which contains new optimizations and is a work in progress. Trail of Bits will ensure that the following properties hold once the branch has stabilized and been included in a Manticore release.

For conveniance, we followed the pattern "if there is reacheable path, there is a bug".

To verify a property, run:

manticore . --contract CONTRACT_NAME --txlimit 1 --smt.solver all --quick-mode --lazy-evaluation --core.procs 1

The command might change once dev-evm-experiments has stabilized

Manticore will create a mcore_X directory. If no X.tx file is generated, it means that Manticore did not find a path violating the property.

ID Description Contract Status
01 BitMath.mostSignificantBit returns a value in x >= 2**msb && (msb == 255 or x < 2**(msb+1)). VerifyBitMathMsb Verified
02 BitMath.leastSignificantBit returns a value in ((x & 2** lsb) != 0) && ((x & (2**(lsb -1))) == 0). VerifyBitMathLsb Verified
03 If LiquidityMath.addDelta returns, the value will be equal to x + uint128(y). VerifyLiquidityMathAddDelta Verified