├── .nvmrc ├── .forge-snapshots ├── mint.snap ├── initialize.snap ├── simple swap.snap ├── poolExtsloadSlot0.snap ├── swap with hooks.snap ├── swap with native.snap ├── NoDelegateCallOverhead.snap ├── mint with empty hook.snap ├── HooksShouldCallBeforeSwap.snap ├── donate gas with 1 token.snap ├── donate gas with 2 tokens.snap ├── gas overhead of no-op lock.snap ├── mint with native token.snap ├── poolExtsloadTickInfoStruct.snap ├── swap against liquidity.snap ├── BitMathLeastSignificantBitMaxUint128.snap ├── BitMathLeastSignificantBitMaxUint256.snap ├── BitMathLeastSignificantBitSmallNumber.snap ├── BitMathMostSignificantBitMaxUint128.snap ├── BitMathMostSignificantBitMaxUint256.snap ├── BitMathMostSignificantBitSmallNumber.snap └── swap against liquidity with native token.snap ├── .yarnrc ├── .gitattributes ├── .prettierignore ├── .prettierrc ├── whitepaper-v4-draft.pdf ├── .gitignore ├── .solhint.json ├── .github ├── pull_request_template.md ├── workflows │ ├── lint.yml │ ├── tests.yml │ └── mythx.yml └── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ └── FEATURE_IMPROVEMENT.yml ├── test ├── __snapshots__ │ ├── PoolManager.spec.ts.snap │ ├── NoDelegateCall.spec.ts.snap │ ├── Hooks.spec.ts.snap │ ├── Tick.spec.ts.snap │ ├── BitMath.spec.ts.snap │ ├── SwapMath.spec.ts.snap │ ├── SqrtPriceMath.spec.ts.snap │ ├── TickBitmap.spec.ts.snap │ ├── Oracle.spec.ts.snap │ └── TickMath.spec.ts.snap ├── shared │ ├── constants.ts │ ├── expect.ts │ ├── format.ts │ ├── fixtures.ts │ ├── checkObservationEquals.ts │ ├── mockContract.ts │ └── utilities.ts ├── foundry-tests │ ├── utils │ │ ├── MockERC20.sol │ │ ├── TokenFixture.sol │ │ └── Deployers.sol │ ├── Owned.t.sol │ ├── SafeCast.t.sol │ ├── NoDelegateCall.t.sol │ ├── types │ │ └── BalanceDelta.t.sol │ ├── DynamicFees.t.sol │ ├── BitMath.t.sol │ └── Pool.t.sol ├── FullMath.spec.ts └── TickMath.spec.ts ├── remappings.txt ├── .gitmodules ├── contracts ├── libraries │ ├── FixedPoint128.sol │ ├── FixedPoint96.sol │ ├── PoolId.sol │ ├── UnsafeMath.sol │ ├── Fees.sol │ ├── SafeCast.sol │ ├── BitMath.sol │ ├── CurrencyLibrary.sol │ ├── Position.sol │ ├── TickBitmap.sol │ ├── SwapMath.sol │ ├── Hooks.sol │ └── FullMath.sol ├── interfaces │ ├── IDynamicFeeManager.sol │ ├── callback │ │ └── ILockCallback.sol │ ├── IProtocolFeeController.sol │ ├── IHookFeeManager.sol │ ├── external │ │ └── IERC20Minimal.sol │ └── IHooks.sol ├── test │ ├── FullMathTest.sol │ ├── UnsafeMathEchidnaTest.sol │ ├── PoolLockTest.sol │ ├── BitMathEchidnaTest.sol │ ├── SwapMathTest.sol │ ├── TickMathEchidnaTest.sol │ ├── ProtocolFeeControllerTest.sol │ ├── NoDelegateCallTest.sol │ ├── TickEchidnaTest.sol │ ├── TickBitmapTest.sol │ ├── TickMathTest.sol │ ├── TickBitmapEchidnaTest.sol │ ├── SwapMathEchidnaTest.sol │ ├── MockContract.sol │ ├── FullMathEchidnaTest.sol │ ├── TestERC20.sol │ ├── TickTest.sol │ ├── TestInvalidERC20.sol │ ├── PoolManagerReentrancyTest.sol │ ├── HooksTest.sol │ ├── PoolDonateTest.sol │ ├── EmptyTestHooks.sol │ ├── SqrtPriceMathTest.sol │ ├── PoolModifyPositionTest.sol │ ├── PoolTakeTest.sol │ ├── TickOverflowSafetyEchidnaTest.sol │ ├── MockHooks.sol │ └── PoolSwapTest.sol ├── Owned.sol ├── NoDelegateCall.sol └── types │ └── BalanceDelta.sol ├── foundry.toml ├── tsconfig.json ├── justfile ├── hardhat.config.ts ├── latex └── main.bib ├── package.json ├── echidna.config.yml ├── README.md ├── LICENSE └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 -------------------------------------------------------------------------------- /.forge-snapshots/mint.snap: -------------------------------------------------------------------------------- 1 | 313109 -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /.forge-snapshots/initialize.snap: -------------------------------------------------------------------------------- 1 | 37613 -------------------------------------------------------------------------------- /.forge-snapshots/simple swap.snap: -------------------------------------------------------------------------------- 1 | 67737 -------------------------------------------------------------------------------- /.forge-snapshots/poolExtsloadSlot0.snap: -------------------------------------------------------------------------------- 1 | 1151 -------------------------------------------------------------------------------- /.forge-snapshots/swap with hooks.snap: -------------------------------------------------------------------------------- 1 | 67712 -------------------------------------------------------------------------------- /.forge-snapshots/swap with native.snap: -------------------------------------------------------------------------------- 1 | 67737 -------------------------------------------------------------------------------- /.forge-snapshots/NoDelegateCallOverhead.snap: -------------------------------------------------------------------------------- 1 | 41 -------------------------------------------------------------------------------- /.forge-snapshots/mint with empty hook.snap: -------------------------------------------------------------------------------- 1 | 320432 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.forge-snapshots/HooksShouldCallBeforeSwap.snap: -------------------------------------------------------------------------------- 1 | 34 -------------------------------------------------------------------------------- /.forge-snapshots/donate gas with 1 token.snap: -------------------------------------------------------------------------------- 1 | 131234 -------------------------------------------------------------------------------- /.forge-snapshots/donate gas with 2 tokens.snap: -------------------------------------------------------------------------------- 1 | 185921 -------------------------------------------------------------------------------- /.forge-snapshots/gas overhead of no-op lock.snap: -------------------------------------------------------------------------------- 1 | 61036 -------------------------------------------------------------------------------- /.forge-snapshots/mint with native token.snap: -------------------------------------------------------------------------------- 1 | 294445 -------------------------------------------------------------------------------- /.forge-snapshots/poolExtsloadTickInfoStruct.snap: -------------------------------------------------------------------------------- 1 | 2785 -------------------------------------------------------------------------------- /.forge-snapshots/swap against liquidity.snap: -------------------------------------------------------------------------------- 1 | 146322 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | typechain/ 2 | lib/forge-std/ 3 | -------------------------------------------------------------------------------- /.forge-snapshots/BitMathLeastSignificantBitMaxUint128.snap: -------------------------------------------------------------------------------- 1 | 458 -------------------------------------------------------------------------------- /.forge-snapshots/BitMathLeastSignificantBitMaxUint256.snap: -------------------------------------------------------------------------------- 1 | 461 -------------------------------------------------------------------------------- /.forge-snapshots/BitMathLeastSignificantBitSmallNumber.snap: -------------------------------------------------------------------------------- 1 | 456 -------------------------------------------------------------------------------- /.forge-snapshots/BitMathMostSignificantBitMaxUint128.snap: -------------------------------------------------------------------------------- 1 | 394 -------------------------------------------------------------------------------- /.forge-snapshots/BitMathMostSignificantBitMaxUint256.snap: -------------------------------------------------------------------------------- 1 | 415 -------------------------------------------------------------------------------- /.forge-snapshots/BitMathMostSignificantBitSmallNumber.snap: -------------------------------------------------------------------------------- 1 | 322 -------------------------------------------------------------------------------- /.forge-snapshots/swap against liquidity with native token.snap: -------------------------------------------------------------------------------- 1 | 161615 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /whitepaper-v4-draft.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aarmia64/v4-corea/HEAD/whitepaper-v4-draft.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | crytic-export/ 4 | node_modules/ 5 | typechain/ 6 | foundry-out/ 7 | .vscode/ -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "rules": { 4 | "prettier/prettier": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | Which issue does this pull request resolve? 3 | 4 | ## Description of changes -------------------------------------------------------------------------------- /test/__snapshots__/PoolManager.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PoolManager bytecode size 1`] = `29219`; 4 | -------------------------------------------------------------------------------- /test/__snapshots__/NoDelegateCall.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NoDelegateCall runtime overhead 1`] = `41`; 4 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | forge-gas-snapshot/=lib/forge-gas-snapshot/src/ 4 | @openzeppelin/contracts/=node_modules/@openzeppelin/contracts 5 | -------------------------------------------------------------------------------- /test/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const MIN_TICK = -887272 2 | export const MAX_TICK = 887272 3 | export const MAX_TICK_SPACING = 32767 4 | export const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' 5 | -------------------------------------------------------------------------------- /test/__snapshots__/Hooks.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hooks #shouldCall gas cost of shouldCall 1`] = `7`; 4 | 5 | exports[`Hooks #validateHookAddress gas cost of validateHookAddress 1`] = `1443`; 6 | -------------------------------------------------------------------------------- /test/shared/expect.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import { solidity } from 'ethereum-waffle' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | 5 | use(solidity) 6 | use(jestSnapshotPlugin()) 7 | 8 | export { expect } 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/forge-gas-snapshot"] 5 | path = lib/forge-gas-snapshot 6 | url = https://github.com/marktoda/forge-gas-snapshot 7 | [submodule "lib/solmate"] 8 | path = lib/solmate 9 | url = https://github.com/transmissions11/solmate 10 | -------------------------------------------------------------------------------- /test/__snapshots__/Tick.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tick #tickSpacingToMaxLiquidityPerTick gas cost 60 tick spacing 1`] = `128`; 4 | 5 | exports[`Tick #tickSpacingToMaxLiquidityPerTick gas cost max tick spacing 1`] = `128`; 6 | 7 | exports[`Tick #tickSpacingToMaxLiquidityPerTick gas cost min tick spacing 1`] = `128`; 8 | -------------------------------------------------------------------------------- /contracts/libraries/FixedPoint128.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title FixedPoint128 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | library FixedPoint128 { 7 | uint256 internal constant Q128 = 0x100000000000000000000000000000000; 8 | } 9 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'contracts' 3 | out = 'foundry-out' 4 | solc_version = '0.8.19' 5 | optimizer_runs = 800 6 | ffi = true 7 | fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}, { access = "read", path = "./foundry-out"}] 8 | 9 | [profile.ci] 10 | fuzz_runs = 100000 11 | 12 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "typeRoots": ["./typechain", "./node_modules/@types"], 9 | "types": ["@nomiclabs/hardhat-ethers", "@nomiclabs/hardhat-waffle"] 10 | }, 11 | "include": ["./test"], 12 | "files": ["./hardhat.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /test/shared/format.ts: -------------------------------------------------------------------------------- 1 | import { Decimal } from 'decimal.js' 2 | import { BigNumberish } from 'ethers' 3 | 4 | export function formatTokenAmount(num: BigNumberish): string { 5 | return new Decimal(num.toString()).dividedBy(new Decimal(10).pow(18)).toPrecision(5) 6 | } 7 | 8 | export function formatPrice(price: BigNumberish): string { 9 | return new Decimal(price.toString()).dividedBy(new Decimal(2).pow(96)).pow(2).toPrecision(5) 10 | } 11 | -------------------------------------------------------------------------------- /contracts/libraries/FixedPoint96.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title FixedPoint96 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | /// @dev Used in SqrtPriceMath.sol 7 | library FixedPoint96 { 8 | uint8 internal constant RESOLUTION = 96; 9 | uint256 internal constant Q96 = 0x1000000000000000000000000; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/libraries/PoolId.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 5 | 6 | type PoolId is bytes32; 7 | 8 | /// @notice Library for computing the ID of a pool 9 | library PoolIdLibrary { 10 | function toId(IPoolManager.PoolKey memory poolKey) internal pure returns (PoolId) { 11 | return PoolId.wrap(keccak256(abi.encode(poolKey))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/interfaces/IDynamicFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {IPoolManager} from "./IPoolManager.sol"; 5 | 6 | /// @notice The dynamic fee manager determines fees for pools 7 | /// @dev note that this pool is only called if the PoolKey fee value is equal to the DYNAMIC_FEE magic value 8 | interface IDynamicFeeManager { 9 | function getFee(IPoolManager.PoolKey calldata key) external returns (uint24); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/test/FullMathTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {FullMath} from "../libraries/FullMath.sol"; 5 | 6 | contract FullMathTest { 7 | function mulDiv(uint256 x, uint256 y, uint256 z) external pure returns (uint256) { 8 | return FullMath.mulDiv(x, y, z); 9 | } 10 | 11 | function mulDivRoundingUp(uint256 x, uint256 y, uint256 z) external pure returns (uint256) { 12 | return FullMath.mulDivRoundingUp(x, y, z); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/callback/ILockCallback.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | interface ILockCallback { 5 | /// @notice Called by the pool manager on `msg.sender` when a lock is acquired 6 | /// @param id The id of the lock that was acquired 7 | /// @param data The data that was passed to the call to lock 8 | /// @return Any data that you want to be returned from the lock call 9 | function lockAcquired(uint256 id, bytes calldata data) external returns (bytes memory); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/test/UnsafeMathEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {UnsafeMath} from "../libraries/UnsafeMath.sol"; 5 | 6 | contract UnsafeMathEchidnaTest { 7 | function checkDivRoundingUp(uint256 x, uint256 d) external pure { 8 | require(d > 0); 9 | uint256 z = UnsafeMath.divRoundingUp(x, d); 10 | uint256 diff = z - (x / d); 11 | if (x % d == 0) { 12 | assert(diff == 0); 13 | } else { 14 | assert(diff == 1); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/__snapshots__/BitMath.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BitMath #leastSignificantBit gas cost of max uint128 1`] = `431`; 4 | 5 | exports[`BitMath #leastSignificantBit gas cost of max uint256 1`] = `431`; 6 | 7 | exports[`BitMath #leastSignificantBit gas cost of smaller number 1`] = `429`; 8 | 9 | exports[`BitMath #mostSignificantBit gas cost of max uint128 1`] = `368`; 10 | 11 | exports[`BitMath #mostSignificantBit gas cost of max uint256 1`] = `386`; 12 | 13 | exports[`BitMath #mostSignificantBit gas cost of smaller number 1`] = `296`; 14 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | test: test-forge test-hardhat 2 | prep: fix snapshots 3 | snapshots: snapshots-forge snapshots-hardhat 4 | 5 | test-forge: install-forge build-forge 6 | forge test 7 | 8 | test-hardhat: install-hardhat 9 | yarn test 10 | 11 | build-forge: install-forge 12 | forge build 13 | 14 | build-hardhat: install-hardhat 15 | yarn build 16 | 17 | snapshots-forge: install-forge 18 | forge snapshot 19 | 20 | snapshots-hardhat: install-hardhat 21 | yarn snapshots 22 | 23 | install-forge: 24 | forge install 25 | 26 | install-hardhat: 27 | yarn install 28 | 29 | fix: 30 | forge fmt 31 | -------------------------------------------------------------------------------- /contracts/interfaces/IProtocolFeeController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {IPoolManager} from "./IPoolManager.sol"; 5 | 6 | interface IProtocolFeeController { 7 | /// @notice Returns the protocol fees for a pool given the conditions of this contract 8 | /// @param poolKey The pool key to identify the pool. The controller may want to use attributes on the pool 9 | /// to determine the protocol fee, hence the entire key is needed. 10 | function protocolFeesForPool(IPoolManager.PoolKey memory poolKey) external view returns (uint8, uint8); 11 | } 12 | -------------------------------------------------------------------------------- /test/foundry-tests/utils/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {ERC20} from "solmate/tokens/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor(string memory name, string memory symbol, uint8 decimals) ERC20(name, symbol, decimals) {} 8 | 9 | function mint(address _to, uint256 _amount) public { 10 | _mint(_to, _amount); 11 | } 12 | 13 | function forceApprove(address _from, address _to, uint256 _amount) public returns (bool) { 14 | allowance[_from][_to] = _amount; 15 | 16 | emit Approval(_from, _to, _amount); 17 | 18 | return true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run-linters: 11 | name: Run linters 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16 22 | cache: 'yarn' 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Install Foundry 28 | uses: foundry-rs/foundry-toolchain@v1 29 | with: 30 | version: nightly 31 | 32 | - name: Compile 33 | run: yarn prettier-check 34 | -------------------------------------------------------------------------------- /contracts/libraries/UnsafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title Math functions that do not check inputs or outputs 5 | /// @notice Contains methods that perform common math functions but do not do any overflow or underflow checks 6 | library UnsafeMath { 7 | /// @notice Returns ceil(x / y) 8 | /// @dev division by 0 has unspecified behavior, and must be checked externally 9 | /// @param x The dividend 10 | /// @param y The divisor 11 | /// @return z The quotient, ceil(x / y) 12 | function divRoundingUp(uint256 x, uint256 y) internal pure returns (uint256 z) { 13 | unchecked { 14 | assembly { 15 | z := add(div(x, y), gt(mod(x, y), 0)) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/test/PoolLockTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 5 | import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; 6 | 7 | contract PoolLockTest is ILockCallback { 8 | event LockAcquired(uint256 id); 9 | 10 | IPoolManager manager; 11 | 12 | constructor(IPoolManager _manager) { 13 | manager = _manager; 14 | } 15 | 16 | function lock() external { 17 | manager.lock(""); 18 | } 19 | 20 | /// @notice Called by the pool manager on `msg.sender` when a lock is acquired 21 | function lockAcquired(uint256 id, bytes calldata) external override returns (bytes memory) { 22 | emit LockAcquired(id); 23 | return ""; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/libraries/Fees.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | library Fees { 5 | uint24 public constant STATIC_FEE_MASK = 0x0FFFFF; 6 | uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; // 1000 7 | uint24 public constant HOOK_SWAP_FEE_FLAG = 0x400000; // 0100 8 | uint24 public constant HOOK_WITHDRAW_FEE_FLAG = 0x200000; // 0010 9 | 10 | function isDynamicFee(uint24 self) internal pure returns (bool) { 11 | return self & DYNAMIC_FEE_FLAG != 0; 12 | } 13 | 14 | function hasHookSwapFee(uint24 self) internal pure returns (bool) { 15 | return self & HOOK_SWAP_FEE_FLAG != 0; 16 | } 17 | 18 | function hasHookWithdrawFee(uint24 self) internal pure returns (bool) { 19 | return self & HOOK_WITHDRAW_FEE_FLAG != 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/test/BitMathEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {BitMath} from "../libraries/BitMath.sol"; 5 | 6 | contract BitMathEchidnaTest { 7 | function mostSignificantBitInvariant(uint256 input) external pure { 8 | unchecked { 9 | uint8 msb = BitMath.mostSignificantBit(input); 10 | assert(input >= (uint256(2) ** msb)); 11 | assert(msb == 255 || input < uint256(2) ** (msb + 1)); 12 | } 13 | } 14 | 15 | function leastSignificantBitInvariant(uint256 input) external pure { 16 | unchecked { 17 | uint8 lsb = BitMath.leastSignificantBit(input); 18 | assert(input & (uint256(2) ** lsb) != 0); 19 | assert(input & (uint256(2) ** lsb - 1) == 0); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/__snapshots__/SwapMath.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SwapMath #computeSwapStep gas swap one for zero exact in capped 1`] = `2221`; 4 | 5 | exports[`SwapMath #computeSwapStep gas swap one for zero exact in partial 1`] = `3049`; 6 | 7 | exports[`SwapMath #computeSwapStep gas swap one for zero exact out capped 1`] = `1968`; 8 | 9 | exports[`SwapMath #computeSwapStep gas swap one for zero exact out partial 1`] = `3049`; 10 | 11 | exports[`SwapMath #computeSwapStep gas swap zero for one exact in capped 1`] = `2210`; 12 | 13 | exports[`SwapMath #computeSwapStep gas swap zero for one exact in partial 1`] = `3208`; 14 | 15 | exports[`SwapMath #computeSwapStep gas swap zero for one exact out capped 1`] = `1957`; 16 | 17 | exports[`SwapMath #computeSwapStep gas swap zero for one exact out partial 1`] = `3208`; 18 | -------------------------------------------------------------------------------- /test/__snapshots__/SqrtPriceMath.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SqrtPriceMath #getAmount0Delta gas cost for amount0 where roundUp = true 1`] = `603`; 4 | 5 | exports[`SqrtPriceMath #getAmount0Delta gas cost for amount0 where roundUp = true 2`] = `483`; 6 | 7 | exports[`SqrtPriceMath #getAmount1Delta gas cost for amount0 where roundUp = false 1`] = `483`; 8 | 9 | exports[`SqrtPriceMath #getAmount1Delta gas cost for amount0 where roundUp = true 1`] = `603`; 10 | 11 | exports[`SqrtPriceMath #getNextSqrtPriceFromInput zeroForOne = false gas 1`] = `567`; 12 | 13 | exports[`SqrtPriceMath #getNextSqrtPriceFromInput zeroForOne = true gas 1`] = `761`; 14 | 15 | exports[`SqrtPriceMath #getNextSqrtPriceFromOutput zeroForOne = false gas 1`] = `859`; 16 | 17 | exports[`SqrtPriceMath #getNextSqrtPriceFromOutput zeroForOne = true gas 1`] = `500`; 18 | -------------------------------------------------------------------------------- /contracts/Owned.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.19; 3 | 4 | contract Owned { 5 | address public owner; 6 | bytes12 private STORAGE_PLACEHOLDER; 7 | 8 | error InvalidCaller(); 9 | 10 | /// @notice Emitted when the owner of the factory is changed 11 | /// @param oldOwner The owner before the owner was changed 12 | /// @param newOwner The owner after the owner was changed 13 | event OwnerChanged(address indexed oldOwner, address indexed newOwner); 14 | 15 | modifier onlyOwner() { 16 | if (msg.sender != owner) revert InvalidCaller(); 17 | _; 18 | } 19 | 20 | constructor() { 21 | owner = msg.sender; 22 | emit OwnerChanged(address(0), msg.sender); 23 | } 24 | 25 | function setOwner(address _owner) external onlyOwner { 26 | emit OwnerChanged(owner, _owner); 27 | owner = _owner; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/shared/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { ethers } from 'hardhat' 3 | import { TestERC20 } from '../../typechain/TestERC20' 4 | 5 | interface TokensFixture { 6 | currency0: TestERC20 7 | currency1: TestERC20 8 | token2: TestERC20 9 | } 10 | 11 | export async function tokensFixture(): Promise { 12 | const tokenFactory = await ethers.getContractFactory('TestERC20') 13 | const tokenA = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 14 | const tokenB = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 15 | const tokenC = (await tokenFactory.deploy(BigNumber.from(2).pow(255))) as TestERC20 16 | 17 | const [currency0, currency1, token2] = [tokenA, tokenB, tokenC].sort((tokenA, tokenB) => 18 | tokenA.address.toLowerCase() < tokenB.address.toLowerCase() ? -1 : 1 19 | ) 20 | 21 | return { currency0, currency1, token2 } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/test/SwapMathTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {SwapMath} from "../libraries/SwapMath.sol"; 5 | 6 | contract SwapMathTest { 7 | function computeSwapStep( 8 | uint160 sqrtP, 9 | uint160 sqrtPTarget, 10 | uint128 liquidity, 11 | int256 amountRemaining, 12 | uint24 feePips 13 | ) external pure returns (uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) { 14 | return SwapMath.computeSwapStep(sqrtP, sqrtPTarget, liquidity, amountRemaining, feePips); 15 | } 16 | 17 | function getGasCostOfComputeSwapStep( 18 | uint160 sqrtP, 19 | uint160 sqrtPTarget, 20 | uint128 liquidity, 21 | int256 amountRemaining, 22 | uint24 feePips 23 | ) external view returns (uint256) { 24 | uint256 gasBefore = gasleft(); 25 | SwapMath.computeSwapStep(sqrtP, sqrtPTarget, liquidity, amountRemaining, feePips); 26 | return gasBefore - gasleft(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/interfaces/IHookFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {IPoolManager} from "./IPoolManager.sol"; 5 | 6 | /// @notice The interface for setting a fee on swap or fee on withdraw to the hook 7 | /// @dev This callback is only made if the Fee.HOOK_SWAP_FEE_FLAG or Fee.HOOK_WITHDRAW_FEE_FLAG in set in the pool's key.fee. 8 | interface IHookFeeManager { 9 | /// @notice Sets the fee a hook can take at swap. 10 | /// @param key The pool key 11 | /// @return The fee as an integer denominator for 1 to 0 swaps (upper bits set) or 0 to 1 swaps (lower bits set). 12 | function getHookSwapFee(IPoolManager.PoolKey calldata key) external view returns (uint8); 13 | 14 | /// @notice Sets the fee a hook can take at withdraw. 15 | /// @param key The pool key 16 | /// @return The fee as an integer denominator for amount1 (upper bits set) or amount0 (lower bits set). 17 | function getHookWithdrawFee(IPoolManager.PoolKey calldata key) external view returns (uint8); 18 | } 19 | -------------------------------------------------------------------------------- /contracts/test/TickMathEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {TickMath} from "../libraries/TickMath.sol"; 5 | 6 | contract TickMathEchidnaTest { 7 | // uniqueness and increasing order 8 | function checkGetSqrtRatioAtTickInvariants(int24 tick) external pure { 9 | uint160 ratio = TickMath.getSqrtRatioAtTick(tick); 10 | assert(TickMath.getSqrtRatioAtTick(tick - 1) < ratio && ratio < TickMath.getSqrtRatioAtTick(tick + 1)); 11 | assert(ratio >= TickMath.MIN_SQRT_RATIO); 12 | assert(ratio <= TickMath.MAX_SQRT_RATIO); 13 | } 14 | 15 | // the ratio is always between the returned tick and the returned tick+1 16 | function checkGetTickAtSqrtRatioInvariants(uint160 ratio) external pure { 17 | int24 tick = TickMath.getTickAtSqrtRatio(ratio); 18 | assert(ratio >= TickMath.getSqrtRatioAtTick(tick) && ratio < TickMath.getSqrtRatioAtTick(tick + 1)); 19 | assert(tick >= TickMath.MIN_TICK); 20 | assert(tick < TickMath.MAX_TICK); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/test/ProtocolFeeControllerTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IProtocolFeeController} from "../interfaces/IProtocolFeeController.sol"; 5 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 6 | import {PoolId, PoolIdLibrary} from "../libraries/PoolId.sol"; 7 | 8 | contract ProtocolFeeControllerTest is IProtocolFeeController { 9 | using PoolIdLibrary for IPoolManager.PoolKey; 10 | 11 | mapping(PoolId => uint8) public swapFeeForPool; 12 | mapping(PoolId => uint8) public withdrawFeeForPool; 13 | 14 | function protocolFeesForPool(IPoolManager.PoolKey memory key) external view returns (uint8, uint8) { 15 | return (swapFeeForPool[key.toId()], withdrawFeeForPool[key.toId()]); 16 | } 17 | 18 | // for tests to set pool protocol fees 19 | function setSwapFeeForPool(PoolId id, uint8 fee) external { 20 | swapFeeForPool[id] = fee; 21 | } 22 | 23 | function setWithdrawFeeForPool(PoolId id, uint8 fee) external { 24 | withdrawFeeForPool[id] = fee; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/foundry-tests/utils/TokenFixture.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {MockERC20} from "./MockERC20.sol"; 5 | import {Currency} from "../../../contracts/libraries/CurrencyLibrary.sol"; 6 | 7 | contract TokenFixture { 8 | Currency internal currency1; 9 | Currency internal currency0; 10 | 11 | function initializeTokens() internal { 12 | MockERC20 tokenA = new MockERC20("TestA", "A", 18); 13 | MockERC20 tokenB = new MockERC20("TestB", "B", 18); 14 | 15 | (currency0, currency1) = sortTokens(tokenA, tokenB); 16 | } 17 | 18 | function sortTokens(MockERC20 tokenA, MockERC20 tokenB) 19 | private 20 | pure 21 | returns (Currency _currency0, Currency _currency1) 22 | { 23 | if (address(tokenA) < address(tokenB)) { 24 | (_currency0, _currency1) = (Currency.wrap(address(tokenA)), Currency.wrap(address(tokenB))); 25 | } else { 26 | (_currency0, _currency1) = (Currency.wrap(address(tokenB)), Currency.wrap(address(tokenA))); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/test/NoDelegateCallTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {NoDelegateCall} from "../NoDelegateCall.sol"; 5 | 6 | contract NoDelegateCallTest is NoDelegateCall { 7 | function canBeDelegateCalled() public view returns (uint256) { 8 | return block.timestamp / 5; 9 | } 10 | 11 | function cannotBeDelegateCalled() public view noDelegateCall returns (uint256) { 12 | return block.timestamp / 5; 13 | } 14 | 15 | function getGasCostOfCanBeDelegateCalled() external view returns (uint256) { 16 | uint256 gasBefore = gasleft(); 17 | canBeDelegateCalled(); 18 | return gasBefore - gasleft(); 19 | } 20 | 21 | function getGasCostOfCannotBeDelegateCalled() external view returns (uint256) { 22 | uint256 gasBefore = gasleft(); 23 | cannotBeDelegateCalled(); 24 | return gasBefore - gasleft(); 25 | } 26 | 27 | function callsIntoNoDelegateCallFunction() external view { 28 | noDelegateCallPrivate(); 29 | } 30 | 31 | function noDelegateCallPrivate() private view noDelegateCall {} 32 | } 33 | -------------------------------------------------------------------------------- /contracts/test/TickEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Pool} from "../libraries/Pool.sol"; 5 | import {TickMath} from "../libraries/TickMath.sol"; 6 | 7 | contract TickEchidnaTest { 8 | function checkTickSpacingToParametersInvariants(int24 tickSpacing) external pure { 9 | require(tickSpacing <= TickMath.MAX_TICK); 10 | require(tickSpacing > 0); 11 | 12 | int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing; 13 | int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing; 14 | 15 | uint128 maxLiquidityPerTick = Pool.tickSpacingToMaxLiquidityPerTick(tickSpacing); 16 | 17 | // symmetry around 0 tick 18 | assert(maxTick == -minTick); 19 | // positive max tick 20 | assert(maxTick > 0); 21 | // divisibility 22 | assert((maxTick - minTick) % tickSpacing == 0); 23 | 24 | uint256 numTicks = uint256(int256((maxTick - minTick) / tickSpacing)) + 1; 25 | // max liquidity at every tick is less than the cap 26 | assert(uint256(maxLiquidityPerTick) * numTicks <= type(uint128).max); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/NoDelegateCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title Prevents delegatecall to a contract 5 | /// @notice Base contract that provides a modifier for preventing delegatecall to methods in a child contract 6 | abstract contract NoDelegateCall { 7 | error DelegateCallNotAllowed(); 8 | 9 | /// @dev The original address of this contract 10 | address private immutable original; 11 | 12 | constructor() { 13 | // Immutables are computed in the init code of the contract, and then inlined into the deployed bytecode. 14 | // In other words, this variable won't change when it's checked at runtime. 15 | original = address(this); 16 | } 17 | 18 | /// @dev Private method is used instead of inlining into modifier because modifiers are copied into each method, 19 | /// and the use of immutable means the address bytes are copied in every place the modifier is used. 20 | function checkNotDelegateCall() private view { 21 | if (address(this) != original) revert DelegateCallNotAllowed(); 22 | } 23 | 24 | /// @notice Prevents delegatecall into the modified method 25 | modifier noDelegateCall() { 26 | checkNotDelegateCall(); 27 | _; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/shared/checkObservationEquals.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers' 2 | import { expect } from './expect' 3 | 4 | // helper function because we cannot do a simple deep equals with the 5 | // observation result object returned from ethers because it extends array 6 | export default function checkObservationEquals( 7 | { 8 | tickCumulative, 9 | blockTimestamp, 10 | initialized, 11 | secondsPerLiquidityCumulativeX128, 12 | }: { 13 | tickCumulative: BigNumber 14 | secondsPerLiquidityCumulativeX128: BigNumber 15 | initialized: boolean 16 | blockTimestamp: number 17 | }, 18 | expected: { 19 | tickCumulative: BigNumberish 20 | secondsPerLiquidityCumulativeX128: BigNumberish 21 | initialized: boolean 22 | blockTimestamp: number 23 | } 24 | ) { 25 | expect( 26 | { 27 | initialized, 28 | blockTimestamp, 29 | tickCumulative: tickCumulative.toString(), 30 | secondsPerLiquidityCumulativeX128: secondsPerLiquidityCumulativeX128.toString(), 31 | }, 32 | `observation is equivalent` 33 | ).to.deep.eq({ 34 | ...expected, 35 | tickCumulative: expected.tickCumulative.toString(), 36 | secondsPerLiquidityCumulativeX128: expected.secondsPerLiquidityCumulativeX128.toString(), 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /test/__snapshots__/TickBitmap.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TickBitmap #flipTick gas cost of flipping a tick that results in deleting a word 1`] = ` 4 | Object { 5 | "calldataByteLength": 36, 6 | "gasUsed": 22214, 7 | } 8 | `; 9 | 10 | exports[`TickBitmap #flipTick gas cost of flipping first tick in word to initialized 1`] = ` 11 | Object { 12 | "calldataByteLength": 36, 13 | "gasUsed": 44126, 14 | } 15 | `; 16 | 17 | exports[`TickBitmap #flipTick gas cost of flipping second tick in word to initialized 1`] = ` 18 | Object { 19 | "calldataByteLength": 36, 20 | "gasUsed": 27026, 21 | } 22 | `; 23 | 24 | exports[`TickBitmap #nextInitializedTickWithinOneWord lte = false gas cost for entire word 1`] = `2572`; 25 | 26 | exports[`TickBitmap #nextInitializedTickWithinOneWord lte = false gas cost just below boundary 1`] = `2572`; 27 | 28 | exports[`TickBitmap #nextInitializedTickWithinOneWord lte = false gas cost on boundary 1`] = `2572`; 29 | 30 | exports[`TickBitmap #nextInitializedTickWithinOneWord lte = true gas cost for entire word 1`] = `2570`; 31 | 32 | exports[`TickBitmap #nextInitializedTickWithinOneWord lte = true gas cost just below boundary 1`] = `2880`; 33 | 34 | exports[`TickBitmap #nextInitializedTickWithinOneWord lte = true gas cost on boundary 1`] = `2570`; 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report to help us improve the code 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please check that the bug is not already being tracked. 11 | - type: textarea 12 | attributes: 13 | label: Describe the bug 14 | description: Provide a clear and concise description of what the bug is and which contracts it affects. 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Expected Behavior 20 | description: Provide a clear and concise description of the desired fix. 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: To Reproduce 26 | description: If you have written tests to showcase the bug, what can we run to reproduce the issue? 27 | placeholder: "git checkout / forge test --match-test " 28 | - type: textarea 29 | attributes: 30 | label: Additional context 31 | description: If there is any additional context needed like a dependency or integrating contract that is affected please describe it below. 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/foundry-tests/Owned.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.19; 2 | 3 | import {Test} from "forge-std/Test.sol"; 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {Owned} from "../../contracts/Owned.sol"; 6 | 7 | contract OwnedTest is Test { 8 | Owned owned; 9 | 10 | function testConstructor(address owner) public { 11 | deployOwnedWithOwner(owner); 12 | 13 | assertEq(owner, owned.owner()); 14 | } 15 | 16 | function testSetOwnerFromOwner(address oldOwner, address nextOwner) public { 17 | // set the old owner as the owner 18 | deployOwnedWithOwner(oldOwner); 19 | 20 | // old owner passes over ownership 21 | vm.prank(oldOwner); 22 | owned.setOwner(nextOwner); 23 | assertEq(nextOwner, owned.owner()); 24 | } 25 | 26 | function testSetOwnerFromNonOwner(address oldOwner, address nextOwner) public { 27 | // set the old owner as the owner 28 | deployOwnedWithOwner(oldOwner); 29 | 30 | if (oldOwner != nextOwner) { 31 | vm.startPrank(nextOwner); 32 | vm.expectRevert(Owned.InvalidCaller.selector); 33 | owned.setOwner(nextOwner); 34 | vm.stopPrank(); 35 | } 36 | } 37 | 38 | function deployOwnedWithOwner(address owner) internal { 39 | vm.prank(owner); 40 | owned = new Owned(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | unit-tests: 11 | name: Hardhat Unit Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | submodules: recursive 18 | 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 16 22 | cache: 'yarn' 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | # This is required separately from yarn test because it generates the typechain definitions 28 | - name: Compile 29 | run: yarn compile 30 | 31 | - name: Run unit tests 32 | run: yarn test 33 | 34 | forge-tests: 35 | name: Forge Tests 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | with: 41 | submodules: recursive 42 | 43 | - uses: actions/setup-node@v3 44 | with: 45 | node-version: 16 46 | cache: 'yarn' 47 | 48 | - run: yarn 49 | 50 | - name: Install Foundry 51 | uses: foundry-rs/foundry-toolchain@v1 52 | with: 53 | version: nightly 54 | 55 | - name: Run tests 56 | run: forge test -vvv 57 | env: 58 | FOUNDRY_PROFILE: ci 59 | -------------------------------------------------------------------------------- /test/foundry-tests/SafeCast.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.19; 2 | 3 | import {Test} from "forge-std/Test.sol"; 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {SafeCast} from "../../contracts/libraries/SafeCast.sol"; 6 | 7 | contract SafeCastTest is Test { 8 | function testToUint160(uint256 x) public { 9 | if (x <= type(uint160).max) { 10 | assertEq(uint256(SafeCast.toUint160(x)), x); 11 | } else { 12 | vm.expectRevert(); 13 | SafeCast.toUint160(x); 14 | } 15 | } 16 | 17 | function testToInt128(int256 x) public { 18 | if (x <= type(int128).max && x >= type(int128).min) { 19 | assertEq(int256(SafeCast.toInt128(x)), x); 20 | } else { 21 | vm.expectRevert(); 22 | SafeCast.toInt128(x); 23 | } 24 | } 25 | 26 | function testToInt256(uint256 x) public { 27 | if (x <= uint256(type(int256).max)) { 28 | assertEq(uint256(SafeCast.toInt256(x)), x); 29 | } else { 30 | vm.expectRevert(); 31 | SafeCast.toInt256(x); 32 | } 33 | } 34 | 35 | function testToInt128(uint256 x) public { 36 | if (x <= uint128(type(int128).max)) { 37 | assertEq(uint128(SafeCast.toInt128(x)), x); 38 | } else { 39 | vm.expectRevert(); 40 | SafeCast.toInt128(x); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/types/BalanceDelta.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | type BalanceDelta is int256; 5 | 6 | using {add as +, sub as -} for BalanceDelta global; 7 | using BalanceDeltaLibrary for BalanceDelta global; 8 | 9 | function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) { 10 | /// @solidity memory-safe-assembly 11 | assembly { 12 | balanceDelta := 13 | or(shl(128, _amount0), and(0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff, _amount1)) 14 | } 15 | } 16 | 17 | function add(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta) { 18 | return toBalanceDelta(a.amount0() + b.amount0(), a.amount1() + b.amount1()); 19 | } 20 | 21 | function sub(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta) { 22 | return toBalanceDelta(a.amount0() - b.amount0(), a.amount1() - b.amount1()); 23 | } 24 | 25 | library BalanceDeltaLibrary { 26 | function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) { 27 | /// @solidity memory-safe-assembly 28 | assembly { 29 | _amount0 := shr(128, balanceDelta) 30 | } 31 | } 32 | 33 | function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) { 34 | /// @solidity memory-safe-assembly 35 | assembly { 36 | _amount1 := balanceDelta 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/test/TickBitmapTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {TickBitmap} from "../libraries/TickBitmap.sol"; 5 | 6 | contract TickBitmapTest { 7 | using TickBitmap for mapping(int16 => uint256); 8 | 9 | mapping(int16 => uint256) public bitmap; 10 | 11 | function flipTick(int24 tick) external { 12 | bitmap.flipTick(tick, 1); 13 | } 14 | 15 | function getGasCostOfFlipTick(int24 tick) external returns (uint256) { 16 | uint256 gasBefore = gasleft(); 17 | bitmap.flipTick(tick, 1); 18 | return gasBefore - gasleft(); 19 | } 20 | 21 | function nextInitializedTickWithinOneWord(int24 tick, bool lte) 22 | external 23 | view 24 | returns (int24 next, bool initialized) 25 | { 26 | return bitmap.nextInitializedTickWithinOneWord(tick, 1, lte); 27 | } 28 | 29 | function getGasCostOfNextInitializedTickWithinOneWord(int24 tick, bool lte) external view returns (uint256) { 30 | uint256 gasBefore = gasleft(); 31 | bitmap.nextInitializedTickWithinOneWord(tick, 1, lte); 32 | return gasBefore - gasleft(); 33 | } 34 | 35 | // returns whether the given tick is initialized 36 | function isInitialized(int24 tick) external view returns (bool) { 37 | (int24 next, bool initialized) = bitmap.nextInitializedTickWithinOneWord(tick, 1, true); 38 | return next == tick ? initialized : false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/test/TickMathTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {TickMath} from "../libraries/TickMath.sol"; 5 | 6 | contract TickMathTest { 7 | function getSqrtRatioAtTick(int24 tick) external pure returns (uint160) { 8 | return TickMath.getSqrtRatioAtTick(tick); 9 | } 10 | 11 | function getGasCostOfGetSqrtRatioAtTick(int24 tick) external view returns (uint256) { 12 | uint256 gasBefore = gasleft(); 13 | TickMath.getSqrtRatioAtTick(tick); 14 | return gasBefore - gasleft(); 15 | } 16 | 17 | function getTickAtSqrtRatio(uint160 sqrtPriceX96) external pure returns (int24) { 18 | return TickMath.getTickAtSqrtRatio(sqrtPriceX96); 19 | } 20 | 21 | function getGasCostOfGetTickAtSqrtRatio(uint160 sqrtPriceX96) external view returns (uint256) { 22 | uint256 gasBefore = gasleft(); 23 | TickMath.getTickAtSqrtRatio(sqrtPriceX96); 24 | return gasBefore - gasleft(); 25 | } 26 | 27 | function MIN_SQRT_RATIO() external pure returns (uint160) { 28 | return TickMath.MIN_SQRT_RATIO; 29 | } 30 | 31 | function MAX_SQRT_RATIO() external pure returns (uint160) { 32 | return TickMath.MAX_SQRT_RATIO; 33 | } 34 | 35 | function MIN_TICK() external pure returns (int24) { 36 | return TickMath.MIN_TICK; 37 | } 38 | 39 | function MAX_TICK() external pure returns (int24) { 40 | return TickMath.MAX_TICK; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/libraries/SafeCast.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title Safe casting methods 5 | /// @notice Contains methods for safely casting between types 6 | library SafeCast { 7 | /// @notice Cast a uint256 to a uint160, revert on overflow 8 | /// @param y The uint256 to be downcasted 9 | /// @return z The downcasted integer, now type uint160 10 | function toUint160(uint256 y) internal pure returns (uint160 z) { 11 | require((z = uint160(y)) == y); 12 | } 13 | 14 | /// @notice Cast a int256 to a int128, revert on overflow or underflow 15 | /// @param y The int256 to be downcasted 16 | /// @return z The downcasted integer, now type int128 17 | function toInt128(int256 y) internal pure returns (int128 z) { 18 | require((z = int128(y)) == y); 19 | } 20 | 21 | /// @notice Cast a uint256 to a int256, revert on overflow 22 | /// @param y The uint256 to be casted 23 | /// @return z The casted integer, now type int256 24 | function toInt256(uint256 y) internal pure returns (int256 z) { 25 | require(y <= uint256(type(int256).max)); 26 | z = int256(y); 27 | } 28 | 29 | /// @notice Cast a uint256 to a int128, revert on overflow 30 | /// @param y The uint256 to be downcasted 31 | /// @return z The downcasted integer, now type int128 32 | function toInt128(uint256 y) internal pure returns (int128 z) { 33 | require(y <= uint128(type(int128).max)); 34 | z = int128(int256(y)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import 'hardhat-typechain' 2 | import '@nomiclabs/hardhat-ethers' 3 | import '@nomiclabs/hardhat-waffle' 4 | import '@nomiclabs/hardhat-etherscan' 5 | 6 | const importToml = require('import-toml') 7 | const foundryConfig = importToml.sync('foundry.toml') 8 | 9 | export default { 10 | networks: { 11 | hardhat: { 12 | allowUnlimitedContractSize: true, 13 | }, 14 | mainnet: { 15 | url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, 16 | }, 17 | ropsten: { 18 | url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`, 19 | }, 20 | rinkeby: { 21 | url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, 22 | }, 23 | goerli: { 24 | url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`, 25 | }, 26 | kovan: { 27 | url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`, 28 | }, 29 | }, 30 | etherscan: { 31 | // Your API key for Etherscan 32 | // Obtain one at https://etherscan.io/ 33 | apiKey: process.env.ETHERSCAN_API_KEY, 34 | }, 35 | solidity: { 36 | version: foundryConfig.profile.default.solc_version, 37 | settings: { 38 | optimizer: { 39 | enabled: true, 40 | runs: foundryConfig.profile.default.optimizer_runs, 41 | }, 42 | metadata: { 43 | // do not include the metadata hash, since this is machine dependent 44 | // and we want all generated code to be deterministic 45 | // https://docs.soliditylang.org/en/v0.8.19/metadata.html 46 | bytecodeHash: 'none', 47 | }, 48 | }, 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /latex/main.bib: -------------------------------------------------------------------------------- 1 | @online{Adams18, 2 | author = "Hayden Adams", 3 | year = "2018", 4 | month = nov, 5 | title = "Uniswap v1 Core", 6 | url = "https://hackmd.io/@HaydenAdams/HJ9jLsfTz", 7 | lastaccessed = "Jun 12, 2023", 8 | } 9 | 10 | @online{Adams20, 11 | author = "Hayden Adams and Noah Zinsmeister and Dan Robinson", 12 | year = "2020", 13 | month = mar, 14 | title = "Uniswap v2 Core", 15 | url = "https://uniswap.org/whitepaper.pdf", 16 | lastaccessed = "Jun 12, 2023", 17 | } 18 | 19 | @online{Adams21, 20 | author = "Hayden Adams and Noah Zinsmeister and Moody Salem and River Keefer and Dan Robinson", 21 | year = "2021", 22 | month = mar, 23 | title = "Uniswap v3 Core", 24 | url = "https://uniswap.org/whitepaper-v3.pdf", 25 | lastaccessed = "Jun 12, 2023", 26 | } 27 | 28 | @online{White2021, 29 | author = "Dave White and Dan Robinson and Hayden Adams", 30 | year = "2021", 31 | month = jul, 32 | title = "TWAMM", 33 | url = "https://www.paradigm.xyz/2021/07/twamm", 34 | lastaccessed = "Jun 12, 2023", 35 | } 36 | 37 | @online{Akhunov2018, 38 | author = "Alexey Akhunov and Moody Salem", 39 | year = "2018", 40 | month = jun, 41 | title = "EIP-1153: Transient storage opcodes", 42 | url = "https://eips.ethereum.org/EIPS/eip-1153", 43 | lastaccessed = "Jun 12, 2023", 44 | } 45 | 46 | @online{Buterin2021, 47 | author = "Vitalik Buterin and Martin Swende", 48 | year = "2021", 49 | month = apr, 50 | title = "EIP-3529: Reduction in refunds", 51 | url = "https://eips.ethereum.org/EIPS/eip-3529", 52 | lastaccessed = "Jun 12, 2023", 53 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_IMPROVEMENT.yml: -------------------------------------------------------------------------------- 1 | name: Feature Improvement 2 | description: Suggest an improvement to v4-core. 3 | labels: ["triage"] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please ensure that the feature has not already been requested. 10 | - type: dropdown 11 | attributes: 12 | label: Component 13 | description: Which area of code does your idea improve? 14 | multiple: true 15 | options: 16 | - Hooks 17 | - Singleton 18 | - Lock and Call 19 | - Delta accounting 20 | - 1155 Balances 21 | - Pool Actions (swap, modifyPosition, donate, take, settle, mint) 22 | - Gas Optimization 23 | - General design optimization (improving efficiency, cleanliness, or developer experience) 24 | - Documentation 25 | - type: textarea 26 | attributes: 27 | label: Describe the suggested feature and problem it solves. 28 | description: Provide a clear and concise description of what feature you would like to see, and what problems it solves. 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: Describe the desired implementation. 34 | description: If possible, provide a suggested architecture change or implementation. 35 | - type: textarea 36 | attributes: 37 | label: Describe alternatives. 38 | description: If possible, describe the alternatives you've considered, or describe the current functionality and how it may be sub-optimal. 39 | - type: textarea 40 | attributes: 41 | label: Additional context. 42 | description: Please list any additional dependencies or integrating contacts that are affected. 43 | -------------------------------------------------------------------------------- /test/shared/mockContract.ts: -------------------------------------------------------------------------------- 1 | import { FormatTypes, Interface } from 'ethers/lib/utils' 2 | import hre, { ethers } from 'hardhat' 3 | import { MockContract } from '../../typechain' 4 | 5 | export interface MockedContract { 6 | address: string 7 | called: (fn: string) => Promise 8 | calledOnce: (fn: string) => Promise 9 | calledWith: (fn: string, params: any[]) => Promise 10 | } 11 | 12 | export const deployMockContract = async ( 13 | contractInterface: Interface, 14 | address: string, 15 | implAddress?: string 16 | ): Promise => { 17 | await setCode(address, 'MockContract') 18 | 19 | const contractMock = (await ethers.getContractFactory('MockContract')).attach(address) as MockContract 20 | 21 | if (implAddress) { 22 | contractMock.setImplementation(implAddress) 23 | } 24 | 25 | return { 26 | address, 27 | called: async (fn: string): Promise => { 28 | return (await contractMock.timesCalled(contractInterface.getFunction(fn).format(FormatTypes.sighash))).gt(0) 29 | }, 30 | calledOnce: async (fn: string): Promise => { 31 | return (await contractMock.timesCalled(contractInterface.getFunction(fn).format(FormatTypes.sighash))).eq(1) 32 | }, 33 | calledWith: async (fn: string, params: any[]): Promise => { 34 | // Drop fn selector, keep 0x prefix so ethers interprets as byte string 35 | const paramsBytes = '0x' + contractInterface.encodeFunctionData(fn, params).slice(10) 36 | return contractMock.calledWith(contractInterface.getFunction(fn).format(FormatTypes.sighash), paramsBytes) 37 | }, 38 | } 39 | } 40 | 41 | export const setCode = async (address: string, artifactName: string) => { 42 | await hre.network.provider.send('hardhat_setCode', [ 43 | address, 44 | (await hre.artifacts.readArtifact(artifactName)).deployedBytecode, 45 | ]) 46 | } 47 | -------------------------------------------------------------------------------- /contracts/test/TickBitmapEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {TickBitmap} from "../libraries/TickBitmap.sol"; 5 | 6 | contract TickBitmapEchidnaTest { 7 | using TickBitmap for mapping(int16 => uint256); 8 | 9 | mapping(int16 => uint256) private bitmap; 10 | 11 | // returns whether the given tick is initialized 12 | function isInitialized(int24 tick) private view returns (bool) { 13 | (int24 next, bool initialized) = bitmap.nextInitializedTickWithinOneWord(tick, 1, true); 14 | return next == tick ? initialized : false; 15 | } 16 | 17 | function flipTick(int24 tick) external { 18 | bool before = isInitialized(tick); 19 | bitmap.flipTick(tick, 1); 20 | assert(isInitialized(tick) == !before); 21 | } 22 | 23 | function checkNextInitializedTickWithinOneWordInvariants(int24 tick, bool lte) external view { 24 | (int24 next, bool initialized) = bitmap.nextInitializedTickWithinOneWord(tick, 1, lte); 25 | if (lte) { 26 | // type(int24).min + 256 27 | require(tick >= -8388352); 28 | assert(next <= tick); 29 | assert(tick - next < 256); 30 | // all the ticks between the input tick and the next tick should be uninitialized 31 | for (int24 i = tick; i > next; i--) { 32 | assert(!isInitialized(i)); 33 | } 34 | assert(isInitialized(next) == initialized); 35 | } else { 36 | // type(int24).max - 256 37 | require(tick < 8388351); 38 | assert(next > tick); 39 | assert(next - tick <= 256); 40 | // all the ticks between the input tick and the next tick should be uninitialized 41 | for (int24 i = tick + 1; i < next; i++) { 42 | assert(!isInitialized(i)); 43 | } 44 | assert(isInitialized(next) == initialized); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/v4-core", 3 | "description": "🦄 Uniswap Protocol smart contracts", 4 | "license": "BUSL-1.1", 5 | "publishConfig": { 6 | "access": "restricted" 7 | }, 8 | "version": "1.0.0", 9 | "homepage": "https://uniswap.org", 10 | "keywords": [ 11 | "uniswap", 12 | "core", 13 | "v4" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Uniswap/v4-core" 18 | }, 19 | "files": [ 20 | "contracts/interfaces", 21 | "contracts/libraries", 22 | "artifacts/contracts/PoolManager.sol/PoolManager.json", 23 | "artifacts/contracts/interfaces/**/*.json", 24 | "!artifacts/contracts/interfaces/**/*.dbg.json" 25 | ], 26 | "engines": { 27 | "node": ">=10" 28 | }, 29 | "dependencies": { 30 | "@openzeppelin/contracts": "4.4.2" 31 | }, 32 | "devDependencies": { 33 | "@nomiclabs/hardhat-ethers": "^2.0.2", 34 | "@nomiclabs/hardhat-etherscan": "^2.1.1", 35 | "@nomiclabs/hardhat-waffle": "^2.0.1", 36 | "@typechain/ethers-v5": "^4.0.0", 37 | "@types/chai": "^4.2.6", 38 | "@types/mocha": "^5.2.7", 39 | "@uniswap/snapshot-gas-cost": "^1.0.0", 40 | "chai": "^4.2.0", 41 | "decimal.js": "^10.2.1", 42 | "ethereum-waffle": "^3.0.2", 43 | "ethers": "^5.0.8", 44 | "hardhat": "^2.9.6", 45 | "hardhat-typechain": "^0.3.5", 46 | "import-toml": "1.0.0", 47 | "mocha": "^6.2.2", 48 | "mocha-chai-jest-snapshot": "^1.1.0", 49 | "prettier": "2.8.6", 50 | "ts-generator": "^0.1.1", 51 | "ts-node": "^8.5.4", 52 | "typechain": "^4.0.0", 53 | "typescript": "^3.7.3" 54 | }, 55 | "scripts": { 56 | "compile": "hardhat compile", 57 | "test": "hardhat test", 58 | "snapshots": "UPDATE_SNAPSHOT=1 hardhat test", 59 | "prettier": "forge fmt contracts/**/*.sol && forge fmt test/**/*.sol && prettier --write 'test/**/*.ts'", 60 | "prettier-check": "forge fmt --check && prettier --check 'test/**/*.ts'" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/test/SwapMathEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {SwapMath} from "../libraries/SwapMath.sol"; 5 | 6 | contract SwapMathEchidnaTest { 7 | function checkComputeSwapStepInvariants( 8 | uint160 sqrtPriceRaw, 9 | uint160 sqrtPriceTargetRaw, 10 | uint128 liquidity, 11 | int256 amountRemaining, 12 | uint24 feePips 13 | ) external pure { 14 | require(sqrtPriceRaw > 0); 15 | require(sqrtPriceTargetRaw > 0); 16 | require(feePips > 0); 17 | require(feePips < 1e6); 18 | 19 | (uint160 sqrtQ, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = 20 | SwapMath.computeSwapStep(sqrtPriceRaw, sqrtPriceTargetRaw, liquidity, amountRemaining, feePips); 21 | 22 | assert(amountIn <= type(uint256).max - feeAmount); 23 | 24 | if (amountRemaining < 0) { 25 | assert(amountOut <= uint256(-amountRemaining)); 26 | } else { 27 | assert(amountIn + feeAmount <= uint256(amountRemaining)); 28 | } 29 | 30 | if (sqrtPriceRaw == sqrtPriceTargetRaw) { 31 | assert(amountIn == 0); 32 | assert(amountOut == 0); 33 | assert(feeAmount == 0); 34 | assert(sqrtQ == sqrtPriceTargetRaw); 35 | } 36 | 37 | // didn't reach price target, entire amount must be consumed 38 | if (sqrtQ != sqrtPriceTargetRaw) { 39 | if (amountRemaining < 0) assert(amountOut == uint256(-amountRemaining)); 40 | else assert(amountIn + feeAmount == uint256(amountRemaining)); 41 | } 42 | 43 | // next price is between price and price target 44 | if (sqrtPriceTargetRaw <= sqrtPriceRaw) { 45 | assert(sqrtQ <= sqrtPriceRaw); 46 | assert(sqrtQ >= sqrtPriceTargetRaw); 47 | } else { 48 | assert(sqrtQ >= sqrtPriceRaw); 49 | assert(sqrtQ <= sqrtPriceTargetRaw); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/foundry-tests/NoDelegateCall.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {NoDelegateCallTest} from "../../contracts/test/NoDelegateCallTest.sol"; 7 | 8 | contract TestDelegateCall is Test, GasSnapshot { 9 | error DelegateCallNotAllowed(); 10 | 11 | NoDelegateCallTest noDelegateCallTest; 12 | 13 | function setUp() public { 14 | noDelegateCallTest = new NoDelegateCallTest(); 15 | } 16 | 17 | function testGasOverhead() public { 18 | snap( 19 | "NoDelegateCallOverhead", 20 | noDelegateCallTest.getGasCostOfCannotBeDelegateCalled() 21 | - noDelegateCallTest.getGasCostOfCanBeDelegateCalled() 22 | ); 23 | } 24 | 25 | function testDelegateCallNoModifier() public { 26 | (bool success,) = 27 | address(noDelegateCallTest).delegatecall(abi.encode(noDelegateCallTest.canBeDelegateCalled.selector)); 28 | assertTrue(success); 29 | } 30 | 31 | function testDelegateCallWithModifier() public { 32 | vm.expectRevert(DelegateCallNotAllowed.selector); 33 | (bool success,) = 34 | address(noDelegateCallTest).delegatecall(abi.encode(noDelegateCallTest.cannotBeDelegateCalled.selector)); 35 | // note vm.expectRevert inverts success, so a true result here means it reverted 36 | assertTrue(success); 37 | } 38 | 39 | function testCanCallIntoPrivateMethodWithModifier() public view { 40 | noDelegateCallTest.callsIntoNoDelegateCallFunction(); 41 | } 42 | 43 | function testCannotDelegateCallPrivateMethodWithModifier() public { 44 | vm.expectRevert(DelegateCallNotAllowed.selector); 45 | (bool success,) = address(noDelegateCallTest).delegatecall( 46 | abi.encode(noDelegateCallTest.callsIntoNoDelegateCallFunction.selector) 47 | ); 48 | // note vm.expectRevert inverts success, so a true result here means it reverted 49 | assertTrue(success); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /contracts/test/MockContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; 5 | 6 | /// @notice Mock contract that tracks the number of calls to various functions by selector 7 | /// @dev allows for proxying to an implementation contract 8 | /// if real logic or return values are needed 9 | contract MockContract is Proxy { 10 | mapping(bytes32 => uint256) public calls; 11 | mapping(bytes32 => mapping(bytes => uint256)) public callParams; 12 | 13 | /// @notice If set, delegatecall to implementation after tracking call 14 | address internal impl; 15 | 16 | function timesCalledSelector(bytes32 selector) public view returns (uint256) { 17 | return calls[selector]; 18 | } 19 | 20 | function timesCalled(string calldata fnSig) public view returns (uint256) { 21 | bytes32 selector = bytes32(uint256(keccak256(bytes(fnSig))) & (type(uint256).max << 224)); 22 | return calls[selector]; 23 | } 24 | 25 | function calledWithSelector(bytes32 selector, bytes calldata params) public view returns (bool) { 26 | return callParams[selector][params[1:]] > 0; // Drop 0x byte string prefix 27 | } 28 | 29 | function calledWith(string calldata fnSig, bytes calldata params) public view returns (bool) { 30 | bytes32 selector = bytes32(uint256(keccak256(bytes(fnSig))) & (type(uint256).max << 224)); 31 | return callParams[selector][params[1:]] > 0; // Drop 0x byte string prefix 32 | } 33 | 34 | /// @notice exposes implementation contract address 35 | function _implementation() internal view override returns (address) { 36 | return impl; 37 | } 38 | 39 | function setImplementation(address _impl) external { 40 | impl = _impl; 41 | } 42 | 43 | /// @notice Captures calls by selector 44 | function _beforeFallback() internal override { 45 | bytes32 selector = bytes32(msg.data[:5]); 46 | bytes memory params = msg.data[5:]; 47 | calls[selector]++; 48 | callParams[selector][params]++; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/test/FullMathEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {FullMath} from "../libraries/FullMath.sol"; 5 | 6 | contract FullMathEchidnaTest { 7 | function checkMulDivRounding(uint256 x, uint256 y, uint256 d) external pure { 8 | unchecked { 9 | require(d > 0); 10 | 11 | uint256 ceiled = FullMath.mulDivRoundingUp(x, y, d); 12 | uint256 floored = FullMath.mulDiv(x, y, d); 13 | 14 | if (mulmod(x, y, d) > 0) { 15 | assert(ceiled - floored == 1); 16 | } else { 17 | assert(ceiled == floored); 18 | } 19 | } 20 | } 21 | 22 | function checkMulDiv(uint256 x, uint256 y, uint256 d) external pure { 23 | unchecked { 24 | require(d > 0); 25 | uint256 z = FullMath.mulDiv(x, y, d); 26 | if (x == 0 || y == 0) { 27 | assert(z == 0); 28 | return; 29 | } 30 | 31 | // recompute x and y via mulDiv of the result of floor(x*y/d), should always be less than original inputs by < d 32 | uint256 x2 = FullMath.mulDiv(z, d, y); 33 | uint256 y2 = FullMath.mulDiv(z, d, x); 34 | assert(x2 <= x); 35 | assert(y2 <= y); 36 | 37 | assert(x - x2 < d); 38 | assert(y - y2 < d); 39 | } 40 | } 41 | 42 | function checkMulDivRoundingUp(uint256 x, uint256 y, uint256 d) external pure { 43 | unchecked { 44 | require(d > 0); 45 | uint256 z = FullMath.mulDivRoundingUp(x, y, d); 46 | if (x == 0 || y == 0) { 47 | assert(z == 0); 48 | return; 49 | } 50 | 51 | // recompute x and y via mulDiv of the result of floor(x*y/d), should always be less than original inputs by < d 52 | uint256 x2 = FullMath.mulDiv(z, d, y); 53 | uint256 y2 = FullMath.mulDiv(z, d, x); 54 | assert(x2 >= x); 55 | assert(y2 >= y); 56 | 57 | assert(x2 - x < d); 58 | assert(y2 - y < d); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/test/TestERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 5 | 6 | contract TestERC20 is IERC20Minimal { 7 | mapping(address => uint256) public override balanceOf; 8 | mapping(address => mapping(address => uint256)) public override allowance; 9 | 10 | constructor(uint256 amountToMint) { 11 | mint(msg.sender, amountToMint); 12 | } 13 | 14 | function mint(address to, uint256 amount) public { 15 | uint256 balanceNext = balanceOf[to] + amount; 16 | require(balanceNext >= amount, "overflow balance"); 17 | balanceOf[to] = balanceNext; 18 | } 19 | 20 | function transfer(address recipient, uint256 amount) external override returns (bool) { 21 | uint256 balanceBefore = balanceOf[msg.sender]; 22 | require(balanceBefore >= amount, "insufficient balance"); 23 | balanceOf[msg.sender] = balanceBefore - amount; 24 | 25 | uint256 balanceRecipient = balanceOf[recipient]; 26 | require(balanceRecipient + amount >= balanceRecipient, "recipient balance overflow"); 27 | balanceOf[recipient] = balanceRecipient + amount; 28 | 29 | emit Transfer(msg.sender, recipient, amount); 30 | return true; 31 | } 32 | 33 | function approve(address spender, uint256 amount) external override returns (bool) { 34 | allowance[msg.sender][spender] = amount; 35 | emit Approval(msg.sender, spender, amount); 36 | return true; 37 | } 38 | 39 | function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) { 40 | uint256 allowanceBefore = allowance[sender][msg.sender]; 41 | require(allowanceBefore >= amount, "allowance insufficient"); 42 | 43 | allowance[sender][msg.sender] = allowanceBefore - amount; 44 | 45 | uint256 balanceRecipient = balanceOf[recipient]; 46 | require(balanceRecipient + amount >= balanceRecipient, "overflow balance recipient"); 47 | balanceOf[recipient] = balanceRecipient + amount; 48 | uint256 balanceSender = balanceOf[sender]; 49 | require(balanceSender >= amount, "underflow balance sender"); 50 | balanceOf[sender] = balanceSender - amount; 51 | 52 | emit Transfer(sender, recipient, amount); 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/mythx.yml: -------------------------------------------------------------------------------- 1 | name: Mythx 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | mythx: 8 | name: Submit to Mythx 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 16 18 | 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.8 23 | 24 | - name: Install node dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Install pip3 28 | run: | 29 | python -m pip install --upgrade pip 30 | 31 | - name: Install mythx CLI 32 | run: | 33 | pip3 install mythx-cli 34 | 35 | - name: Install solc-select 36 | run: | 37 | pip3 install solc-select 38 | 39 | - name: Install solc 0.8.19 40 | run: | 41 | solc-select install 0.8.19 42 | solc-select use 0.8.19 43 | 44 | - name: Submit code to Mythx 45 | run: | 46 | mythx --api-key ${{ secrets.MYTHX_API_KEY }} \ 47 | --yes \ 48 | analyze \ 49 | --mode deep \ 50 | --async \ 51 | --create-group \ 52 | --group-name "@uniswap/core-next@${{ github.sha }}" \ 53 | --solc-version 0.8.19 \ 54 | --check-properties \ 55 | --remap-import "@openzeppelin/contracts/=$(pwd)/node_modules/@openzeppelin/contracts/" \ 56 | contracts/test/TickBitmapEchidnaTest.sol --include TickBitmapEchidnaTest \ 57 | contracts/test/TickMathEchidnaTest.sol --include TickMathEchidnaTest \ 58 | contracts/test/SqrtPriceMathEchidnaTest.sol --include SqrtPriceMathEchidnaTest \ 59 | contracts/test/SwapMathEchidnaTest.sol --include SwapMathEchidnaTest \ 60 | contracts/test/TickEchidnaTest.sol --include TickEchidnaTest \ 61 | contracts/test/TickOverflowSafetyEchidnaTest.sol --include TickOverflowSafetyEchidnaTest \ 62 | contracts/test/OracleEchidnaTest.sol --include OracleEchidnaTest \ 63 | contracts/test/BitMathEchidnaTest.sol --include BitMathEchidnaTest \ 64 | contracts/test/UnsafeMathEchidnaTest.sol --include UnsafeMathEchidnaTest \ 65 | contracts/test/FullMathEchidnaTest.sol --include FullMathEchidnaTest 66 | -------------------------------------------------------------------------------- /contracts/test/TickTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Pool} from "../libraries/Pool.sol"; 5 | 6 | contract TickTest { 7 | using Pool for Pool.State; 8 | 9 | Pool.State public pool; 10 | 11 | function ticks(int24 tick) external view returns (Pool.TickInfo memory) { 12 | return pool.ticks[tick]; 13 | } 14 | 15 | function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) external pure returns (uint128) { 16 | return Pool.tickSpacingToMaxLiquidityPerTick(tickSpacing); 17 | } 18 | 19 | function getGasCostOfTickSpacingToMaxLiquidityPerTick(int24 tickSpacing) external view returns (uint256) { 20 | uint256 gasBefore = gasleft(); 21 | Pool.tickSpacingToMaxLiquidityPerTick(tickSpacing); 22 | return gasBefore - gasleft(); 23 | } 24 | 25 | function setTick(int24 tick, Pool.TickInfo memory info) external { 26 | pool.ticks[tick] = info; 27 | } 28 | 29 | function getFeeGrowthInside( 30 | int24 tickLower, 31 | int24 tickUpper, 32 | int24 tickCurrent, 33 | uint256 feeGrowthGlobal0X128, 34 | uint256 feeGrowthGlobal1X128 35 | ) external returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) { 36 | pool.slot0.tick = tickCurrent; 37 | pool.feeGrowthGlobal0X128 = feeGrowthGlobal0X128; 38 | pool.feeGrowthGlobal1X128 = feeGrowthGlobal1X128; 39 | return pool.getFeeGrowthInside(tickLower, tickUpper); 40 | } 41 | 42 | function update( 43 | int24 tick, 44 | int24 tickCurrent, 45 | int128 liquidityDelta, 46 | uint256 feeGrowthGlobal0X128, 47 | uint256 feeGrowthGlobal1X128, 48 | bool upper 49 | ) external returns (bool flipped, uint128 liquidityGrossAfter) { 50 | pool.slot0.tick = tickCurrent; 51 | pool.feeGrowthGlobal0X128 = feeGrowthGlobal0X128; 52 | pool.feeGrowthGlobal1X128 = feeGrowthGlobal1X128; 53 | return pool.updateTick(tick, liquidityDelta, upper); 54 | } 55 | 56 | function clear(int24 tick) external { 57 | pool.clearTick(tick); 58 | } 59 | 60 | function cross(int24 tick, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) 61 | external 62 | returns (int128 liquidityNet) 63 | { 64 | return pool.crossTick(tick, feeGrowthGlobal0X128, feeGrowthGlobal1X128); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/test/TestInvalidERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 5 | 6 | // Regular ERC20 but it doesn't return true on transfer. 7 | contract TestInvalidERC20 is IERC20Minimal { 8 | mapping(address => uint256) public override balanceOf; 9 | mapping(address => mapping(address => uint256)) public override allowance; 10 | 11 | constructor(uint256 amountToMint) { 12 | mint(msg.sender, amountToMint); 13 | } 14 | 15 | function mint(address to, uint256 amount) public { 16 | uint256 balanceNext = balanceOf[to] + amount; 17 | require(balanceNext >= amount, "overflow balance"); 18 | balanceOf[to] = balanceNext; 19 | } 20 | 21 | function transfer(address recipient, uint256 amount) external override returns (bool) { 22 | uint256 balanceBefore = balanceOf[msg.sender]; 23 | require(balanceBefore >= amount, "insufficient balance"); 24 | balanceOf[msg.sender] = balanceBefore - amount; 25 | 26 | uint256 balanceRecipient = balanceOf[recipient]; 27 | require(balanceRecipient + amount >= balanceRecipient, "recipient balance overflow"); 28 | balanceOf[recipient] = balanceRecipient + amount; 29 | 30 | emit Transfer(msg.sender, recipient, amount); 31 | return false; 32 | } 33 | 34 | function approve(address spender, uint256 amount) external override returns (bool) { 35 | allowance[msg.sender][spender] = amount; 36 | emit Approval(msg.sender, spender, amount); 37 | return true; 38 | } 39 | 40 | function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) { 41 | uint256 allowanceBefore = allowance[sender][msg.sender]; 42 | require(allowanceBefore >= amount, "allowance insufficient"); 43 | 44 | allowance[sender][msg.sender] = allowanceBefore - amount; 45 | 46 | uint256 balanceRecipient = balanceOf[recipient]; 47 | require(balanceRecipient + amount >= balanceRecipient, "overflow balance recipient"); 48 | balanceOf[recipient] = balanceRecipient + amount; 49 | uint256 balanceSender = balanceOf[sender]; 50 | require(balanceSender >= amount, "underflow balance sender"); 51 | balanceOf[sender] = balanceSender - amount; 52 | 53 | emit Transfer(sender, recipient, amount); 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/foundry-tests/types/BalanceDelta.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {BalanceDelta, toBalanceDelta} from "../../../contracts/types/BalanceDelta.sol"; 6 | 7 | contract TestBalanceDelta is Test { 8 | function testToBalanceDelta() public { 9 | BalanceDelta balanceDelta = toBalanceDelta(0, 0); 10 | assertEq(balanceDelta.amount0(), 0); 11 | assertEq(balanceDelta.amount1(), 0); 12 | 13 | balanceDelta = toBalanceDelta(0, 1); 14 | assertEq(balanceDelta.amount0(), 0); 15 | assertEq(balanceDelta.amount1(), 1); 16 | 17 | balanceDelta = toBalanceDelta(1, 0); 18 | assertEq(balanceDelta.amount0(), 1); 19 | assertEq(balanceDelta.amount1(), 0); 20 | 21 | balanceDelta = toBalanceDelta(type(int128).max, type(int128).max); 22 | assertEq(balanceDelta.amount0(), type(int128).max); 23 | assertEq(balanceDelta.amount1(), type(int128).max); 24 | 25 | balanceDelta = toBalanceDelta(type(int128).min, type(int128).min); 26 | assertEq(balanceDelta.amount0(), type(int128).min); 27 | assertEq(balanceDelta.amount1(), type(int128).min); 28 | } 29 | 30 | function testToBalanceDelta(int128 x, int128 y) public { 31 | BalanceDelta balanceDelta = toBalanceDelta(x, y); 32 | assertEq(balanceDelta.amount0(), x); 33 | assertEq(balanceDelta.amount1(), y); 34 | } 35 | 36 | function testAdd(int128 a, int128 b, int128 c, int128 d) public { 37 | int256 ac = int256(a) + c; 38 | int256 bd = int256(b) + d; 39 | 40 | // make sure the addition doesn't overflow 41 | vm.assume(ac == int128(ac)); 42 | vm.assume(bd == int128(bd)); 43 | 44 | BalanceDelta balanceDelta = toBalanceDelta(a, b) + toBalanceDelta(c, d); 45 | assertEq(balanceDelta.amount0(), ac); 46 | assertEq(balanceDelta.amount1(), bd); 47 | } 48 | 49 | function testSub(int128 a, int128 b, int128 c, int128 d) public { 50 | int256 ac = int256(a) - c; 51 | int256 bd = int256(b) - d; 52 | 53 | // make sure the subtraction doesn't underflow 54 | vm.assume(ac == int128(ac)); 55 | vm.assume(bd == int128(bd)); 56 | 57 | BalanceDelta balanceDelta = toBalanceDelta(a, b) - toBalanceDelta(c, d); 58 | assertEq(balanceDelta.amount0(), ac); 59 | assertEq(balanceDelta.amount1(), bd); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/foundry-tests/utils/Deployers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {TestERC20} from "../../../contracts/test/TestERC20.sol"; 5 | import {Hooks} from "../../../contracts/libraries/Hooks.sol"; 6 | import {Currency} from "../../../contracts/libraries/CurrencyLibrary.sol"; 7 | import {IHooks} from "../../../contracts/interfaces/IHooks.sol"; 8 | import {IPoolManager} from "../../../contracts/interfaces/IPoolManager.sol"; 9 | import {PoolManager} from "../../../contracts/PoolManager.sol"; 10 | import {PoolId, PoolIdLibrary} from "../../../contracts/libraries/PoolId.sol"; 11 | import {Fees} from "../../../contracts/libraries/Fees.sol"; 12 | 13 | contract Deployers { 14 | using Fees for uint24; 15 | using PoolIdLibrary for IPoolManager.PoolKey; 16 | 17 | uint160 constant SQRT_RATIO_1_1 = 79228162514264337593543950336; 18 | uint160 constant SQRT_RATIO_1_2 = 56022770974786139918731938227; 19 | uint160 constant SQRT_RATIO_1_4 = 39614081257132168796771975168; 20 | uint160 constant SQRT_RATIO_4_1 = 158456325028528675187087900672; 21 | 22 | function deployTokens(uint8 count, uint256 totalSupply) internal returns (TestERC20[] memory tokens) { 23 | tokens = new TestERC20[](count); 24 | for (uint8 i = 0; i < count; i++) { 25 | tokens[i] = new TestERC20(totalSupply); 26 | } 27 | } 28 | 29 | function createPool(PoolManager manager, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) 30 | private 31 | returns (IPoolManager.PoolKey memory key, PoolId id) 32 | { 33 | TestERC20[] memory tokens = deployTokens(2, 2 ** 255); 34 | key = IPoolManager.PoolKey( 35 | Currency.wrap(address(tokens[0])), 36 | Currency.wrap(address(tokens[1])), 37 | fee, 38 | fee.isDynamicFee() ? int24(60) : int24(fee / 100 * 2), 39 | hooks 40 | ); 41 | id = key.toId(); 42 | manager.initialize(key, sqrtPriceX96); 43 | } 44 | 45 | function createFreshPool(IHooks hooks, uint24 fee, uint160 sqrtPriceX96) 46 | internal 47 | returns (PoolManager manager, IPoolManager.PoolKey memory key, PoolId id) 48 | { 49 | manager = createFreshManager(); 50 | (key, id) = createPool(manager, hooks, fee, sqrtPriceX96); 51 | return (manager, key, id); 52 | } 53 | 54 | function createFreshManager() internal returns (PoolManager manager) { 55 | manager = new PoolManager(500000); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/test/PoolManagerReentrancyTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Currency, CurrencyLibrary} from "../libraries/CurrencyLibrary.sol"; 5 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 6 | import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; 7 | 8 | contract PoolManagerReentrancyTest is ILockCallback { 9 | using CurrencyLibrary for Currency; 10 | 11 | event LockAcquired(uint256 count); 12 | 13 | function reenter(IPoolManager poolManager, Currency currencyToBorrow, uint256 count) external { 14 | helper(poolManager, currencyToBorrow, count, count); 15 | } 16 | 17 | function helper(IPoolManager poolManager, Currency currencyToBorrow, uint256 total, uint256 count) internal { 18 | // check that it is currently already locked `total-count` times, ... 19 | assert(poolManager.lockedByLength() == total - count); 20 | poolManager.lock(abi.encode(currencyToBorrow, total, count)); 21 | // and still has that many locks after this particular lock is released 22 | assert(poolManager.lockedByLength() == total - count); 23 | } 24 | 25 | function lockAcquired(uint256 id, bytes calldata data) external returns (bytes memory) { 26 | (Currency currencyToBorrow, uint256 total, uint256 count) = abi.decode(data, (Currency, uint256, uint256)); 27 | emit LockAcquired(count); 28 | 29 | IPoolManager poolManager = IPoolManager(msg.sender); 30 | 31 | assert(poolManager.lockedBy(id) == address(this)); 32 | assert(poolManager.lockedByLength() == id + 1); 33 | 34 | assert(poolManager.getNonzeroDeltaCount(id) == 0); 35 | 36 | int256 delta = poolManager.getCurrencyDelta(id, currencyToBorrow); 37 | assert(delta == 0); 38 | 39 | // take some 40 | poolManager.take(currencyToBorrow, address(this), 1); 41 | assert(poolManager.getNonzeroDeltaCount(id) == 1); 42 | delta = poolManager.getCurrencyDelta(id, currencyToBorrow); 43 | assert(delta == 1); 44 | 45 | // then pay it back 46 | currencyToBorrow.transfer(address(poolManager), 1); 47 | poolManager.settle(currencyToBorrow); 48 | assert(poolManager.getNonzeroDeltaCount(id) == 0); 49 | delta = poolManager.getCurrencyDelta(id, currencyToBorrow); 50 | assert(delta == 0); 51 | 52 | if (count > 0) helper(IPoolManager(msg.sender), currencyToBorrow, total, count - 1); 53 | 54 | return ""; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/test/HooksTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Hooks} from "../libraries/Hooks.sol"; 5 | import {IHooks} from "../interfaces/IHooks.sol"; 6 | 7 | contract HooksTest { 8 | using Hooks for IHooks; 9 | 10 | function validateHookAddress(address hookAddress, Hooks.Calls calldata params) external pure { 11 | IHooks(hookAddress).validateHookAddress(params); 12 | } 13 | 14 | function isValidHookAddress(address hookAddress, uint24 fee) external pure returns (bool) { 15 | return IHooks(hookAddress).isValidHookAddress(fee); 16 | } 17 | 18 | function shouldCallBeforeInitialize(address hookAddress) external pure returns (bool) { 19 | return IHooks(hookAddress).shouldCallBeforeInitialize(); 20 | } 21 | 22 | function shouldCallAfterInitialize(address hookAddress) external pure returns (bool) { 23 | return IHooks(hookAddress).shouldCallAfterInitialize(); 24 | } 25 | 26 | function shouldCallBeforeSwap(address hookAddress) external pure returns (bool) { 27 | return IHooks(hookAddress).shouldCallBeforeSwap(); 28 | } 29 | 30 | function shouldCallAfterSwap(address hookAddress) external pure returns (bool) { 31 | return IHooks(hookAddress).shouldCallAfterSwap(); 32 | } 33 | 34 | function shouldCallBeforeModifyPosition(address hookAddress) external pure returns (bool) { 35 | return IHooks(hookAddress).shouldCallBeforeModifyPosition(); 36 | } 37 | 38 | function shouldCallAfterModifyPosition(address hookAddress) external pure returns (bool) { 39 | return IHooks(hookAddress).shouldCallAfterModifyPosition(); 40 | } 41 | 42 | function shouldCallBeforeDonate(address hookAddress) external pure returns (bool) { 43 | return IHooks(hookAddress).shouldCallBeforeDonate(); 44 | } 45 | 46 | function shouldCallAfterDonate(address hookAddress) external pure returns (bool) { 47 | return IHooks(hookAddress).shouldCallAfterDonate(); 48 | } 49 | 50 | function getGasCostOfShouldCall(address hookAddress) external view returns (uint256) { 51 | uint256 gasBefore = gasleft(); 52 | IHooks(hookAddress).shouldCallBeforeSwap(); 53 | return gasBefore - gasleft(); 54 | } 55 | 56 | function getGasCostOfValidateHookAddress(address hookAddress, Hooks.Calls calldata params) 57 | external 58 | view 59 | returns (uint256) 60 | { 61 | uint256 gasBefore = gasleft(); 62 | IHooks(hookAddress).validateHookAddress(params); 63 | return gasBefore - gasleft(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/test/PoolDonateTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Currency, CurrencyLibrary} from "../libraries/CurrencyLibrary.sol"; 5 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 6 | 7 | import {Currency} from "../libraries/CurrencyLibrary.sol"; 8 | import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; 9 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 10 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 11 | 12 | contract PoolDonateTest is ILockCallback { 13 | using CurrencyLibrary for Currency; 14 | 15 | IPoolManager public immutable manager; 16 | 17 | constructor(IPoolManager _manager) { 18 | manager = _manager; 19 | } 20 | 21 | struct CallbackData { 22 | address sender; 23 | IPoolManager.PoolKey key; 24 | uint256 amount0; 25 | uint256 amount1; 26 | } 27 | 28 | function donate(IPoolManager.PoolKey memory key, uint256 amount0, uint256 amount1) 29 | external 30 | payable 31 | returns (BalanceDelta delta) 32 | { 33 | delta = abi.decode(manager.lock(abi.encode(CallbackData(msg.sender, key, amount0, amount1))), (BalanceDelta)); 34 | 35 | uint256 ethBalance = address(this).balance; 36 | if (ethBalance > 0) { 37 | CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); 38 | } 39 | } 40 | 41 | function lockAcquired(uint256, bytes calldata rawData) external returns (bytes memory) { 42 | require(msg.sender == address(manager)); 43 | 44 | CallbackData memory data = abi.decode(rawData, (CallbackData)); 45 | 46 | BalanceDelta delta = manager.donate(data.key, data.amount0, data.amount1); 47 | 48 | if (delta.amount0() > 0) { 49 | if (data.key.currency0.isNative()) { 50 | manager.settle{value: uint128(delta.amount0())}(data.key.currency0); 51 | } else { 52 | IERC20Minimal(Currency.unwrap(data.key.currency0)).transferFrom( 53 | data.sender, address(manager), uint128(delta.amount0()) 54 | ); 55 | manager.settle(data.key.currency0); 56 | } 57 | } 58 | if (delta.amount1() > 0) { 59 | if (data.key.currency1.isNative()) { 60 | manager.settle{value: uint128(delta.amount1())}(data.key.currency1); 61 | } else { 62 | IERC20Minimal(Currency.unwrap(data.key.currency1)).transferFrom( 63 | data.sender, address(manager), uint128(delta.amount1()) 64 | ); 65 | manager.settle(data.key.currency1); 66 | } 67 | } 68 | 69 | return abi.encode(delta); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/test/EmptyTestHooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Hooks} from "../libraries/Hooks.sol"; 5 | import {IHooks} from "../interfaces/IHooks.sol"; 6 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 7 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 8 | 9 | contract EmptyTestHooks is IHooks { 10 | using Hooks for IHooks; 11 | 12 | constructor() { 13 | IHooks(this).validateHookAddress( 14 | Hooks.Calls({ 15 | beforeInitialize: true, 16 | afterInitialize: true, 17 | beforeModifyPosition: true, 18 | afterModifyPosition: true, 19 | beforeSwap: true, 20 | afterSwap: true, 21 | beforeDonate: true, 22 | afterDonate: true 23 | }) 24 | ); 25 | } 26 | 27 | function beforeInitialize(address, IPoolManager.PoolKey memory, uint160) external pure override returns (bytes4) { 28 | return IHooks.beforeInitialize.selector; 29 | } 30 | 31 | function afterInitialize(address, IPoolManager.PoolKey memory, uint160, int24) 32 | external 33 | pure 34 | override 35 | returns (bytes4) 36 | { 37 | return IHooks.afterInitialize.selector; 38 | } 39 | 40 | function beforeModifyPosition(address, IPoolManager.PoolKey calldata, IPoolManager.ModifyPositionParams calldata) 41 | external 42 | pure 43 | override 44 | returns (bytes4) 45 | { 46 | return IHooks.beforeModifyPosition.selector; 47 | } 48 | 49 | function afterModifyPosition( 50 | address, 51 | IPoolManager.PoolKey calldata, 52 | IPoolManager.ModifyPositionParams calldata, 53 | BalanceDelta 54 | ) external pure override returns (bytes4) { 55 | return IHooks.afterModifyPosition.selector; 56 | } 57 | 58 | function beforeSwap(address, IPoolManager.PoolKey calldata, IPoolManager.SwapParams calldata) 59 | external 60 | pure 61 | override 62 | returns (bytes4) 63 | { 64 | return IHooks.beforeSwap.selector; 65 | } 66 | 67 | function afterSwap(address, IPoolManager.PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta) 68 | external 69 | pure 70 | override 71 | returns (bytes4) 72 | { 73 | return IHooks.afterSwap.selector; 74 | } 75 | 76 | function beforeDonate(address, IPoolManager.PoolKey calldata, uint256, uint256) 77 | external 78 | pure 79 | override 80 | returns (bytes4) 81 | { 82 | return IHooks.beforeDonate.selector; 83 | } 84 | 85 | function afterDonate(address, IPoolManager.PoolKey calldata, uint256, uint256) 86 | external 87 | pure 88 | override 89 | returns (bytes4) 90 | { 91 | return IHooks.afterDonate.selector; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/test/SqrtPriceMathTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {SqrtPriceMath} from "../libraries/SqrtPriceMath.sol"; 5 | 6 | contract SqrtPriceMathTest { 7 | function getNextSqrtPriceFromInput(uint160 sqrtP, uint128 liquidity, uint256 amountIn, bool zeroForOne) 8 | external 9 | pure 10 | returns (uint160 sqrtQ) 11 | { 12 | return SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, liquidity, amountIn, zeroForOne); 13 | } 14 | 15 | function getGasCostOfGetNextSqrtPriceFromInput(uint160 sqrtP, uint128 liquidity, uint256 amountIn, bool zeroForOne) 16 | external 17 | view 18 | returns (uint256) 19 | { 20 | uint256 gasBefore = gasleft(); 21 | SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, liquidity, amountIn, zeroForOne); 22 | return gasBefore - gasleft(); 23 | } 24 | 25 | function getNextSqrtPriceFromOutput(uint160 sqrtP, uint128 liquidity, uint256 amountOut, bool zeroForOne) 26 | external 27 | pure 28 | returns (uint160 sqrtQ) 29 | { 30 | return SqrtPriceMath.getNextSqrtPriceFromOutput(sqrtP, liquidity, amountOut, zeroForOne); 31 | } 32 | 33 | function getGasCostOfGetNextSqrtPriceFromOutput( 34 | uint160 sqrtP, 35 | uint128 liquidity, 36 | uint256 amountOut, 37 | bool zeroForOne 38 | ) external view returns (uint256) { 39 | uint256 gasBefore = gasleft(); 40 | SqrtPriceMath.getNextSqrtPriceFromOutput(sqrtP, liquidity, amountOut, zeroForOne); 41 | return gasBefore - gasleft(); 42 | } 43 | 44 | function getAmount0Delta(uint160 sqrtLower, uint160 sqrtUpper, uint128 liquidity, bool roundUp) 45 | external 46 | pure 47 | returns (uint256 amount0) 48 | { 49 | return SqrtPriceMath.getAmount0Delta(sqrtLower, sqrtUpper, liquidity, roundUp); 50 | } 51 | 52 | function getAmount1Delta(uint160 sqrtLower, uint160 sqrtUpper, uint128 liquidity, bool roundUp) 53 | external 54 | pure 55 | returns (uint256 amount1) 56 | { 57 | return SqrtPriceMath.getAmount1Delta(sqrtLower, sqrtUpper, liquidity, roundUp); 58 | } 59 | 60 | function getGasCostOfGetAmount0Delta(uint160 sqrtLower, uint160 sqrtUpper, uint128 liquidity, bool roundUp) 61 | external 62 | view 63 | returns (uint256) 64 | { 65 | uint256 gasBefore = gasleft(); 66 | SqrtPriceMath.getAmount0Delta(sqrtLower, sqrtUpper, liquidity, roundUp); 67 | return gasBefore - gasleft(); 68 | } 69 | 70 | function getGasCostOfGetAmount1Delta(uint160 sqrtLower, uint160 sqrtUpper, uint128 liquidity, bool roundUp) 71 | external 72 | view 73 | returns (uint256) 74 | { 75 | uint256 gasBefore = gasleft(); 76 | SqrtPriceMath.getAmount1Delta(sqrtLower, sqrtUpper, liquidity, roundUp); 77 | return gasBefore - gasleft(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contracts/test/PoolModifyPositionTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {CurrencyLibrary, Currency} from "../libraries/CurrencyLibrary.sol"; 5 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 6 | 7 | import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; 8 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 9 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 10 | 11 | contract PoolModifyPositionTest is ILockCallback { 12 | using CurrencyLibrary for Currency; 13 | 14 | IPoolManager public immutable manager; 15 | 16 | constructor(IPoolManager _manager) { 17 | manager = _manager; 18 | } 19 | 20 | struct CallbackData { 21 | address sender; 22 | IPoolManager.PoolKey key; 23 | IPoolManager.ModifyPositionParams params; 24 | } 25 | 26 | function modifyPosition(IPoolManager.PoolKey memory key, IPoolManager.ModifyPositionParams memory params) 27 | external 28 | payable 29 | returns (BalanceDelta delta) 30 | { 31 | delta = abi.decode(manager.lock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); 32 | 33 | uint256 ethBalance = address(this).balance; 34 | if (ethBalance > 0) { 35 | CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); 36 | } 37 | } 38 | 39 | function lockAcquired(uint256, bytes calldata rawData) external returns (bytes memory) { 40 | require(msg.sender == address(manager)); 41 | 42 | CallbackData memory data = abi.decode(rawData, (CallbackData)); 43 | 44 | BalanceDelta delta = manager.modifyPosition(data.key, data.params); 45 | 46 | if (delta.amount0() > 0) { 47 | if (data.key.currency0.isNative()) { 48 | manager.settle{value: uint128(delta.amount0())}(data.key.currency0); 49 | } else { 50 | IERC20Minimal(Currency.unwrap(data.key.currency0)).transferFrom( 51 | data.sender, address(manager), uint128(delta.amount0()) 52 | ); 53 | manager.settle(data.key.currency0); 54 | } 55 | } 56 | if (delta.amount1() > 0) { 57 | if (data.key.currency1.isNative()) { 58 | manager.settle{value: uint128(delta.amount1())}(data.key.currency1); 59 | } else { 60 | IERC20Minimal(Currency.unwrap(data.key.currency1)).transferFrom( 61 | data.sender, address(manager), uint128(delta.amount1()) 62 | ); 63 | manager.settle(data.key.currency1); 64 | } 65 | } 66 | 67 | if (delta.amount0() < 0) { 68 | manager.take(data.key.currency0, data.sender, uint128(-delta.amount0())); 69 | } 70 | if (delta.amount1() < 0) { 71 | manager.take(data.key.currency1, data.sender, uint128(-delta.amount1())); 72 | } 73 | 74 | return abi.encode(delta); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /contracts/test/PoolTakeTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Currency, CurrencyLibrary} from "../libraries/CurrencyLibrary.sol"; 5 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 6 | 7 | import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; 8 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 9 | 10 | contract PoolTakeTest is ILockCallback { 11 | using CurrencyLibrary for Currency; 12 | 13 | IPoolManager public immutable manager; 14 | 15 | constructor(IPoolManager _manager) { 16 | manager = _manager; 17 | } 18 | 19 | struct CallbackData { 20 | address sender; 21 | IPoolManager.PoolKey key; 22 | uint256 amount0; 23 | uint256 amount1; 24 | } 25 | 26 | function take(IPoolManager.PoolKey memory key, uint256 amount0, uint256 amount1) external payable { 27 | manager.lock(abi.encode(CallbackData(msg.sender, key, amount0, amount1))); 28 | } 29 | 30 | function balanceOf(Currency currency, address user) internal view returns (uint256) { 31 | if (currency.isNative()) { 32 | return user.balance; 33 | } else { 34 | return IERC20Minimal(Currency.unwrap(currency)).balanceOf(user); 35 | } 36 | } 37 | 38 | function lockAcquired(uint256, bytes calldata rawData) external returns (bytes memory) { 39 | require(msg.sender == address(manager)); 40 | 41 | CallbackData memory data = abi.decode(rawData, (CallbackData)); 42 | 43 | if (data.amount0 > 0) { 44 | uint256 balBefore = balanceOf(data.key.currency0, data.sender); 45 | manager.take(data.key.currency0, data.sender, data.amount0); 46 | uint256 balAfter = balanceOf(data.key.currency0, data.sender); 47 | require(balAfter - balBefore == data.amount0); 48 | 49 | if (data.key.currency0.isNative()) { 50 | manager.settle{value: uint256(data.amount0)}(data.key.currency0); 51 | } else { 52 | IERC20Minimal(Currency.unwrap(data.key.currency0)).transferFrom( 53 | data.sender, address(manager), uint256(data.amount0) 54 | ); 55 | manager.settle(data.key.currency0); 56 | } 57 | } 58 | 59 | if (data.amount1 > 0) { 60 | uint256 balBefore = balanceOf(data.key.currency1, data.sender); 61 | manager.take(data.key.currency1, data.sender, data.amount1); 62 | uint256 balAfter = balanceOf(data.key.currency1, data.sender); 63 | require(balAfter - balBefore == data.amount1); 64 | 65 | if (data.key.currency1.isNative()) { 66 | manager.settle{value: uint256(data.amount1)}(data.key.currency1); 67 | } else { 68 | IERC20Minimal(Currency.unwrap(data.key.currency1)).transferFrom( 69 | data.sender, address(manager), uint256(data.amount1) 70 | ); 71 | manager.settle(data.key.currency1); 72 | } 73 | } 74 | 75 | return abi.encode(0); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/interfaces/external/IERC20Minimal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title Minimal ERC20 interface for Uniswap 5 | /// @notice Contains a subset of the full ERC20 interface that is used in Uniswap V3 6 | interface IERC20Minimal { 7 | /// @notice Returns the balance of a token 8 | /// @param account The account for which to look up the number of tokens it has, i.e. its balance 9 | /// @return The number of tokens held by the account 10 | function balanceOf(address account) external view returns (uint256); 11 | 12 | /// @notice Transfers the amount of token from the `msg.sender` to the recipient 13 | /// @param recipient The account that will receive the amount transferred 14 | /// @param amount The number of tokens to send from the sender to the recipient 15 | /// @return Returns true for a successful transfer, false for an unsuccessful transfer 16 | function transfer(address recipient, uint256 amount) external returns (bool); 17 | 18 | /// @notice Returns the current allowance given to a spender by an owner 19 | /// @param owner The account of the token owner 20 | /// @param spender The account of the token spender 21 | /// @return The current allowance granted by `owner` to `spender` 22 | function allowance(address owner, address spender) external view returns (uint256); 23 | 24 | /// @notice Sets the allowance of a spender from the `msg.sender` to the value `amount` 25 | /// @param spender The account which will be allowed to spend a given amount of the owners tokens 26 | /// @param amount The amount of tokens allowed to be used by `spender` 27 | /// @return Returns true for a successful approval, false for unsuccessful 28 | function approve(address spender, uint256 amount) external returns (bool); 29 | 30 | /// @notice Transfers `amount` tokens from `sender` to `recipient` up to the allowance given to the `msg.sender` 31 | /// @param sender The account from which the transfer will be initiated 32 | /// @param recipient The recipient of the transfer 33 | /// @param amount The amount of the transfer 34 | /// @return Returns true for a successful transfer, false for unsuccessful 35 | function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); 36 | 37 | /// @notice Event emitted when tokens are transferred from one address to another, either via `#transfer` or `#transferFrom`. 38 | /// @param from The account from which the tokens were sent, i.e. the balance decreased 39 | /// @param to The account to which the tokens were sent, i.e. the balance increased 40 | /// @param value The amount of tokens that were transferred 41 | event Transfer(address indexed from, address indexed to, uint256 value); 42 | 43 | /// @notice Event emitted when the approval amount for the spender of a given owner's tokens changes. 44 | /// @param owner The account that approved spending of its tokens 45 | /// @param spender The account for which the spending allowance was modified 46 | /// @param value The new allowance from the owner to the spender 47 | event Approval(address indexed owner, address indexed spender, uint256 value); 48 | } 49 | -------------------------------------------------------------------------------- /test/foundry-tests/DynamicFees.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Vm} from "forge-std/Vm.sol"; 6 | import {PoolId, PoolIdLibrary} from "../../contracts/libraries/PoolId.sol"; 7 | import {Hooks} from "../../contracts/libraries/Hooks.sol"; 8 | import {IPoolManager} from "../../contracts/interfaces/IPoolManager.sol"; 9 | import {IHooks} from "../../contracts/interfaces/IHooks.sol"; 10 | import {Currency} from "../../contracts/libraries/CurrencyLibrary.sol"; 11 | import {PoolManager} from "../../contracts/PoolManager.sol"; 12 | import {PoolSwapTest} from "../../contracts/test/PoolSwapTest.sol"; 13 | import {Deployers} from "./utils/Deployers.sol"; 14 | import {IDynamicFeeManager} from "././../../contracts/interfaces/IDynamicFeeManager.sol"; 15 | import {Fees} from "./../../contracts/libraries/Fees.sol"; 16 | 17 | contract DynamicFees is IDynamicFeeManager { 18 | uint24 internal fee; 19 | 20 | function setFee(uint24 _fee) external { 21 | fee = _fee; 22 | } 23 | 24 | function getFee(IPoolManager.PoolKey calldata) public view returns (uint24) { 25 | return fee; 26 | } 27 | } 28 | 29 | contract TestDynamicFees is Test, Deployers { 30 | using PoolIdLibrary for IPoolManager.PoolKey; 31 | 32 | DynamicFees dynamicFees = DynamicFees( 33 | address( 34 | uint160(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF) 35 | & uint160( 36 | ~Hooks.BEFORE_INITIALIZE_FLAG & ~Hooks.AFTER_INITIALIZE_FLAG & ~Hooks.BEFORE_MODIFY_POSITION_FLAG 37 | & ~Hooks.AFTER_MODIFY_POSITION_FLAG & ~Hooks.BEFORE_SWAP_FLAG & ~Hooks.AFTER_SWAP_FLAG 38 | & ~Hooks.BEFORE_DONATE_FLAG & ~Hooks.AFTER_DONATE_FLAG 39 | ) 40 | ) 41 | ); 42 | PoolManager manager; 43 | IPoolManager.PoolKey key; 44 | PoolSwapTest swapRouter; 45 | 46 | function setUp() public { 47 | DynamicFees impl = new DynamicFees(); 48 | vm.etch(address(dynamicFees), address(impl).code); 49 | 50 | (manager, key,) = Deployers.createFreshPool(IHooks(address(dynamicFees)), Fees.DYNAMIC_FEE_FLAG, SQRT_RATIO_1_1); 51 | swapRouter = new PoolSwapTest(manager); 52 | } 53 | 54 | function testSwapFailsWithTooLargeFee() public { 55 | dynamicFees.setFee(1000000); 56 | vm.expectRevert(IPoolManager.FeeTooLarge.selector); 57 | swapRouter.swap( 58 | key, IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), PoolSwapTest.TestSettings(false, false) 59 | ); 60 | } 61 | 62 | event Swap( 63 | PoolId indexed poolId, 64 | address indexed sender, 65 | int128 amount0, 66 | int128 amount1, 67 | uint160 sqrtPriceX96, 68 | uint128 liquidity, 69 | int24 tick, 70 | uint24 fee 71 | ); 72 | 73 | function testSwapWorks() public { 74 | dynamicFees.setFee(123); 75 | vm.expectEmit(true, true, true, true, address(manager)); 76 | emit Swap(key.toId(), address(swapRouter), 0, 0, SQRT_RATIO_1_1 + 1, 0, 0, 123); 77 | swapRouter.swap( 78 | key, IPoolManager.SwapParams(false, 1, SQRT_RATIO_1_1 + 1), PoolSwapTest.TestSettings(false, false) 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /echidna.config.yml: -------------------------------------------------------------------------------- 1 | #format can be "text" or "json" for different output (human or machine readable) 2 | format: 'text' 3 | #checkAsserts checks assertions 4 | checkAsserts: true 5 | #coverage controls coverage guided testing 6 | coverage: false 7 | # #psender is the sender for property transactions; by default intentionally 8 | # #the same as contract deployer 9 | # psender: "0x00a329c0648769a73afac7f9381e08fb43dbea70" 10 | # #prefix is the prefix for Boolean functions that are properties to be checked 11 | # prefix: "echidna_" 12 | # #propMaxGas defines gas cost at which a property fails 13 | # propMaxGas: 8000030 14 | # #testMaxGas is a gas limit; does not cause failure, but terminates sequence 15 | # testMaxGas: 8000030 16 | # #maxGasprice is the maximum gas price 17 | # maxGasprice: 100000000000 18 | # #testLimit is the number of test sequences to run 19 | # testLimit: 50000 20 | # #stopOnFail makes echidna terminate as soon as any property fails and has been shrunk 21 | # stopOnFail: false 22 | # #estimateGas makes echidna perform analysis of maximum gas costs for functions (experimental) 23 | # estimateGas: false 24 | # #seqLen defines how many transactions are in a test sequence 25 | # seqLen: 100 26 | # #shrinkLimit determines how much effort is spent shrinking failing sequences 27 | # shrinkLimit: 5000 28 | # #contractAddr is the address of the contract itself 29 | # contractAddr: "0x00a329c0648769a73afac7f9381e08fb43dbea72" 30 | # #deployer is address of the contract deployer (who often is privileged owner, etc.) 31 | # deployer: "0x00a329c0648769a73afac7f9381e08fb43dbea70" 32 | # #sender is set of addresses transactions may originate from 33 | # sender: ["0x10000", "0x20000", "0x00a329c0648769a73afac7f9381e08fb43dbea70"] 34 | # #balanceAddr is default balance for addresses 35 | # balanceAddr: 0xffffffff 36 | # #balanceContract overrides balanceAddr for the contract address 37 | # balanceContract: 0 38 | # #solcArgs allows special args to solc 39 | # solcArgs: "" 40 | # #solcLibs is solc libraries 41 | # solcLibs: [] 42 | # #cryticArgs allows special args to crytic 43 | # cryticArgs: [] 44 | # #quiet produces (much) less verbose output 45 | # quiet: false 46 | # #initialize the blockchain with some data 47 | # initialize: null 48 | # #whether ot not to use the multi-abi mode of testing 49 | # multi-abi: false 50 | # #benchmarkMode enables benchmark mode 51 | # benchmarkMode: false 52 | # #timeout controls test timeout settings 53 | # timeout: null 54 | # #seed not defined by default, is the random seed 55 | # #seed: 0 56 | # #dictFreq controls how often to use echidna's internal dictionary vs random 57 | # #values 58 | # dictFreq: 0.40 59 | # maxTimeDelay: 604800 60 | # #maximum time between generated txs; default is one week 61 | # maxBlockDelay: 60480 62 | # #maximum number of blocks elapsed between generated txs; default is expected increment in one week 63 | # # timeout: 64 | # #campaign timeout (in seconds) 65 | # # list of methods to filter 66 | # filterFunctions: [] 67 | # # by default, blacklist methods in filterFunctions 68 | # filterBlacklist: true 69 | # #directory to save the corpus; by default is disabled 70 | # corpusDir: null 71 | # # constants for corpus mutations (for experimentation only) 72 | # mutConsts: [100, 1, 1] 73 | # # maximum value to send to payable functions 74 | # maxValue: 100000000000000000000 # 100 eth 75 | -------------------------------------------------------------------------------- /contracts/test/TickOverflowSafetyEchidnaTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Pool} from "../libraries/Pool.sol"; 5 | 6 | contract TickOverflowSafetyEchidnaTest { 7 | using Pool for Pool.State; 8 | 9 | int24 private constant MIN_TICK = -16; 10 | int24 private constant MAX_TICK = 16; 11 | 12 | Pool.State private pool; 13 | int24 private tick = 0; 14 | 15 | // half the cap of fee growth has happened, this can overflow 16 | uint256 feeGrowthGlobal0X128 = type(uint256).max / 2; 17 | uint256 feeGrowthGlobal1X128 = type(uint256).max / 2; 18 | 19 | // used to track how much total liquidity has been added. should never be negative 20 | int256 totalLiquidity = 0; 21 | // how much total growth has happened, this cannot overflow 22 | uint256 private totalGrowth0 = 0; 23 | uint256 private totalGrowth1 = 0; 24 | 25 | function increaseFeeGrowthGlobal0X128(uint256 amount) external { 26 | require(totalGrowth0 + amount > totalGrowth0); // overflow check 27 | feeGrowthGlobal0X128 += amount; // overflow desired 28 | totalGrowth0 += amount; 29 | } 30 | 31 | function increaseFeeGrowthGlobal1X128(uint256 amount) external { 32 | require(totalGrowth1 + amount > totalGrowth1); // overflow check 33 | feeGrowthGlobal1X128 += amount; // overflow desired 34 | totalGrowth1 += amount; 35 | } 36 | 37 | function setPosition(int24 tickLower, int24 tickUpper, int128 liquidityDelta) external { 38 | require(tickLower > MIN_TICK); 39 | require(tickUpper < MAX_TICK); 40 | require(tickLower < tickUpper); 41 | (bool flippedLower,) = pool.updateTick(tickLower, liquidityDelta, false); 42 | (bool flippedUpper,) = pool.updateTick(tickUpper, liquidityDelta, true); 43 | 44 | if (flippedLower) { 45 | if (liquidityDelta < 0) { 46 | assert(pool.ticks[tickLower].liquidityGross == 0); 47 | pool.clearTick(tickLower); 48 | } else { 49 | assert(pool.ticks[tickLower].liquidityGross > 0); 50 | } 51 | } 52 | 53 | if (flippedUpper) { 54 | if (liquidityDelta < 0) { 55 | assert(pool.ticks[tickUpper].liquidityGross == 0); 56 | pool.clearTick(tickUpper); 57 | } else { 58 | assert(pool.ticks[tickUpper].liquidityGross > 0); 59 | } 60 | } 61 | 62 | totalLiquidity += liquidityDelta; 63 | // requires should have prevented this 64 | assert(totalLiquidity >= 0); 65 | 66 | if (totalLiquidity == 0) { 67 | totalGrowth0 = 0; 68 | totalGrowth1 = 0; 69 | } 70 | } 71 | 72 | function moveToTick(int24 target) external { 73 | require(target > MIN_TICK); 74 | require(target < MAX_TICK); 75 | while (tick != target) { 76 | if (tick < target) { 77 | if (pool.ticks[tick + 1].liquidityGross > 0) { 78 | pool.crossTick(tick + 1, feeGrowthGlobal0X128, feeGrowthGlobal1X128); 79 | } 80 | tick++; 81 | } else { 82 | if (pool.ticks[tick].liquidityGross > 0) { 83 | pool.crossTick(tick, feeGrowthGlobal0X128, feeGrowthGlobal1X128); 84 | } 85 | tick--; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /contracts/libraries/BitMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title BitMath 5 | /// @dev This library provides functionality for computing bit properties of an unsigned integer 6 | library BitMath { 7 | /// @notice Returns the index of the most significant bit of the number, 8 | /// where the least significant bit is at index 0 and the most significant bit is at index 255 9 | /// @dev The function satisfies the property: 10 | /// x >= 2**mostSignificantBit(x) and x < 2**(mostSignificantBit(x)+1) 11 | /// @param x the value for which to compute the most significant bit, must be greater than 0 12 | /// @return r the index of the most significant bit 13 | function mostSignificantBit(uint256 x) internal pure returns (uint8 r) { 14 | require(x > 0); 15 | 16 | unchecked { 17 | if (x >= 0x100000000000000000000000000000000) { 18 | x >>= 128; 19 | r += 128; 20 | } 21 | if (x >= 0x10000000000000000) { 22 | x >>= 64; 23 | r += 64; 24 | } 25 | if (x >= 0x100000000) { 26 | x >>= 32; 27 | r += 32; 28 | } 29 | if (x >= 0x10000) { 30 | x >>= 16; 31 | r += 16; 32 | } 33 | if (x >= 0x100) { 34 | x >>= 8; 35 | r += 8; 36 | } 37 | if (x >= 0x10) { 38 | x >>= 4; 39 | r += 4; 40 | } 41 | if (x >= 0x4) { 42 | x >>= 2; 43 | r += 2; 44 | } 45 | if (x >= 0x2) r += 1; 46 | } 47 | } 48 | 49 | /// @notice Returns the index of the least significant bit of the number, 50 | /// where the least significant bit is at index 0 and the most significant bit is at index 255 51 | /// @dev The function satisfies the property: 52 | /// (x & 2**leastSignificantBit(x)) != 0 and (x & (2**(leastSignificantBit(x)) - 1)) == 0) 53 | /// @param x the value for which to compute the least significant bit, must be greater than 0 54 | /// @return r the index of the least significant bit 55 | function leastSignificantBit(uint256 x) internal pure returns (uint8 r) { 56 | require(x > 0); 57 | 58 | unchecked { 59 | r = 255; 60 | if (x & type(uint128).max > 0) { 61 | r -= 128; 62 | } else { 63 | x >>= 128; 64 | } 65 | if (x & type(uint64).max > 0) { 66 | r -= 64; 67 | } else { 68 | x >>= 64; 69 | } 70 | if (x & type(uint32).max > 0) { 71 | r -= 32; 72 | } else { 73 | x >>= 32; 74 | } 75 | if (x & type(uint16).max > 0) { 76 | r -= 16; 77 | } else { 78 | x >>= 16; 79 | } 80 | if (x & type(uint8).max > 0) { 81 | r -= 8; 82 | } else { 83 | x >>= 8; 84 | } 85 | if (x & 0xf > 0) { 86 | r -= 4; 87 | } else { 88 | x >>= 4; 89 | } 90 | if (x & 0x3 > 0) { 91 | r -= 2; 92 | } else { 93 | x >>= 2; 94 | } 95 | if (x & 0x1 > 0) r -= 1; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /contracts/libraries/CurrencyLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 5 | 6 | type Currency is address; 7 | 8 | /// @title CurrencyLibrary 9 | /// @dev This library allows for transferring and holding native tokens and ERC20 tokens 10 | library CurrencyLibrary { 11 | using CurrencyLibrary for Currency; 12 | 13 | /// @notice Thrown when a native transfer fails 14 | error NativeTransferFailed(); 15 | 16 | /// @notice Thrown when an ERC20 transfer fails 17 | error ERC20TransferFailed(); 18 | 19 | Currency public constant NATIVE = Currency.wrap(address(0)); 20 | 21 | function transfer(Currency currency, address to, uint256 amount) internal { 22 | // implementation from 23 | // https://github.com/transmissions11/solmate/blob/e8f96f25d48fe702117ce76c79228ca4f20206cb/src/utils/SafeTransferLib.sol 24 | 25 | bool success; 26 | if (currency.isNative()) { 27 | assembly { 28 | // Transfer the ETH and store if it succeeded or not. 29 | success := call(gas(), to, amount, 0, 0, 0, 0) 30 | } 31 | 32 | if (!success) revert NativeTransferFailed(); 33 | } else { 34 | assembly { 35 | // We'll write our calldata to this slot below, but restore it later. 36 | let memPointer := mload(0x40) 37 | 38 | // Write the abi-encoded calldata into memory, beginning with the function selector. 39 | mstore(0, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) 40 | mstore(4, to) // Append the "to" argument. 41 | mstore(36, amount) // Append the "amount" argument. 42 | 43 | success := 44 | and( 45 | // Set success to whether the call reverted, if not we check it either 46 | // returned exactly 1 (can't just be non-zero data), or had no return data. 47 | or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), 48 | // We use 68 because that's the total length of our calldata (4 + 32 * 2) 49 | // Counterintuitively, this call() must be positioned after the or() in the 50 | // surrounding and() because and() evaluates its arguments from right to left. 51 | call(gas(), currency, 0, 0, 68, 0, 32) 52 | ) 53 | 54 | mstore(0x60, 0) // Restore the zero slot to zero. 55 | mstore(0x40, memPointer) // Restore the memPointer. 56 | } 57 | 58 | if (!success) revert ERC20TransferFailed(); 59 | } 60 | } 61 | 62 | function balanceOfSelf(Currency currency) internal view returns (uint256) { 63 | if (currency.isNative()) { 64 | return address(this).balance; 65 | } else { 66 | return IERC20Minimal(Currency.unwrap(currency)).balanceOf(address(this)); 67 | } 68 | } 69 | 70 | function equals(Currency currency, Currency other) internal pure returns (bool) { 71 | return Currency.unwrap(currency) == Currency.unwrap(other); 72 | } 73 | 74 | function isNative(Currency currency) internal pure returns (bool) { 75 | return Currency.unwrap(currency) == Currency.unwrap(NATIVE); 76 | } 77 | 78 | function toId(Currency currency) internal pure returns (uint256) { 79 | return uint160(Currency.unwrap(currency)); 80 | } 81 | 82 | function fromId(uint256 id) internal pure returns (Currency) { 83 | return Currency.wrap(address(uint160(id))); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /contracts/libraries/Position.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.19; 3 | 4 | import {FullMath} from "./FullMath.sol"; 5 | import {FixedPoint128} from "./FixedPoint128.sol"; 6 | 7 | /// @title Position 8 | /// @notice Positions represent an owner address' liquidity between a lower and upper tick boundary 9 | /// @dev Positions store additional state for tracking fees owed to the position 10 | library Position { 11 | /// @notice Cannot update a position with no liquidity 12 | error CannotUpdateEmptyPosition(); 13 | 14 | // info stored for each user's position 15 | struct Info { 16 | // the amount of liquidity owned by this position 17 | uint128 liquidity; 18 | // fee growth per unit of liquidity as of the last update to liquidity or fees owed 19 | uint256 feeGrowthInside0LastX128; 20 | uint256 feeGrowthInside1LastX128; 21 | } 22 | 23 | /// @notice Returns the Info struct of a position, given an owner and position boundaries 24 | /// @param self The mapping containing all user positions 25 | /// @param owner The address of the position owner 26 | /// @param tickLower The lower tick boundary of the position 27 | /// @param tickUpper The upper tick boundary of the position 28 | /// @return position The position info struct of the given owners' position 29 | function get(mapping(bytes32 => Info) storage self, address owner, int24 tickLower, int24 tickUpper) 30 | internal 31 | view 32 | returns (Position.Info storage position) 33 | { 34 | position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))]; 35 | } 36 | 37 | /// @notice Credits accumulated fees to a user's position 38 | /// @param self The individual position to update 39 | /// @param liquidityDelta The change in pool liquidity as a result of the position update 40 | /// @param feeGrowthInside0X128 The all-time fee growth in currency0, per unit of liquidity, inside the position's tick boundaries 41 | /// @param feeGrowthInside1X128 The all-time fee growth in currency1, per unit of liquidity, inside the position's tick boundaries 42 | /// @return feesOwed0 The amount of currency0 owed to the position owner 43 | /// @return feesOwed1 The amount of currency1 owed to the position owner 44 | function update( 45 | Info storage self, 46 | int128 liquidityDelta, 47 | uint256 feeGrowthInside0X128, 48 | uint256 feeGrowthInside1X128 49 | ) internal returns (uint256 feesOwed0, uint256 feesOwed1) { 50 | Info memory _self = self; 51 | 52 | uint128 liquidityNext; 53 | if (liquidityDelta == 0) { 54 | if (_self.liquidity == 0) revert CannotUpdateEmptyPosition(); // disallow pokes for 0 liquidity positions 55 | liquidityNext = _self.liquidity; 56 | } else { 57 | liquidityNext = liquidityDelta < 0 58 | ? _self.liquidity - uint128(-liquidityDelta) 59 | : _self.liquidity + uint128(liquidityDelta); 60 | } 61 | 62 | // calculate accumulated fees. overflow in the subtraction of fee growth is expected 63 | unchecked { 64 | feesOwed0 = FullMath.mulDiv( 65 | feeGrowthInside0X128 - _self.feeGrowthInside0LastX128, _self.liquidity, FixedPoint128.Q128 66 | ); 67 | feesOwed1 = FullMath.mulDiv( 68 | feeGrowthInside1X128 - _self.feeGrowthInside1LastX128, _self.liquidity, FixedPoint128.Q128 69 | ); 70 | } 71 | 72 | // update the position 73 | if (liquidityDelta != 0) self.liquidity = liquidityNext; 74 | self.feeGrowthInside0LastX128 = feeGrowthInside0X128; 75 | self.feeGrowthInside1LastX128 = feeGrowthInside1X128; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/foundry-tests/BitMath.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | import {BitMath} from "../../contracts/libraries/BitMath.sol"; 7 | 8 | contract TestBitMath is Test, GasSnapshot { 9 | function testMostSignificantBitZero() public { 10 | vm.expectRevert(); 11 | BitMath.mostSignificantBit(0); 12 | } 13 | 14 | function testMostSignificantBitOne() public { 15 | assertEq(BitMath.mostSignificantBit(1), 0); 16 | } 17 | 18 | function testMostSignificantBitTwo() public { 19 | assertEq(BitMath.mostSignificantBit(2), 1); 20 | } 21 | 22 | function testMostSignificantBitPowersOfTwo() public { 23 | for (uint256 i = 0; i < 255; i++) { 24 | uint256 x = 1 << i; 25 | assertEq(BitMath.mostSignificantBit(x), i); 26 | } 27 | } 28 | 29 | function testMostSignificantBitMaxUint256() public { 30 | assertEq(BitMath.mostSignificantBit(type(uint256).max), 255); 31 | } 32 | 33 | function testMostSignificantBit(uint256 x) public { 34 | vm.assume(x != 0); 35 | assertEq(BitMath.mostSignificantBit(x), mostSignificantBitReference(x)); 36 | } 37 | 38 | function testMsbGas() public { 39 | snapStart("BitMathMostSignificantBitSmallNumber"); 40 | BitMath.mostSignificantBit(3568); 41 | snapEnd(); 42 | 43 | snapStart("BitMathMostSignificantBitMaxUint128"); 44 | BitMath.mostSignificantBit(type(uint128).max); 45 | snapEnd(); 46 | 47 | snapStart("BitMathMostSignificantBitMaxUint256"); 48 | BitMath.mostSignificantBit(type(uint256).max); 49 | snapEnd(); 50 | } 51 | 52 | function testLeastSignificantBitZero() public { 53 | vm.expectRevert(); 54 | BitMath.leastSignificantBit(0); 55 | } 56 | 57 | function testLeastSignificantBitOne() public { 58 | assertEq(BitMath.leastSignificantBit(1), 0); 59 | } 60 | 61 | function testLeastSignificantBitTwo() public { 62 | assertEq(BitMath.leastSignificantBit(2), 1); 63 | } 64 | 65 | function testLeastSignificantBitPowersOfTwo() public { 66 | for (uint256 i = 0; i < 255; i++) { 67 | uint256 x = 1 << i; 68 | assertEq(BitMath.leastSignificantBit(x), i); 69 | } 70 | } 71 | 72 | function testLeastSignificantBitMaxUint256() public { 73 | assertEq(BitMath.leastSignificantBit(type(uint256).max), 0); 74 | } 75 | 76 | function testLeastSignificantBit(uint256 x) public { 77 | vm.assume(x != 0); 78 | assertEq(BitMath.leastSignificantBit(x), leastSignificantBitReference(x)); 79 | } 80 | 81 | function testLsbGas() public { 82 | snapStart("BitMathLeastSignificantBitSmallNumber"); 83 | BitMath.leastSignificantBit(3568); 84 | snapEnd(); 85 | 86 | snapStart("BitMathLeastSignificantBitMaxUint128"); 87 | BitMath.leastSignificantBit(type(uint128).max); 88 | snapEnd(); 89 | 90 | snapStart("BitMathLeastSignificantBitMaxUint256"); 91 | BitMath.leastSignificantBit(type(uint256).max); 92 | snapEnd(); 93 | } 94 | 95 | function mostSignificantBitReference(uint256 x) private pure returns (uint256) { 96 | uint256 i = 0; 97 | while ((x >>= 1) > 0) { 98 | ++i; 99 | } 100 | return i; 101 | } 102 | 103 | function leastSignificantBitReference(uint256 x) private pure returns (uint256) { 104 | require(x > 0, "BitMath: zero has no least significant bit"); 105 | 106 | uint256 i = 0; 107 | while ((x >> i) & 1 == 0) { 108 | ++i; 109 | } 110 | return i; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/__snapshots__/Oracle.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Oracle #grow gas for growing by 1 slot when index != cardinality - 1 1`] = ` 4 | Object { 5 | "calldataByteLength": 36, 6 | "gasUsed": 49072, 7 | } 8 | `; 9 | 10 | exports[`Oracle #grow gas for growing by 1 slot when index == cardinality - 1 1`] = ` 11 | Object { 12 | "calldataByteLength": 36, 13 | "gasUsed": 49072, 14 | } 15 | `; 16 | 17 | exports[`Oracle #grow gas for growing by 10 slots when index != cardinality - 1 1`] = ` 18 | Object { 19 | "calldataByteLength": 36, 20 | "gasUsed": 249214, 21 | } 22 | `; 23 | 24 | exports[`Oracle #grow gas for growing by 10 slots when index == cardinality - 1 1`] = ` 25 | Object { 26 | "calldataByteLength": 36, 27 | "gasUsed": 249214, 28 | } 29 | `; 30 | 31 | exports[`Oracle #initialize gas 1`] = ` 32 | Object { 33 | "calldataByteLength": 100, 34 | "gasUsed": 67690, 35 | } 36 | `; 37 | 38 | exports[`Oracle #observe before initialization gas for observe since most recent 1`] = `4615`; 39 | 40 | exports[`Oracle #observe before initialization gas for single observation at current time 1`] = `3525`; 41 | 42 | exports[`Oracle #observe before initialization gas for single observation at current time counterfactually computed 1`] = `4000`; 43 | 44 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 fetch many values 1`] = ` 45 | Object { 46 | "secondsPerLiquidityCumulativeX128s": Array [ 47 | "544451787073501541541399371890829138329", 48 | "799663562264205389138930327464655296921", 49 | "1045423049484883168306923099498710116305", 50 | "1423514568285925905488450441089563684590", 51 | "2152691068830794041481396028443352709138", 52 | "2347138135642758877746181518404363115684", 53 | "2395749902345750086812377890894615717321", 54 | ], 55 | "tickCumulatives": Array [ 56 | -13, 57 | -31, 58 | -43, 59 | -37, 60 | -15, 61 | 9, 62 | 15, 63 | ], 64 | } 65 | `; 66 | 67 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 gas all of last 20 seconds 1`] = `87289`; 68 | 69 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 gas between oldest and oldest + 1 1`] = `15571`; 70 | 71 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 gas latest equal 1`] = `3525`; 72 | 73 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 gas latest transform 1`] = `4000`; 74 | 75 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 gas middle 1`] = `13746`; 76 | 77 | exports[`Oracle #observe initialized with 5 observations with starting time of 5 gas oldest 1`] = `15277`; 78 | 79 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 fetch many values 1`] = ` 80 | Object { 81 | "secondsPerLiquidityCumulativeX128s": Array [ 82 | "544451787073501541541399371890829138329", 83 | "799663562264205389138930327464655296921", 84 | "1045423049484883168306923099498710116305", 85 | "1423514568285925905488450441089563684590", 86 | "2152691068830794041481396028443352709138", 87 | "2347138135642758877746181518404363115684", 88 | "2395749902345750086812377890894615717321", 89 | ], 90 | "tickCumulatives": Array [ 91 | -13, 92 | -31, 93 | -43, 94 | -37, 95 | -15, 96 | 9, 97 | 15, 98 | ], 99 | } 100 | `; 101 | 102 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 gas all of last 20 seconds 1`] = `87289`; 103 | 104 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 gas between oldest and oldest + 1 1`] = `15571`; 105 | 106 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 gas latest equal 1`] = `3525`; 107 | 108 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 gas latest transform 1`] = `4000`; 109 | 110 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 gas middle 1`] = `13746`; 111 | 112 | exports[`Oracle #observe initialized with 5 observations with starting time of 4294967291 gas oldest 1`] = `15277`; 113 | -------------------------------------------------------------------------------- /contracts/test/MockHooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Hooks} from "../libraries/Hooks.sol"; 5 | import {IHooks} from "../interfaces/IHooks.sol"; 6 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 7 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 8 | import {IHookFeeManager} from "../interfaces/IHookFeeManager.sol"; 9 | import {PoolId, PoolIdLibrary} from "../libraries/PoolId.sol"; 10 | 11 | contract MockHooks is IHooks, IHookFeeManager { 12 | using PoolIdLibrary for IPoolManager.PoolKey; 13 | using Hooks for IHooks; 14 | 15 | mapping(bytes4 => bytes4) public returnValues; 16 | 17 | mapping(PoolId => uint8) public swapFees; 18 | 19 | mapping(PoolId => uint8) public withdrawFees; 20 | 21 | function beforeInitialize(address, IPoolManager.PoolKey memory, uint160) external view override returns (bytes4) { 22 | bytes4 selector = MockHooks.beforeInitialize.selector; 23 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 24 | } 25 | 26 | function afterInitialize(address, IPoolManager.PoolKey memory, uint160, int24) 27 | external 28 | view 29 | override 30 | returns (bytes4) 31 | { 32 | bytes4 selector = MockHooks.afterInitialize.selector; 33 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 34 | } 35 | 36 | function beforeModifyPosition(address, IPoolManager.PoolKey calldata, IPoolManager.ModifyPositionParams calldata) 37 | external 38 | view 39 | override 40 | returns (bytes4) 41 | { 42 | bytes4 selector = MockHooks.beforeModifyPosition.selector; 43 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 44 | } 45 | 46 | function afterModifyPosition( 47 | address, 48 | IPoolManager.PoolKey calldata, 49 | IPoolManager.ModifyPositionParams calldata, 50 | BalanceDelta 51 | ) external view override returns (bytes4) { 52 | bytes4 selector = MockHooks.afterModifyPosition.selector; 53 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 54 | } 55 | 56 | function beforeSwap(address, IPoolManager.PoolKey calldata, IPoolManager.SwapParams calldata) 57 | external 58 | view 59 | override 60 | returns (bytes4) 61 | { 62 | bytes4 selector = MockHooks.beforeSwap.selector; 63 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 64 | } 65 | 66 | function afterSwap(address, IPoolManager.PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta) 67 | external 68 | view 69 | override 70 | returns (bytes4) 71 | { 72 | bytes4 selector = MockHooks.afterSwap.selector; 73 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 74 | } 75 | 76 | function beforeDonate(address, IPoolManager.PoolKey calldata, uint256, uint256) 77 | external 78 | view 79 | override 80 | returns (bytes4) 81 | { 82 | bytes4 selector = MockHooks.beforeDonate.selector; 83 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 84 | } 85 | 86 | function afterDonate(address, IPoolManager.PoolKey calldata, uint256, uint256) 87 | external 88 | view 89 | override 90 | returns (bytes4) 91 | { 92 | bytes4 selector = MockHooks.afterDonate.selector; 93 | return returnValues[selector] == bytes4(0) ? selector : returnValues[selector]; 94 | } 95 | 96 | function getHookSwapFee(IPoolManager.PoolKey calldata key) external view override returns (uint8) { 97 | return swapFees[key.toId()]; 98 | } 99 | 100 | function getHookWithdrawFee(IPoolManager.PoolKey calldata key) external view override returns (uint8) { 101 | return withdrawFees[key.toId()]; 102 | } 103 | 104 | function setReturnValue(bytes4 key, bytes4 value) external { 105 | returnValues[key] = value; 106 | } 107 | 108 | function setSwapFee(IPoolManager.PoolKey calldata key, uint8 value) external { 109 | swapFees[key.toId()] = value; 110 | } 111 | 112 | function setWithdrawFee(IPoolManager.PoolKey calldata key, uint8 value) external { 113 | withdrawFees[key.toId()] = value; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap v4 Core 2 | 3 | [![Lint](https://github.com/Uniswap/v4-core/actions/workflows/lint.yml/badge.svg)](https://github.com/Uniswap/v4-core/actions/workflows/lint.yml) 4 | [![Tests](https://github.com/Uniswap/v4-core/actions/workflows/tests.yml/badge.svg)](https://github.com/Uniswap/v4-core/actions/workflows/tests.yml) 5 | 6 | Uniswap v4 is a new automated market maker protocol that provides extensible and customizable pools. `v4-core` hosts the core pool logic for creating pools and executing pool actions like swapping and providing liquidity. 7 | 8 | The contracts in this repo are in early stages - we are releasing the draft code now so that v4 can be built in public, with open feedback and meaningful community contribution. We expect this will be a months-long process, and we appreciate any kind of contribution, no matter how small. 9 | 10 | ## Contributing 11 | 12 | If you’re interested in contributing please see our [contribution guidelines](./CONTRIBUTING.md)! 13 | 14 | ## Whitepaper 15 | 16 | A more detailed description of Uniswap v4 Core can be found in the draft of the [Uniswap v4 Core Whitepaper](./whitepaper-v4-draft.pdf). 17 | 18 | ## Architecture 19 | 20 | `v4-core` uses a singleton-style architecture, where all pool state is managed in the `PoolManager.sol` contract. Pool actions can be taken by acquiring a lock on the contract and implementing the `lockAcquired` callback to then proceed with any of the following actions on the pools: 21 | 22 | - `swap` 23 | - `modifyPosition` 24 | - `donate` 25 | - `take` 26 | - `settle` 27 | - `mint` 28 | 29 | Only the net balances owed to the pool (negative) or to the user (positive) are tracked throughout the duration of a lock. This is the `delta` field held in the lock state. Any number of actions can be run on the pools, as long as the deltas accumulated during the lock reach 0 by the lock’s release. This lock and call style architecture gives callers maximum flexibility in integrating with the core code. 30 | 31 | Additionally, a pool may be initialized with a hook contract, that can implement any of the following callbacks in the lifecycle of pool actions: 32 | 33 | - {before,after}Initialize 34 | - {before,after}ModifyPosition 35 | - {before,after}Swap 36 | - {before,after}Donate 37 | 38 | Hooks may also elect to specify fees on swaps, or liquidity withdrawal. Much like the actions above, fees are implemented using callback functions. 39 | 40 | The fee values, or callback logic, may be updated by the hooks dependent on their implementation. However _which_ callbacks are executed on a pool, including the type of fee or lack of fee, cannot change after pool initialization. 41 | 42 | ## Repository Structure 43 | 44 | All contracts are held within the `v4-core/contracts` folder. 45 | 46 | Note that helper contracts used by tests are held in the `v4-core/contracts/test` subfolder within the contracts folder. Any new test helper contracts should be added here, but all foundry tests are in the `v4-core/test/foundry-tests` folder. 47 | 48 | ```markdown 49 | contracts/ 50 | ----interfaces/ 51 | | IPoolManager.sol 52 | | ... 53 | ----libraries/ 54 | | Position.sol 55 | | Pool.sol 56 | | ... 57 | ----test 58 | ... 59 | PoolManager.sol 60 | test/ 61 | ----foundry-tests/ 62 | ``` 63 | 64 | ## Local deployment and Usage 65 | 66 | To utilize the contracts and deploy to a local testnet, you can install the code in your repo with forge: 67 | 68 | ```markdown 69 | forge install https://github.com/Uniswap/v4-core 70 | ``` 71 | 72 | To integrate with the contracts, the interfaces are available to use: 73 | 74 | ```solidity 75 | 76 | import {IPoolManager} from 'v4-core/contracts/interfaces/IPoolManager.sol'; 77 | import {ILockCallback} from 'v4-core/contracts/interfaces/callback/ILockCallback.sol'; 78 | 79 | contract MyContract is ILockCallback { 80 | IPoolManager poolManager; 81 | 82 | function doSomethingWithPools() { 83 | // this function will call `lockAcquired` below 84 | poolManager.lock(...); 85 | } 86 | 87 | function lockAcquired(uint256 id, bytes calldata data) external returns (bytes memory) { 88 | // perform pool actions 89 | poolManager.swap(...) 90 | } 91 | } 92 | 93 | ``` 94 | 95 | ## License 96 | 97 | The primary license for Uniswap V4 Core is the Business Source License 1.1 (`BUSL-1.1`), see [LICENSE](https://github.com/Uniswap/v4-core/blob/main/LICENSE). Minus the following exceptions: 98 | 99 | - [Interfaces](./contracts/interfaces) have a General Public License 100 | - Some [libraries](./contracts/libraries) and [types](./contracts/types/) have a General Public License 101 | - [FullMath.sol](./contracts/libraries/FullMath.sol) has an MIT License 102 | 103 | Each of these files states their license type. 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: Uniswap Labs 11 | 12 | Licensed Work: Uniswap V4 Core 13 | The Licensed Work is (c) 2023 Uniswap Labs 14 | 15 | Additional Use Grant: Any uses listed and defined at 16 | v4-core-license-grants.uniswap.eth 17 | 18 | Change Date: The earlier of 2027-06-15 or a date specified at 19 | v4-core-license-date.uniswap.eth 20 | 21 | Change License: GNU General Public License v2.0 or later 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | Terms 26 | 27 | The Licensor hereby grants you the right to copy, modify, create derivative 28 | works, redistribute, and make non-production use of the Licensed Work. The 29 | Licensor may make an Additional Use Grant, above, permitting limited 30 | production use. 31 | 32 | Effective on the Change Date, or the fourth anniversary of the first publicly 33 | available distribution of a specific version of the Licensed Work under this 34 | License, whichever comes first, the Licensor hereby grants you rights under 35 | the terms of the Change License, and the rights granted in the paragraph 36 | above terminate. 37 | 38 | If your use of the Licensed Work does not comply with the requirements 39 | currently in effect as described in this License, you must purchase a 40 | commercial license from the Licensor, its affiliated entities, or authorized 41 | resellers, or you must refrain from using the Licensed Work. 42 | 43 | All copies of the original and modified Licensed Work, and derivative works 44 | of the Licensed Work, are subject to this License. This License applies 45 | separately for each version of the Licensed Work and the Change Date may vary 46 | for each version of the Licensed Work released by Licensor. 47 | 48 | You must conspicuously display this License on each original or modified copy 49 | of the Licensed Work. If you receive the Licensed Work in original or 50 | modified form from a third party, the terms and conditions set forth in this 51 | License apply to your use of that work. 52 | 53 | Any use of the Licensed Work in violation of this License will automatically 54 | terminate your rights under this License for the current and all other 55 | versions of the Licensed Work. 56 | 57 | This License does not grant you any right in any trademark or logo of 58 | Licensor or its affiliates (provided that you may use a trademark or logo of 59 | Licensor as expressly required by this License). 60 | 61 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 62 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 63 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 65 | TITLE. 66 | 67 | MariaDB hereby grants you permission to use this License’s text to license 68 | your works, and to refer to it using the trademark "Business Source License", 69 | as long as you comply with the Covenants of Licensor below. 70 | 71 | ----------------------------------------------------------------------------- 72 | 73 | Covenants of Licensor 74 | 75 | In consideration of the right to use this License’s text and the "Business 76 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 77 | other recipients of the licensed work to be provided by Licensor: 78 | 79 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 80 | or a license that is compatible with GPL Version 2.0 or a later version, 81 | where "compatible" means that software provided under the Change License can 82 | be included in a program with software provided under GPL Version 2.0 or a 83 | later version. Licensor may specify additional Change Licenses without 84 | limitation. 85 | 86 | 2. To either: (a) specify an additional grant of rights to use that does not 87 | impose any additional restriction on the right granted in this License, as 88 | the Additional Use Grant; or (b) insert the text "None". 89 | 90 | 3. To specify a Change Date. 91 | 92 | 4. Not to modify this License in any other way. 93 | 94 | ----------------------------------------------------------------------------- 95 | 96 | Notice 97 | 98 | The Business Source License (this document, or the "License") is not an Open 99 | Source license. However, the Licensed Work will eventually be made available 100 | under an Open Source License, as stated in this License. -------------------------------------------------------------------------------- /contracts/interfaces/IHooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {IPoolManager} from "./IPoolManager.sol"; 5 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 6 | 7 | /// @notice The PoolManager contract decides whether to invoke specific hooks by inspecting the leading bits 8 | /// of the hooks contract address. For example, a 1 bit in the first bit of the address will 9 | /// cause the 'before swap' hook to be invoked. See the Hooks library for the full spec. 10 | /// @dev Should only be callable by the v4 PoolManager. 11 | interface IHooks { 12 | /// @notice The hook called before the state of a pool is initialized 13 | /// @param sender The initial msg.sender for the initialize call 14 | /// @param key The key for the pool being initialized 15 | /// @param sqrtPriceX96 The sqrt(price) of the pool as a Q64.96 16 | /// @return bytes4 The function selector for the hook 17 | function beforeInitialize(address sender, IPoolManager.PoolKey calldata key, uint160 sqrtPriceX96) 18 | external 19 | returns (bytes4); 20 | 21 | /// @notice The hook called after the state of a pool is initialized 22 | /// @param sender The initial msg.sender for the initialize call 23 | /// @param key The key for the pool being initialized 24 | /// @param sqrtPriceX96 The sqrt(price) of the pool as a Q64.96 25 | /// @param tick The current tick after the state of a pool is initialized 26 | /// @return bytes4 The function selector for the hook 27 | function afterInitialize(address sender, IPoolManager.PoolKey calldata key, uint160 sqrtPriceX96, int24 tick) 28 | external 29 | returns (bytes4); 30 | 31 | /// @notice The hook called before a position is modified 32 | /// @param sender The initial msg.sender for the modify position call 33 | /// @param key The key for the pool 34 | /// @param params The parameters for modifying the position 35 | /// @return bytes4 The function selector for the hook 36 | function beforeModifyPosition( 37 | address sender, 38 | IPoolManager.PoolKey calldata key, 39 | IPoolManager.ModifyPositionParams calldata params 40 | ) external returns (bytes4); 41 | 42 | /// @notice The hook called after a position is modified 43 | /// @param sender The initial msg.sender for the modify position call 44 | /// @param key The key for the pool 45 | /// @param params The parameters for modifying the position 46 | /// @return bytes4 The function selector for the hook 47 | function afterModifyPosition( 48 | address sender, 49 | IPoolManager.PoolKey calldata key, 50 | IPoolManager.ModifyPositionParams calldata params, 51 | BalanceDelta delta 52 | ) external returns (bytes4); 53 | 54 | /// @notice The hook called before a swap 55 | /// @param sender The initial msg.sender for the swap call 56 | /// @param key The key for the pool 57 | /// @param params The parameters for the swap 58 | /// @return bytes4 The function selector for the hook 59 | function beforeSwap(address sender, IPoolManager.PoolKey calldata key, IPoolManager.SwapParams calldata params) 60 | external 61 | returns (bytes4); 62 | 63 | /// @notice The hook called after a swap 64 | /// @param sender The initial msg.sender for the swap call 65 | /// @param key The key for the pool 66 | /// @param params The parameters for the swap 67 | /// @param delta The amount owed to the locker (positive) or owed to the pool (negative) 68 | /// @return bytes4 The function selector for the hook 69 | function afterSwap( 70 | address sender, 71 | IPoolManager.PoolKey calldata key, 72 | IPoolManager.SwapParams calldata params, 73 | BalanceDelta delta 74 | ) external returns (bytes4); 75 | 76 | /// @notice The hook called before donate 77 | /// @param sender The initial msg.sender for the donate call 78 | /// @param key The key for the pool 79 | /// @param amount0 The amount of token0 being donated 80 | /// @param amount1 The amount of token1 being donated 81 | /// @return bytes4 The function selector for the hook 82 | function beforeDonate(address sender, IPoolManager.PoolKey calldata key, uint256 amount0, uint256 amount1) 83 | external 84 | returns (bytes4); 85 | 86 | /// @notice The hook called after donate 87 | /// @param sender The initial msg.sender for the donate call 88 | /// @param key The key for the pool 89 | /// @param amount0 The amount of token0 being donated 90 | /// @param amount1 The amount of token1 being donated 91 | /// @return bytes4 The function selector for the hook 92 | function afterDonate(address sender, IPoolManager.PoolKey calldata key, uint256 amount0, uint256 amount1) 93 | external 94 | returns (bytes4); 95 | } 96 | -------------------------------------------------------------------------------- /contracts/libraries/TickBitmap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {BitMath} from "./BitMath.sol"; 5 | 6 | /// @title Packed tick initialized state library 7 | /// @notice Stores a packed mapping of tick index to its initialized state 8 | /// @dev The mapping uses int16 for keys since ticks are represented as int24 and there are 256 (2^8) values per word. 9 | library TickBitmap { 10 | /// @notice Thrown when the tick is not enumerated by the tick spacing 11 | /// @param tick the invalid tick 12 | /// @param tickSpacing The tick spacing of the pool 13 | error TickMisaligned(int24 tick, int24 tickSpacing); 14 | 15 | /// @notice Computes the position in the mapping where the initialized bit for a tick lives 16 | /// @param tick The tick for which to compute the position 17 | /// @return wordPos The key in the mapping containing the word in which the bit is stored 18 | /// @return bitPos The bit position in the word where the flag is stored 19 | function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { 20 | unchecked { 21 | wordPos = int16(tick >> 8); 22 | bitPos = uint8(int8(tick % 256)); 23 | } 24 | } 25 | 26 | /// @notice Flips the initialized state for a given tick from false to true, or vice versa 27 | /// @param self The mapping in which to flip the tick 28 | /// @param tick The tick to flip 29 | /// @param tickSpacing The spacing between usable ticks 30 | function flipTick(mapping(int16 => uint256) storage self, int24 tick, int24 tickSpacing) internal { 31 | unchecked { 32 | if (tick % tickSpacing != 0) revert TickMisaligned(tick, tickSpacing); // ensure that the tick is spaced 33 | (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing); 34 | uint256 mask = 1 << bitPos; 35 | self[wordPos] ^= mask; 36 | } 37 | } 38 | 39 | /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either 40 | /// to the left (less than or equal to) or right (greater than) of the given tick 41 | /// @param self The mapping in which to compute the next initialized tick 42 | /// @param tick The starting tick 43 | /// @param tickSpacing The spacing between usable ticks 44 | /// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) 45 | /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick 46 | /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks 47 | function nextInitializedTickWithinOneWord( 48 | mapping(int16 => uint256) storage self, 49 | int24 tick, 50 | int24 tickSpacing, 51 | bool lte 52 | ) internal view returns (int24 next, bool initialized) { 53 | unchecked { 54 | int24 compressed = tick / tickSpacing; 55 | if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity 56 | 57 | if (lte) { 58 | (int16 wordPos, uint8 bitPos) = position(compressed); 59 | // all the 1s at or to the right of the current bitPos 60 | uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); 61 | uint256 masked = self[wordPos] & mask; 62 | 63 | // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word 64 | initialized = masked != 0; 65 | // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 66 | next = initialized 67 | ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing 68 | : (compressed - int24(uint24(bitPos))) * tickSpacing; 69 | } else { 70 | // start from the word of the next tick, since the current tick state doesn't matter 71 | (int16 wordPos, uint8 bitPos) = position(compressed + 1); 72 | // all the 1s at or to the left of the bitPos 73 | uint256 mask = ~((1 << bitPos) - 1); 74 | uint256 masked = self[wordPos] & mask; 75 | 76 | // if there are no initialized ticks to the left of the current tick, return leftmost in the word 77 | initialized = masked != 0; 78 | // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 79 | next = initialized 80 | ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing 81 | : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /contracts/test/PoolSwapTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {CurrencyLibrary, Currency} from "../libraries/CurrencyLibrary.sol"; 5 | import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; 6 | 7 | import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; 8 | import {IPoolManager} from "../interfaces/IPoolManager.sol"; 9 | import {BalanceDelta} from "../types/BalanceDelta.sol"; 10 | 11 | contract PoolSwapTest is ILockCallback { 12 | using CurrencyLibrary for Currency; 13 | 14 | IPoolManager public immutable manager; 15 | 16 | constructor(IPoolManager _manager) { 17 | manager = _manager; 18 | } 19 | 20 | struct CallbackData { 21 | address sender; 22 | TestSettings testSettings; 23 | IPoolManager.PoolKey key; 24 | IPoolManager.SwapParams params; 25 | } 26 | 27 | struct TestSettings { 28 | bool withdrawTokens; 29 | bool settleUsingTransfer; 30 | } 31 | 32 | function swap( 33 | IPoolManager.PoolKey memory key, 34 | IPoolManager.SwapParams memory params, 35 | TestSettings memory testSettings 36 | ) external payable returns (BalanceDelta delta) { 37 | delta = 38 | abi.decode(manager.lock(abi.encode(CallbackData(msg.sender, testSettings, key, params))), (BalanceDelta)); 39 | 40 | uint256 ethBalance = address(this).balance; 41 | if (ethBalance > 0) { 42 | CurrencyLibrary.NATIVE.transfer(msg.sender, ethBalance); 43 | } 44 | } 45 | 46 | function lockAcquired(uint256, bytes calldata rawData) external returns (bytes memory) { 47 | require(msg.sender == address(manager)); 48 | 49 | CallbackData memory data = abi.decode(rawData, (CallbackData)); 50 | 51 | BalanceDelta delta = manager.swap(data.key, data.params); 52 | 53 | if (data.params.zeroForOne) { 54 | if (delta.amount0() > 0) { 55 | if (data.testSettings.settleUsingTransfer) { 56 | if (data.key.currency0.isNative()) { 57 | manager.settle{value: uint128(delta.amount0())}(data.key.currency0); 58 | } else { 59 | IERC20Minimal(Currency.unwrap(data.key.currency0)).transferFrom( 60 | data.sender, address(manager), uint128(delta.amount0()) 61 | ); 62 | manager.settle(data.key.currency0); 63 | } 64 | } else { 65 | // the received hook on this transfer will burn the tokens 66 | manager.safeTransferFrom( 67 | data.sender, 68 | address(manager), 69 | uint256(uint160(Currency.unwrap(data.key.currency0))), 70 | uint128(delta.amount0()), 71 | "" 72 | ); 73 | } 74 | } 75 | if (delta.amount1() < 0) { 76 | if (data.testSettings.withdrawTokens) { 77 | manager.take(data.key.currency1, data.sender, uint128(-delta.amount1())); 78 | } else { 79 | manager.mint(data.key.currency1, data.sender, uint128(-delta.amount1())); 80 | } 81 | } 82 | } else { 83 | if (delta.amount1() > 0) { 84 | if (data.testSettings.settleUsingTransfer) { 85 | if (data.key.currency1.isNative()) { 86 | manager.settle{value: uint128(delta.amount1())}(data.key.currency1); 87 | } else { 88 | IERC20Minimal(Currency.unwrap(data.key.currency1)).transferFrom( 89 | data.sender, address(manager), uint128(delta.amount1()) 90 | ); 91 | manager.settle(data.key.currency1); 92 | } 93 | } else { 94 | // the received hook on this transfer will burn the tokens 95 | manager.safeTransferFrom( 96 | data.sender, 97 | address(manager), 98 | uint256(uint160(Currency.unwrap(data.key.currency1))), 99 | uint128(delta.amount1()), 100 | "" 101 | ); 102 | } 103 | } 104 | if (delta.amount0() < 0) { 105 | if (data.testSettings.withdrawTokens) { 106 | manager.take(data.key.currency0, data.sender, uint128(-delta.amount0())); 107 | } else { 108 | manager.mint(data.key.currency0, data.sender, uint128(-delta.amount0())); 109 | } 110 | } 111 | } 112 | 113 | return abi.encode(delta); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /contracts/libraries/SwapMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {FullMath} from "./FullMath.sol"; 5 | import {SqrtPriceMath} from "./SqrtPriceMath.sol"; 6 | 7 | /// @title Computes the result of a swap within ticks 8 | /// @notice Contains methods for computing the result of a swap within a single tick price range, i.e., a single tick. 9 | library SwapMath { 10 | /// @notice Computes the result of swapping some amount in, or amount out, given the parameters of the swap 11 | /// @dev The fee, plus the amount in, will never exceed the amount remaining if the swap's `amountSpecified` is positive 12 | /// @param sqrtRatioCurrentX96 The current sqrt price of the pool 13 | /// @param sqrtRatioTargetX96 The price that cannot be exceeded, from which the direction of the swap is inferred 14 | /// @param liquidity The usable liquidity 15 | /// @param amountRemaining How much input or output amount is remaining to be swapped in/out 16 | /// @param feePips The fee taken from the input amount, expressed in hundredths of a bip 17 | /// @return sqrtRatioNextX96 The price after swapping the amount in/out, not to exceed the price target 18 | /// @return amountIn The amount to be swapped in, of either currency0 or currency1, based on the direction of the swap 19 | /// @return amountOut The amount to be received, of either currency0 or currency1, based on the direction of the swap 20 | /// @return feeAmount The amount of input that will be taken as a fee 21 | function computeSwapStep( 22 | uint160 sqrtRatioCurrentX96, 23 | uint160 sqrtRatioTargetX96, 24 | uint128 liquidity, 25 | int256 amountRemaining, 26 | uint24 feePips 27 | ) internal pure returns (uint160 sqrtRatioNextX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) { 28 | unchecked { 29 | bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96; 30 | bool exactIn = amountRemaining >= 0; 31 | 32 | if (exactIn) { 33 | uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6); 34 | amountIn = zeroForOne 35 | ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true) 36 | : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true); 37 | if (amountRemainingLessFee >= amountIn) { 38 | sqrtRatioNextX96 = sqrtRatioTargetX96; 39 | } else { 40 | sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( 41 | sqrtRatioCurrentX96, liquidity, amountRemainingLessFee, zeroForOne 42 | ); 43 | } 44 | } else { 45 | amountOut = zeroForOne 46 | ? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) 47 | : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false); 48 | if (uint256(-amountRemaining) >= amountOut) { 49 | sqrtRatioNextX96 = sqrtRatioTargetX96; 50 | } else { 51 | sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput( 52 | sqrtRatioCurrentX96, liquidity, uint256(-amountRemaining), zeroForOne 53 | ); 54 | } 55 | } 56 | 57 | bool max = sqrtRatioTargetX96 == sqrtRatioNextX96; 58 | 59 | // get the input/output amounts 60 | if (zeroForOne) { 61 | amountIn = max && exactIn 62 | ? amountIn 63 | : SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true); 64 | amountOut = max && !exactIn 65 | ? amountOut 66 | : SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false); 67 | } else { 68 | amountIn = max && exactIn 69 | ? amountIn 70 | : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true); 71 | amountOut = max && !exactIn 72 | ? amountOut 73 | : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false); 74 | } 75 | 76 | // cap the output amount to not exceed the remaining output amount 77 | if (!exactIn && amountOut > uint256(-amountRemaining)) { 78 | amountOut = uint256(-amountRemaining); 79 | } 80 | 81 | if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) { 82 | // we didn't reach the target, so take the remainder of the maximum input as fee 83 | feeAmount = uint256(amountRemaining) - amountIn; 84 | } else { 85 | feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /contracts/libraries/Hooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.19; 3 | 4 | import {IHooks} from "../interfaces/IHooks.sol"; 5 | import {Fees} from "../libraries/Fees.sol"; 6 | 7 | /// @notice V4 decides whether to invoke specific hooks by inspecting the leading bits of the address that 8 | /// the hooks contract is deployed to. 9 | /// For example, a hooks contract deployed to address: 0x9000000000000000000000000000000000000000 10 | /// has leading bits '1001' which would cause the 'before initialize' and 'after modify position' hooks to be used. 11 | library Hooks { 12 | using Fees for uint24; 13 | 14 | uint256 internal constant BEFORE_INITIALIZE_FLAG = 1 << 159; 15 | uint256 internal constant AFTER_INITIALIZE_FLAG = 1 << 158; 16 | uint256 internal constant BEFORE_MODIFY_POSITION_FLAG = 1 << 157; 17 | uint256 internal constant AFTER_MODIFY_POSITION_FLAG = 1 << 156; 18 | uint256 internal constant BEFORE_SWAP_FLAG = 1 << 155; 19 | uint256 internal constant AFTER_SWAP_FLAG = 1 << 154; 20 | uint256 internal constant BEFORE_DONATE_FLAG = 1 << 153; 21 | uint256 internal constant AFTER_DONATE_FLAG = 1 << 152; 22 | 23 | struct Calls { 24 | bool beforeInitialize; 25 | bool afterInitialize; 26 | bool beforeModifyPosition; 27 | bool afterModifyPosition; 28 | bool beforeSwap; 29 | bool afterSwap; 30 | bool beforeDonate; 31 | bool afterDonate; 32 | } 33 | 34 | /// @notice Thrown if the address will not lead to the specified hook calls being called 35 | /// @param hooks The address of the hooks contract 36 | error HookAddressNotValid(address hooks); 37 | 38 | /// @notice Hook did not return its selector 39 | error InvalidHookResponse(); 40 | 41 | /// @notice Utility function intended to be used in hook constructors to ensure 42 | /// the deployed hooks address causes the intended hooks to be called 43 | /// @param calls The hooks that are intended to be called 44 | /// @dev calls param is memory as the function will be called from constructors 45 | function validateHookAddress(IHooks self, Calls memory calls) internal pure { 46 | if ( 47 | calls.beforeInitialize != shouldCallBeforeInitialize(self) 48 | || calls.afterInitialize != shouldCallAfterInitialize(self) 49 | || calls.beforeModifyPosition != shouldCallBeforeModifyPosition(self) 50 | || calls.afterModifyPosition != shouldCallAfterModifyPosition(self) 51 | || calls.beforeSwap != shouldCallBeforeSwap(self) || calls.afterSwap != shouldCallAfterSwap(self) 52 | || calls.beforeDonate != shouldCallBeforeDonate(self) || calls.afterDonate != shouldCallAfterDonate(self) 53 | ) { 54 | revert HookAddressNotValid(address(self)); 55 | } 56 | } 57 | 58 | /// @notice Ensures that the hook address includes at least one hook flag or dynamic fees, or is the 0 address 59 | /// @param hook The hook to verify 60 | function isValidHookAddress(IHooks hook, uint24 fee) internal pure returns (bool) { 61 | // If there is no hook contract set, then fee cannot be dynamic and there cannot be a hook fee on swap or withdrawal. 62 | return address(hook) == address(0) 63 | ? !fee.isDynamicFee() && !fee.hasHookSwapFee() && !fee.hasHookWithdrawFee() 64 | : ( 65 | uint160(address(hook)) >= AFTER_DONATE_FLAG || fee.isDynamicFee() || fee.hasHookSwapFee() 66 | || fee.hasHookWithdrawFee() 67 | ); 68 | } 69 | 70 | function shouldCallBeforeInitialize(IHooks self) internal pure returns (bool) { 71 | return uint256(uint160(address(self))) & BEFORE_INITIALIZE_FLAG != 0; 72 | } 73 | 74 | function shouldCallAfterInitialize(IHooks self) internal pure returns (bool) { 75 | return uint256(uint160(address(self))) & AFTER_INITIALIZE_FLAG != 0; 76 | } 77 | 78 | function shouldCallBeforeModifyPosition(IHooks self) internal pure returns (bool) { 79 | return uint256(uint160(address(self))) & BEFORE_MODIFY_POSITION_FLAG != 0; 80 | } 81 | 82 | function shouldCallAfterModifyPosition(IHooks self) internal pure returns (bool) { 83 | return uint256(uint160(address(self))) & AFTER_MODIFY_POSITION_FLAG != 0; 84 | } 85 | 86 | function shouldCallBeforeSwap(IHooks self) internal pure returns (bool) { 87 | return uint256(uint160(address(self))) & BEFORE_SWAP_FLAG != 0; 88 | } 89 | 90 | function shouldCallAfterSwap(IHooks self) internal pure returns (bool) { 91 | return uint256(uint160(address(self))) & AFTER_SWAP_FLAG != 0; 92 | } 93 | 94 | function shouldCallBeforeDonate(IHooks self) internal pure returns (bool) { 95 | return uint256(uint160(address(self))) & BEFORE_DONATE_FLAG != 0; 96 | } 97 | 98 | function shouldCallAfterDonate(IHooks self) internal pure returns (bool) { 99 | return uint256(uint160(address(self))) & AFTER_DONATE_FLAG != 0; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/foundry-tests/Pool.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.19; 2 | 3 | import {Test} from "forge-std/Test.sol"; 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {Pool} from "../../contracts/libraries/Pool.sol"; 6 | import {PoolManager} from "../../contracts/PoolManager.sol"; 7 | import {Position} from "../../contracts/libraries/Position.sol"; 8 | import {TickMath} from "../../contracts/libraries/TickMath.sol"; 9 | import {TickBitmap} from "../../contracts/libraries/TickBitmap.sol"; 10 | 11 | contract PoolTest is Test { 12 | using Pool for Pool.State; 13 | 14 | Pool.State state; 15 | 16 | function testPoolInitialize(uint160 sqrtPriceX96, uint8 protocolFee, uint8 hookFee) public { 17 | if (sqrtPriceX96 < TickMath.MIN_SQRT_RATIO || sqrtPriceX96 >= TickMath.MAX_SQRT_RATIO) { 18 | vm.expectRevert(TickMath.InvalidSqrtRatio.selector); 19 | state.initialize(sqrtPriceX96, protocolFee, hookFee, protocolFee, hookFee); 20 | } else { 21 | state.initialize(sqrtPriceX96, protocolFee, hookFee, protocolFee, hookFee); 22 | assertEq(state.slot0.sqrtPriceX96, sqrtPriceX96); 23 | assertEq(state.slot0.protocolSwapFee, protocolFee); 24 | assertEq(state.slot0.tick, TickMath.getTickAtSqrtRatio(sqrtPriceX96)); 25 | assertLt(state.slot0.tick, TickMath.MAX_TICK); 26 | assertGt(state.slot0.tick, TickMath.MIN_TICK - 1); 27 | } 28 | } 29 | 30 | function testModifyPosition(uint160 sqrtPriceX96, Pool.ModifyPositionParams memory params) public { 31 | // Assumptions tested in PoolManager.t.sol 32 | vm.assume(params.tickSpacing > 0); 33 | vm.assume(params.tickSpacing < 32768); 34 | 35 | testPoolInitialize(sqrtPriceX96, 0, 0); 36 | 37 | if (params.tickLower >= params.tickUpper) { 38 | vm.expectRevert(abi.encodeWithSelector(Pool.TicksMisordered.selector, params.tickLower, params.tickUpper)); 39 | } else if (params.tickLower < TickMath.MIN_TICK) { 40 | vm.expectRevert(abi.encodeWithSelector(Pool.TickLowerOutOfBounds.selector, params.tickLower)); 41 | } else if (params.tickUpper > TickMath.MAX_TICK) { 42 | vm.expectRevert(abi.encodeWithSelector(Pool.TickUpperOutOfBounds.selector, params.tickUpper)); 43 | } else if (params.liquidityDelta < 0) { 44 | vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); 45 | } else if (params.liquidityDelta == 0) { 46 | vm.expectRevert(Position.CannotUpdateEmptyPosition.selector); 47 | } else if (params.liquidityDelta > int128(Pool.tickSpacingToMaxLiquidityPerTick(params.tickSpacing))) { 48 | vm.expectRevert(abi.encodeWithSelector(Pool.TickLiquidityOverflow.selector, params.tickLower)); 49 | } else if (params.tickLower % params.tickSpacing != 0) { 50 | vm.expectRevert( 51 | abi.encodeWithSelector(TickBitmap.TickMisaligned.selector, params.tickLower, params.tickSpacing) 52 | ); 53 | } else if (params.tickUpper % params.tickSpacing != 0) { 54 | vm.expectRevert( 55 | abi.encodeWithSelector(TickBitmap.TickMisaligned.selector, params.tickUpper, params.tickSpacing) 56 | ); 57 | } 58 | 59 | params.owner = address(this); 60 | state.modifyPosition(params); 61 | } 62 | 63 | function testSwap(uint160 sqrtPriceX96, Pool.SwapParams memory params) public { 64 | // Assumptions tested in PoolManager.t.sol 65 | vm.assume(params.tickSpacing > 0); 66 | vm.assume(params.tickSpacing < 32768); 67 | vm.assume(params.fee < 1000000); 68 | 69 | testPoolInitialize(sqrtPriceX96, 0, 0); 70 | Pool.Slot0 memory slot0 = state.slot0; 71 | 72 | if (params.amountSpecified == 0) { 73 | vm.expectRevert(Pool.SwapAmountCannotBeZero.selector); 74 | } else if (params.zeroForOne) { 75 | if (params.sqrtPriceLimitX96 >= slot0.sqrtPriceX96) { 76 | vm.expectRevert( 77 | abi.encodeWithSelector( 78 | Pool.PriceLimitAlreadyExceeded.selector, slot0.sqrtPriceX96, params.sqrtPriceLimitX96 79 | ) 80 | ); 81 | } else if (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) { 82 | vm.expectRevert(abi.encodeWithSelector(Pool.PriceLimitOutOfBounds.selector, params.sqrtPriceLimitX96)); 83 | } 84 | } else if (!params.zeroForOne) { 85 | if (params.sqrtPriceLimitX96 <= slot0.sqrtPriceX96) { 86 | vm.expectRevert( 87 | abi.encodeWithSelector( 88 | Pool.PriceLimitAlreadyExceeded.selector, slot0.sqrtPriceX96, params.sqrtPriceLimitX96 89 | ) 90 | ); 91 | } else if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_RATIO) { 92 | vm.expectRevert(abi.encodeWithSelector(Pool.PriceLimitOutOfBounds.selector, params.sqrtPriceLimitX96)); 93 | } 94 | } 95 | 96 | state.swap(params); 97 | 98 | if (params.zeroForOne) { 99 | assertLe(state.slot0.sqrtPriceX96, params.sqrtPriceLimitX96); 100 | } else { 101 | assertGe(state.slot0.sqrtPriceX96, params.sqrtPriceLimitX96); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thanks for your interest in contributing to v4 of the Uniswap Protocol! The contracts in this repo are in early stages - we are releasing the draft code now so that v4 can be built in public, with open feedback and meaningful community contribution. We expect this will be a months-long process, and we appreciate any kind of contribution, no matter how small. 4 | 5 | If you need to get in contact with the repository maintainers, please reach out in our [Discord](https://discord.com/invite/FCfyBSbCU5). 6 | 7 | ## Types of Contributing 8 | 9 | There are many ways to contribute, but here are a few if you want a place to start: 10 | 11 | 1. **Opening an issue.** Before opening an issue, please check that there is not an issue already open. If there is, feel free to comment more details, explanations, or examples within the open issue rather than duplicating it. Suggesting changes to the open development process are within the bounds of opening issues. We are always open to feedback and receptive to suggestions! 12 | 2. **Resolving an issue.** You can resolve an issue either by showing that it is not an issue or by fixing the issue with code changes, additional tests, etc. Any pull request fixing an issue should reference that issue. 13 | 3. **Reviewing open PRs.** You can provide comments, standards guidance, naming suggestions, gas optimizations, or ideas for alternative designs on any open pull request. 14 | 15 | ## Opening an Issue 16 | 17 | When opening an [issue](https://github.com/Uniswap/v4-core/issues/new/choose), choose a template to start from: Bug Report or Feature Improvement. For bug reports, you should be able to reproduce the bug through tests or proof of concept implementations. For feature improvements, please title it with a concise problem statement and check that a similar request is not already open or already in progress. Not all issues may be deemed worth resolving, so please follow through with responding to any questions or comments that others may have regarding the issue. 18 | 19 | Feel free to tag the issue as a “good first issue” for any clean-up related issues, or small scoped changes to help encourage pull requests from first time contributors! 20 | 21 | ## Opening a Pull Request 22 | 23 | All pull requests should be opened against the `main` branch. In the pull request, please reference the issue you are fixing. 24 | 25 | Pull requests can be reviewed by community members, but to be merged they will need approval from the repository maintainers. Please understand it will take time to receive a response, although the maintainers will aim to respond and comment as soon as possible. 26 | 27 | **For larger, more substantial changes to the code, it is best to open an issue and start a discussion with the maintainers to align on the change before spending time on the development.** 28 | 29 | Finally, before opening a pull request please do the following: 30 | 31 | - Check that the code style follows the [standards](#standards). 32 | - Run the tests and snapshots. Commands are outlined in the [tests](#tests) section. 33 | - Document any new functions, structs, or interfaces following the natspec standard. 34 | - Add tests! For smaller contributions, they should be tested with unit tests, and fuzz tests where possible. For bigger contributions, they should be tested with integration tests and invariant tests where possible. 35 | - Make sure all commits are [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) 36 | 37 | ## Standards 38 | 39 | All contributions must follow the below standards. Maintainers will close out PRs that do not adhere to these standards. 40 | 41 | 1. All contracts should be formatted with the default forge fmt config. Run `forge fmt`. 42 | 2. These contracts follow the [solidity style guide](https://docs.soliditylang.org/en/v0.8.17/style-guide.html) with one minor exception of using the _prependUnderscore style naming for internal contract functions, internal top-level parameters, and function parameters with naming collisions. 43 | 3. All external facing contracts should inherit from interfaces, which specify and document its functions with natspec. 44 | 4. Picking up stale issues by other authors is fine! Please just communicate with them ahead of time and it is best practice to include co-authors in any commits. 45 | 5. Squash commits where possible to make reviews clean and efficient. PRs that are merged to main will be squashed into 1 commit. 46 | 47 | ## Setup 48 | 49 | `yarn install` to install dependencies for hardhat 50 | 51 | `yarn compile` to compile contracts for hardhat 52 | 53 | `forge build` to get contract artifacts and dependencies for forge 54 | 55 | ## Tests 56 | 57 | This repo currently uses hardhat and forge tests. Please run both test suites before opening a PR. 58 | 59 | `yarn snapshots` to update the hardhat gas snapshots 60 | 61 | `yarn test` to run hardhat tests 62 | 63 | `yarn prettier` to run the formatter (runs both typescript and solidity formatting) 64 | 65 | `forge snapshot`to update the forge gas snapshots 66 | 67 | `forge test` to run forge tests 68 | 69 | Any new tests that you add should be written with forge, as the repo is undergoing a full migration to the forge test suite. 70 | 71 | ## Code of Conduct 72 | 73 | Above all else, please be respectful of the people behind the code. Any kind of aggressive or disrespectful comments, issues, and language will be removed. 74 | 75 | Issues and PRs that are obviously spam and unhelpful to the development process or unrelated to the core code will also be closed. 76 | -------------------------------------------------------------------------------- /test/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import bn from 'bignumber.js' 2 | import { BigNumber, BigNumberish, Contract, ContractTransaction, utils, Wallet } from 'ethers' 3 | import { ethers } from 'hardhat' 4 | 5 | export const MaxUint128 = BigNumber.from(2).pow(128).sub(1) 6 | 7 | export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing 8 | export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing 9 | 10 | export const MIN_SQRT_RATIO = BigNumber.from('4295128739') 11 | export const MAX_SQRT_RATIO = BigNumber.from('1461446703485210103287273052203988822378723970342') 12 | 13 | export enum FeeAmount { 14 | LOW = 500, 15 | MEDIUM = 3000, 16 | HIGH = 10000, 17 | } 18 | 19 | export const TICK_SPACINGS: { [amount in FeeAmount]: number } = { 20 | [FeeAmount.LOW]: 10, 21 | [FeeAmount.MEDIUM]: 60, 22 | [FeeAmount.HIGH]: 200, 23 | } 24 | 25 | export function expandTo18Decimals(n: number): BigNumber { 26 | return BigNumber.from(n).mul(BigNumber.from(10).pow(18)) 27 | } 28 | 29 | bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }) 30 | 31 | // returns the sqrt price as a 64x96 32 | export function encodeSqrtPriceX96(reserve1: BigNumberish, reserve0: BigNumberish): BigNumber { 33 | return BigNumber.from( 34 | new bn(reserve1.toString()) 35 | .div(reserve0.toString()) 36 | .sqrt() 37 | .multipliedBy(new bn(2).pow(96)) 38 | .integerValue(3) 39 | .toString() 40 | ) 41 | } 42 | 43 | export function getPositionKey(address: string, lowerTick: number, upperTick: number): string { 44 | return utils.keccak256(utils.solidityPack(['address', 'int24', 'int24'], [address, lowerTick, upperTick])) 45 | } 46 | 47 | export function getPoolId({ 48 | currency0, 49 | currency1, 50 | fee, 51 | tickSpacing, 52 | hooks, 53 | }: { 54 | currency0: string | Contract 55 | currency1: string | Contract 56 | fee: number 57 | tickSpacing: number 58 | hooks: string | Contract 59 | }): string { 60 | return utils.keccak256( 61 | utils.defaultAbiCoder.encode( 62 | ['address', 'address', 'uint24', 'int24', 'address'], 63 | [ 64 | typeof currency0 === 'string' ? currency0 : currency0.address, 65 | typeof currency1 === 'string' ? currency1 : currency1.address, 66 | fee, 67 | tickSpacing, 68 | typeof hooks === 'string' ? hooks : hooks.address, 69 | ] 70 | ) 71 | ) 72 | } 73 | 74 | export type SwapFunction = ( 75 | amount: BigNumberish, 76 | to: Wallet | string, 77 | sqrtPriceLimitX96?: BigNumberish 78 | ) => Promise 79 | export type SwapToPriceFunction = (sqrtPriceX96: BigNumberish, to: Wallet | string) => Promise 80 | export type ModifyPositionFunction = ( 81 | tickLower: BigNumberish, 82 | tickUpper: BigNumberish, 83 | liquidityDelta: BigNumberish 84 | ) => Promise 85 | export type DonateFunction = (amount0: BigNumberish, amount1: BigNumberish) => Promise 86 | 87 | interface HookMask { 88 | beforeInitialize: boolean 89 | afterInitialize: boolean 90 | beforeModifyPosition: boolean 91 | afterModifyPosition: boolean 92 | beforeSwap: boolean 93 | afterSwap: boolean 94 | beforeDonate: boolean 95 | afterDonate: boolean 96 | } 97 | 98 | /** 99 | * Creates a 20 byte mask for the given hook configuration 100 | */ 101 | export function createHookMask({ 102 | beforeInitialize, 103 | afterInitialize, 104 | beforeModifyPosition, 105 | afterModifyPosition, 106 | beforeSwap, 107 | afterSwap, 108 | beforeDonate, 109 | afterDonate, 110 | }: HookMask): string { 111 | let result: BigNumber = BigNumber.from(0) 112 | if (beforeInitialize) result = result.add(BigNumber.from(1).shl(159)) 113 | if (afterInitialize) result = result.add(BigNumber.from(1).shl(158)) 114 | if (beforeModifyPosition) result = result.add(BigNumber.from(1).shl(157)) 115 | if (afterModifyPosition) result = result.add(BigNumber.from(1).shl(156)) 116 | if (beforeSwap) result = result.add(BigNumber.from(1).shl(155)) 117 | if (afterSwap) result = result.add(BigNumber.from(1).shl(154)) 118 | if (beforeDonate) result = result.add(BigNumber.from(1).shl(153)) 119 | if (afterDonate) result = result.add(BigNumber.from(1).shl(152)) 120 | return utils.hexZeroPad(result.toHexString(), 20) 121 | } 122 | 123 | /** 124 | * Returns a wallet whose first transaction will create a contract satisfying the leading 125 | * bytes required by hookMask. If provided, the mnemonic argument short-circuits our search 126 | * to save time. 127 | */ 128 | export function getWalletForDeployingHookMask(hookMask: HookMask, mnemonic?: string): [Wallet, string] { 129 | const startingString = createHookMask(hookMask).slice(0, 4) 130 | let wallet: Wallet = mnemonic ? ethers.Wallet.fromMnemonic(mnemonic) : ethers.Wallet.createRandom() 131 | let contractAddress: string | undefined 132 | 133 | while (contractAddress === undefined) { 134 | const prospectiveContractAddress = utils.getContractAddress({ from: wallet.address, nonce: 0 }) 135 | if (prospectiveContractAddress.slice(0, 4).toLowerCase() === startingString) { 136 | contractAddress = prospectiveContractAddress 137 | } else { 138 | // if, for whatever reason, we generate a bad address but a mnemonic was provided, 139 | // it's stale and we surface the error 140 | if (mnemonic) throw Error('Stale mnemonic') 141 | wallet = ethers.Wallet.createRandom() 142 | } 143 | } 144 | 145 | return [wallet, contractAddress] 146 | } 147 | -------------------------------------------------------------------------------- /contracts/libraries/FullMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title Contains 512-bit math functions 5 | /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision 6 | /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits 7 | library FullMath { 8 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 9 | /// @param a The multiplicand 10 | /// @param b The multiplier 11 | /// @param denominator The divisor 12 | /// @return result The 256-bit result 13 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 14 | function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { 15 | unchecked { 16 | // 512-bit multiply [prod1 prod0] = a * b 17 | // Compute the product mod 2**256 and mod 2**256 - 1 18 | // then use the Chinese Remainder Theorem to reconstruct 19 | // the 512 bit result. The result is stored in two 256 20 | // variables such that product = prod1 * 2**256 + prod0 21 | uint256 prod0; // Least significant 256 bits of the product 22 | uint256 prod1; // Most significant 256 bits of the product 23 | assembly { 24 | let mm := mulmod(a, b, not(0)) 25 | prod0 := mul(a, b) 26 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 27 | } 28 | 29 | // Handle non-overflow cases, 256 by 256 division 30 | if (prod1 == 0) { 31 | require(denominator > 0); 32 | assembly { 33 | result := div(prod0, denominator) 34 | } 35 | return result; 36 | } 37 | 38 | // Make sure the result is less than 2**256. 39 | // Also prevents denominator == 0 40 | require(denominator > prod1); 41 | 42 | /////////////////////////////////////////////// 43 | // 512 by 256 division. 44 | /////////////////////////////////////////////// 45 | 46 | // Make division exact by subtracting the remainder from [prod1 prod0] 47 | // Compute remainder using mulmod 48 | uint256 remainder; 49 | assembly { 50 | remainder := mulmod(a, b, denominator) 51 | } 52 | // Subtract 256 bit number from 512 bit number 53 | assembly { 54 | prod1 := sub(prod1, gt(remainder, prod0)) 55 | prod0 := sub(prod0, remainder) 56 | } 57 | 58 | // Factor powers of two out of denominator 59 | // Compute largest power of two divisor of denominator. 60 | // Always >= 1. 61 | uint256 twos = (0 - denominator) & denominator; 62 | // Divide denominator by power of two 63 | assembly { 64 | denominator := div(denominator, twos) 65 | } 66 | 67 | // Divide [prod1 prod0] by the factors of two 68 | assembly { 69 | prod0 := div(prod0, twos) 70 | } 71 | // Shift in bits from prod1 into prod0. For this we need 72 | // to flip `twos` such that it is 2**256 / twos. 73 | // If twos is zero, then it becomes one 74 | assembly { 75 | twos := add(div(sub(0, twos), twos), 1) 76 | } 77 | prod0 |= prod1 * twos; 78 | 79 | // Invert denominator mod 2**256 80 | // Now that denominator is an odd number, it has an inverse 81 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 82 | // Compute the inverse by starting with a seed that is correct 83 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 84 | uint256 inv = (3 * denominator) ^ 2; 85 | // Now use Newton-Raphson iteration to improve the precision. 86 | // Thanks to Hensel's lifting lemma, this also works in modular 87 | // arithmetic, doubling the correct bits in each step. 88 | inv *= 2 - denominator * inv; // inverse mod 2**8 89 | inv *= 2 - denominator * inv; // inverse mod 2**16 90 | inv *= 2 - denominator * inv; // inverse mod 2**32 91 | inv *= 2 - denominator * inv; // inverse mod 2**64 92 | inv *= 2 - denominator * inv; // inverse mod 2**128 93 | inv *= 2 - denominator * inv; // inverse mod 2**256 94 | 95 | // Because the division is now exact we can divide by multiplying 96 | // with the modular inverse of denominator. This will give us the 97 | // correct result modulo 2**256. Since the precoditions guarantee 98 | // that the outcome is less than 2**256, this is the final result. 99 | // We don't need to compute the high bits of the result and prod1 100 | // is no longer required. 101 | result = prod0 * inv; 102 | return result; 103 | } 104 | } 105 | 106 | /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 107 | /// @param a The multiplicand 108 | /// @param b The multiplier 109 | /// @param denominator The divisor 110 | /// @return result The 256-bit result 111 | function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { 112 | unchecked { 113 | result = mulDiv(a, b, denominator); 114 | if (mulmod(a, b, denominator) > 0) { 115 | require(result < type(uint256).max); 116 | result++; 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/FullMath.spec.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { FullMathTest } from '../typechain/FullMathTest' 3 | import { expect } from './shared/expect' 4 | import { Decimal } from 'decimal.js' 5 | 6 | const { 7 | BigNumber, 8 | constants: { MaxUint256 }, 9 | } = ethers 10 | const Q128 = BigNumber.from(2).pow(128) 11 | 12 | Decimal.config({ toExpNeg: -500, toExpPos: 500 }) 13 | 14 | describe('FullMath', () => { 15 | let fullMath: FullMathTest 16 | before('deploy FullMathTest', async () => { 17 | const factory = await ethers.getContractFactory('FullMathTest') 18 | fullMath = (await factory.deploy()) as FullMathTest 19 | }) 20 | 21 | describe('#mulDiv', () => { 22 | it('reverts if denominator is 0', async () => { 23 | await expect(fullMath.mulDiv(Q128, 5, 0)).to.be.reverted 24 | }) 25 | it('reverts if denominator is 0 and numerator overflows', async () => { 26 | await expect(fullMath.mulDiv(Q128, Q128, 0)).to.be.reverted 27 | }) 28 | it('reverts if output overflows uint256', async () => { 29 | await expect(fullMath.mulDiv(Q128, Q128, 1)).to.be.reverted 30 | }) 31 | it('reverts if output overflows uint256', async () => { 32 | await expect(fullMath.mulDiv(Q128, Q128, 1)).to.be.reverted 33 | }) 34 | it('reverts on overflow with all max inputs', async () => { 35 | await expect(fullMath.mulDiv(MaxUint256, MaxUint256, MaxUint256.sub(1))).to.be.reverted 36 | }) 37 | 38 | it('all max inputs', async () => { 39 | expect(await fullMath.mulDiv(MaxUint256, MaxUint256, MaxUint256)).to.eq(MaxUint256) 40 | }) 41 | 42 | it('accurate without phantom overflow', async () => { 43 | const result = Q128.div(3) 44 | expect( 45 | await fullMath.mulDiv( 46 | Q128, 47 | /*0.5=*/ BigNumber.from(50).mul(Q128).div(100), 48 | /*1.5=*/ BigNumber.from(150).mul(Q128).div(100) 49 | ) 50 | ).to.eq(result) 51 | }) 52 | 53 | it('accurate with phantom overflow', async () => { 54 | const result = BigNumber.from(4375).mul(Q128).div(1000) 55 | expect(await fullMath.mulDiv(Q128, BigNumber.from(35).mul(Q128), BigNumber.from(8).mul(Q128))).to.eq(result) 56 | }) 57 | 58 | it('accurate with phantom overflow and repeating decimal', async () => { 59 | const result = BigNumber.from(1).mul(Q128).div(3) 60 | expect(await fullMath.mulDiv(Q128, BigNumber.from(1000).mul(Q128), BigNumber.from(3000).mul(Q128))).to.eq(result) 61 | }) 62 | }) 63 | 64 | describe('#mulDivRoundingUp', () => { 65 | it('reverts if denominator is 0', async () => { 66 | await expect(fullMath.mulDivRoundingUp(Q128, 5, 0)).to.be.reverted 67 | }) 68 | it('reverts if denominator is 0 and numerator overflows', async () => { 69 | await expect(fullMath.mulDivRoundingUp(Q128, Q128, 0)).to.be.reverted 70 | }) 71 | it('reverts if output overflows uint256', async () => { 72 | await expect(fullMath.mulDivRoundingUp(Q128, Q128, 1)).to.be.reverted 73 | }) 74 | it('reverts on overflow with all max inputs', async () => { 75 | await expect(fullMath.mulDivRoundingUp(MaxUint256, MaxUint256, MaxUint256.sub(1))).to.be.reverted 76 | }) 77 | 78 | it('reverts if mulDiv overflows 256 bits after rounding up', async () => { 79 | await expect( 80 | fullMath.mulDivRoundingUp( 81 | '535006138814359', 82 | '432862656469423142931042426214547535783388063929571229938474969', 83 | '2' 84 | ) 85 | ).to.be.reverted 86 | }) 87 | 88 | it('reverts if mulDiv overflows 256 bits after rounding up case 2', async () => { 89 | await expect( 90 | fullMath.mulDivRoundingUp( 91 | '115792089237316195423570985008687907853269984659341747863450311749907997002549', 92 | '115792089237316195423570985008687907853269984659341747863450311749907997002550', 93 | '115792089237316195423570985008687907853269984653042931687443039491902864365164' 94 | ) 95 | ).to.be.reverted 96 | }) 97 | 98 | it('all max inputs', async () => { 99 | expect(await fullMath.mulDivRoundingUp(MaxUint256, MaxUint256, MaxUint256)).to.eq(MaxUint256) 100 | }) 101 | 102 | it('accurate without phantom overflow', async () => { 103 | const result = Q128.div(3).add(1) 104 | expect( 105 | await fullMath.mulDivRoundingUp( 106 | Q128, 107 | /*0.5=*/ BigNumber.from(50).mul(Q128).div(100), 108 | /*1.5=*/ BigNumber.from(150).mul(Q128).div(100) 109 | ) 110 | ).to.eq(result) 111 | }) 112 | 113 | it('accurate with phantom overflow', async () => { 114 | const result = BigNumber.from(4375).mul(Q128).div(1000) 115 | expect(await fullMath.mulDivRoundingUp(Q128, BigNumber.from(35).mul(Q128), BigNumber.from(8).mul(Q128))).to.eq( 116 | result 117 | ) 118 | }) 119 | 120 | it('accurate with phantom overflow and repeating decimal', async () => { 121 | const result = BigNumber.from(1).mul(Q128).div(3).add(1) 122 | expect( 123 | await fullMath.mulDivRoundingUp(Q128, BigNumber.from(1000).mul(Q128), BigNumber.from(3000).mul(Q128)) 124 | ).to.eq(result) 125 | }) 126 | }) 127 | 128 | function pseudoRandomBigNumber() { 129 | return BigNumber.from(new Decimal(MaxUint256.toString()).mul(Math.random().toString()).round().toString()) 130 | } 131 | 132 | // tiny fuzzer. unskip to run 133 | it.skip('check a bunch of random inputs against JS implementation', async () => { 134 | // generates random inputs 135 | const tests = Array(1_000) 136 | .fill(null) 137 | .map(() => { 138 | return { 139 | x: pseudoRandomBigNumber(), 140 | y: pseudoRandomBigNumber(), 141 | d: pseudoRandomBigNumber(), 142 | } 143 | }) 144 | .map(({ x, y, d }) => { 145 | return { 146 | input: { 147 | x, 148 | y, 149 | d, 150 | }, 151 | floored: fullMath.mulDiv(x, y, d), 152 | ceiled: fullMath.mulDivRoundingUp(x, y, d), 153 | } 154 | }) 155 | 156 | await Promise.all( 157 | tests.map(async ({ input: { x, y, d }, floored, ceiled }) => { 158 | if (d.eq(0)) { 159 | await expect(floored).to.be.reverted 160 | await expect(ceiled).to.be.reverted 161 | return 162 | } 163 | 164 | if (x.eq(0) || y.eq(0)) { 165 | await expect(floored).to.eq(0) 166 | await expect(ceiled).to.eq(0) 167 | } else if (x.mul(y).div(d).gt(MaxUint256)) { 168 | await expect(floored).to.be.reverted 169 | await expect(ceiled).to.be.reverted 170 | } else { 171 | expect(await floored).to.eq(x.mul(y).div(d)) 172 | expect(await ceiled).to.eq( 173 | x 174 | .mul(y) 175 | .div(d) 176 | .add(x.mul(y).mod(d).gt(0) ? 1 : 0) 177 | ) 178 | } 179 | }) 180 | ) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /test/TickMath.spec.ts: -------------------------------------------------------------------------------- 1 | import snapshotGasCost from '@uniswap/snapshot-gas-cost' 2 | import Decimal from 'decimal.js' 3 | import { BigNumber } from 'ethers' 4 | import { ethers } from 'hardhat' 5 | import { TickMathTest } from '../typechain/TickMathTest' 6 | import { MAX_TICK, MIN_TICK } from './shared/constants' 7 | import { expect } from './shared/expect' 8 | import { encodeSqrtPriceX96, MAX_SQRT_RATIO, MIN_SQRT_RATIO } from './shared/utilities' 9 | 10 | Decimal.config({ toExpNeg: -500, toExpPos: 500 }) 11 | 12 | describe('TickMath', () => { 13 | let tickMath: TickMathTest 14 | 15 | before('deploy TickMathTest', async () => { 16 | const factory = await ethers.getContractFactory('TickMathTest') 17 | tickMath = (await factory.deploy()) as TickMathTest 18 | }) 19 | 20 | describe('#getSqrtRatioAtTick', () => { 21 | it('throws for too low', async () => { 22 | await expect(tickMath.getSqrtRatioAtTick(MIN_TICK - 1)).to.be.revertedWith('InvalidTick()') 23 | }) 24 | 25 | it('throws for too low', async () => { 26 | await expect(tickMath.getSqrtRatioAtTick(MAX_TICK + 1)).to.be.revertedWith('InvalidTick()') 27 | }) 28 | 29 | it('min tick', async () => { 30 | expect(await tickMath.getSqrtRatioAtTick(MIN_TICK)).to.eq('4295128739') 31 | }) 32 | 33 | it('min tick +1', async () => { 34 | expect(await tickMath.getSqrtRatioAtTick(MIN_TICK + 1)).to.eq('4295343490') 35 | }) 36 | 37 | it('max tick - 1', async () => { 38 | expect(await tickMath.getSqrtRatioAtTick(MAX_TICK - 1)).to.eq('1461373636630004318706518188784493106690254656249') 39 | }) 40 | 41 | it('min tick ratio is less than js implementation', async () => { 42 | expect(await tickMath.getSqrtRatioAtTick(MIN_TICK)).to.be.lt(encodeSqrtPriceX96(1, BigNumber.from(2).pow(127))) 43 | }) 44 | 45 | it('max tick ratio is greater than js implementation', async () => { 46 | expect(await tickMath.getSqrtRatioAtTick(MAX_TICK)).to.be.gt(encodeSqrtPriceX96(BigNumber.from(2).pow(127), 1)) 47 | }) 48 | 49 | it('max tick', async () => { 50 | expect(await tickMath.getSqrtRatioAtTick(MAX_TICK)).to.eq('1461446703485210103287273052203988822378723970342') 51 | }) 52 | 53 | for (const absTick of [ 54 | 50, 100, 250, 500, 1_000, 2_500, 3_000, 4_000, 5_000, 50_000, 150_000, 250_000, 500_000, 738_203, 55 | ]) { 56 | for (const tick of [-absTick, absTick]) { 57 | describe(`tick ${tick}`, () => { 58 | it('is at most off by 1/100th of a bips', async () => { 59 | const jsResult = new Decimal(1.0001).pow(tick).sqrt().mul(new Decimal(2).pow(96)) 60 | const result = await tickMath.getSqrtRatioAtTick(tick) 61 | const absDiff = new Decimal(result.toString()).sub(jsResult).abs() 62 | expect(absDiff.div(jsResult).toNumber()).to.be.lt(0.000001) 63 | }) 64 | it('result', async () => { 65 | expect((await tickMath.getSqrtRatioAtTick(tick)).toString()).to.matchSnapshot() 66 | }) 67 | it('gas', async () => { 68 | await snapshotGasCost(tickMath.getGasCostOfGetSqrtRatioAtTick(tick)) 69 | }) 70 | }) 71 | } 72 | } 73 | }) 74 | 75 | describe('#MIN_TICK', async () => { 76 | // this invariant is required in the Tick#tickSpacingToMaxLiquidityPerTick formula 77 | it('equals -#MAX_TICK', async () => { 78 | const min = await tickMath.MIN_TICK() 79 | expect(min).to.eq((await tickMath.MAX_TICK()) * -1) 80 | expect(min).to.eq(MIN_TICK) // also just check the JS matches 81 | }) 82 | }) 83 | 84 | describe('#MAX_TICK', async () => { 85 | // this invariant is required in the Tick#tickSpacingToMaxLiquidityPerTick formula 86 | // this test is redundant with the above MIN_TICK test 87 | it('equals -#MIN_TICK', async () => { 88 | const max = await tickMath.MAX_TICK() 89 | expect(max).to.eq((await tickMath.MIN_TICK()) * -1) 90 | expect(max).to.eq(MAX_TICK) // also just check the JS matches 91 | }) 92 | }) 93 | 94 | describe('#MIN_SQRT_RATIO', async () => { 95 | it('equals #getSqrtRatioAtTick(MIN_TICK)', async () => { 96 | const min = await tickMath.getSqrtRatioAtTick(MIN_TICK) 97 | expect(min).to.eq(await tickMath.MIN_SQRT_RATIO()) 98 | expect(min).to.eq(MIN_SQRT_RATIO) // also just check the JS matches 99 | }) 100 | }) 101 | 102 | describe('#MAX_SQRT_RATIO', async () => { 103 | it('equals #getSqrtRatioAtTick(MAX_TICK)', async () => { 104 | const max = await tickMath.getSqrtRatioAtTick(MAX_TICK) 105 | expect(max).to.eq(await tickMath.MAX_SQRT_RATIO()) 106 | expect(max).to.eq(MAX_SQRT_RATIO) // also just check the JS matches 107 | }) 108 | }) 109 | 110 | describe('#getTickAtSqrtRatio', () => { 111 | it('throws for too low', async () => { 112 | await expect(tickMath.getTickAtSqrtRatio(MIN_SQRT_RATIO.sub(1))).to.be.revertedWith('InvalidSqrtRatio()') 113 | }) 114 | 115 | it('throws for too high', async () => { 116 | await expect(tickMath.getTickAtSqrtRatio(BigNumber.from(MAX_SQRT_RATIO))).to.be.revertedWith('InvalidSqrtRatio()') 117 | }) 118 | 119 | it('ratio of min tick', async () => { 120 | expect(await tickMath.getTickAtSqrtRatio(MIN_SQRT_RATIO)).to.eq(MIN_TICK) 121 | }) 122 | it('ratio of min tick + 1', async () => { 123 | expect(await tickMath.getTickAtSqrtRatio('4295343490')).to.eq(MIN_TICK + 1) 124 | }) 125 | it('ratio of max tick - 1', async () => { 126 | expect(await tickMath.getTickAtSqrtRatio('1461373636630004318706518188784493106690254656249')).to.eq(MAX_TICK - 1) 127 | }) 128 | it('ratio closest to max tick', async () => { 129 | expect(await tickMath.getTickAtSqrtRatio(MAX_SQRT_RATIO.sub(1))).to.eq(MAX_TICK - 1) 130 | }) 131 | 132 | for (const ratio of [ 133 | MIN_SQRT_RATIO, 134 | encodeSqrtPriceX96(BigNumber.from(10).pow(12), 1), 135 | encodeSqrtPriceX96(BigNumber.from(10).pow(6), 1), 136 | encodeSqrtPriceX96(1, 64), 137 | encodeSqrtPriceX96(1, 8), 138 | encodeSqrtPriceX96(1, 2), 139 | encodeSqrtPriceX96(1, 1), 140 | encodeSqrtPriceX96(2, 1), 141 | encodeSqrtPriceX96(8, 1), 142 | encodeSqrtPriceX96(64, 1), 143 | encodeSqrtPriceX96(1, BigNumber.from(10).pow(6)), 144 | encodeSqrtPriceX96(1, BigNumber.from(10).pow(12)), 145 | MAX_SQRT_RATIO.sub(1), 146 | ]) { 147 | describe(`ratio ${ratio}`, () => { 148 | it('is at most off by 1', async () => { 149 | const jsResult = new Decimal(ratio.toString()).div(new Decimal(2).pow(96)).pow(2).log(1.0001).floor() 150 | const result = await tickMath.getTickAtSqrtRatio(ratio) 151 | const absDiff = new Decimal(result.toString()).sub(jsResult).abs() 152 | expect(absDiff.toNumber()).to.be.lte(1) 153 | }) 154 | it('ratio is between the tick and tick+1', async () => { 155 | const tick = await tickMath.getTickAtSqrtRatio(ratio) 156 | const ratioOfTick = await tickMath.getSqrtRatioAtTick(tick) 157 | const ratioOfTickPlusOne = await tickMath.getSqrtRatioAtTick(tick + 1) 158 | expect(ratio).to.be.gte(ratioOfTick) 159 | expect(ratio).to.be.lt(ratioOfTickPlusOne) 160 | }) 161 | it('result', async () => { 162 | expect(await tickMath.getTickAtSqrtRatio(ratio)).to.matchSnapshot() 163 | }) 164 | it('gas', async () => { 165 | await snapshotGasCost(tickMath.getGasCostOfGetTickAtSqrtRatio(ratio)) 166 | }) 167 | }) 168 | } 169 | }) 170 | }) 171 | -------------------------------------------------------------------------------- /test/__snapshots__/TickMath.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TickMath #getSqrtRatioAtTick tick -50 gas 1`] = `829`; 4 | 5 | exports[`TickMath #getSqrtRatioAtTick tick -50 result 1`] = `"79030349367926598376800521322"`; 6 | 7 | exports[`TickMath #getSqrtRatioAtTick tick -100 gas 1`] = `829`; 8 | 9 | exports[`TickMath #getSqrtRatioAtTick tick -100 result 1`] = `"78833030112140176575862854579"`; 10 | 11 | exports[`TickMath #getSqrtRatioAtTick tick -250 gas 1`] = `871`; 12 | 13 | exports[`TickMath #getSqrtRatioAtTick tick -250 result 1`] = `"78244023372248365697264290337"`; 14 | 15 | exports[`TickMath #getSqrtRatioAtTick tick -500 gas 1`] = `871`; 16 | 17 | exports[`TickMath #getSqrtRatioAtTick tick -500 result 1`] = `"77272108795590369356373805297"`; 18 | 19 | exports[`TickMath #getSqrtRatioAtTick tick -1000 gas 1`] = `871`; 20 | 21 | exports[`TickMath #getSqrtRatioAtTick tick -1000 result 1`] = `"75364347830767020784054125655"`; 22 | 23 | exports[`TickMath #getSqrtRatioAtTick tick -2500 gas 1`] = `857`; 24 | 25 | exports[`TickMath #getSqrtRatioAtTick tick -2500 result 1`] = `"69919044979842180277688105136"`; 26 | 27 | exports[`TickMath #getSqrtRatioAtTick tick -3000 gas 1`] = `885`; 28 | 29 | exports[`TickMath #getSqrtRatioAtTick tick -3000 result 1`] = `"68192822843687888778582228483"`; 30 | 31 | exports[`TickMath #getSqrtRatioAtTick tick -4000 gas 1`] = `871`; 32 | 33 | exports[`TickMath #getSqrtRatioAtTick tick -4000 result 1`] = `"64867181785621769311890333195"`; 34 | 35 | exports[`TickMath #getSqrtRatioAtTick tick -5000 gas 1`] = `857`; 36 | 37 | exports[`TickMath #getSqrtRatioAtTick tick -5000 result 1`] = `"61703726247759831737814779831"`; 38 | 39 | exports[`TickMath #getSqrtRatioAtTick tick -50000 gas 1`] = `871`; 40 | 41 | exports[`TickMath #getSqrtRatioAtTick tick -50000 result 1`] = `"6504256538020985011912221507"`; 42 | 43 | exports[`TickMath #getSqrtRatioAtTick tick -150000 gas 1`] = `899`; 44 | 45 | exports[`TickMath #getSqrtRatioAtTick tick -150000 result 1`] = `"43836292794701720435367485"`; 46 | 47 | exports[`TickMath #getSqrtRatioAtTick tick -250000 gas 1`] = `885`; 48 | 49 | exports[`TickMath #getSqrtRatioAtTick tick -250000 result 1`] = `"295440463448801648376846"`; 50 | 51 | exports[`TickMath #getSqrtRatioAtTick tick -500000 gas 1`] = `885`; 52 | 53 | exports[`TickMath #getSqrtRatioAtTick tick -500000 result 1`] = `"1101692437043807371"`; 54 | 55 | exports[`TickMath #getSqrtRatioAtTick tick -738203 gas 1`] = `917`; 56 | 57 | exports[`TickMath #getSqrtRatioAtTick tick -738203 result 1`] = `"7409801140451"`; 58 | 59 | exports[`TickMath #getSqrtRatioAtTick tick 50 gas 1`] = `869`; 60 | 61 | exports[`TickMath #getSqrtRatioAtTick tick 50 result 1`] = `"79426470787362580746886972461"`; 62 | 63 | exports[`TickMath #getSqrtRatioAtTick tick 100 gas 1`] = `869`; 64 | 65 | exports[`TickMath #getSqrtRatioAtTick tick 100 result 1`] = `"79625275426524748796330556128"`; 66 | 67 | exports[`TickMath #getSqrtRatioAtTick tick 250 gas 1`] = `911`; 68 | 69 | exports[`TickMath #getSqrtRatioAtTick tick 250 result 1`] = `"80224679980005306637834519095"`; 70 | 71 | exports[`TickMath #getSqrtRatioAtTick tick 500 gas 1`] = `911`; 72 | 73 | exports[`TickMath #getSqrtRatioAtTick tick 500 result 1`] = `"81233731461783161732293370115"`; 74 | 75 | exports[`TickMath #getSqrtRatioAtTick tick 1000 gas 1`] = `911`; 76 | 77 | exports[`TickMath #getSqrtRatioAtTick tick 1000 result 1`] = `"83290069058676223003182343270"`; 78 | 79 | exports[`TickMath #getSqrtRatioAtTick tick 2500 gas 1`] = `897`; 80 | 81 | exports[`TickMath #getSqrtRatioAtTick tick 2500 result 1`] = `"89776708723587163891445672585"`; 82 | 83 | exports[`TickMath #getSqrtRatioAtTick tick 3000 gas 1`] = `925`; 84 | 85 | exports[`TickMath #getSqrtRatioAtTick tick 3000 result 1`] = `"92049301871182272007977902845"`; 86 | 87 | exports[`TickMath #getSqrtRatioAtTick tick 4000 gas 1`] = `911`; 88 | 89 | exports[`TickMath #getSqrtRatioAtTick tick 4000 result 1`] = `"96768528593268422080558758223"`; 90 | 91 | exports[`TickMath #getSqrtRatioAtTick tick 5000 gas 1`] = `897`; 92 | 93 | exports[`TickMath #getSqrtRatioAtTick tick 5000 result 1`] = `"101729702841318637793976746270"`; 94 | 95 | exports[`TickMath #getSqrtRatioAtTick tick 50000 gas 1`] = `911`; 96 | 97 | exports[`TickMath #getSqrtRatioAtTick tick 50000 result 1`] = `"965075977353221155028623082916"`; 98 | 99 | exports[`TickMath #getSqrtRatioAtTick tick 150000 gas 1`] = `939`; 100 | 101 | exports[`TickMath #getSqrtRatioAtTick tick 150000 result 1`] = `"143194173941309278083010301478497"`; 102 | 103 | exports[`TickMath #getSqrtRatioAtTick tick 250000 gas 1`] = `925`; 104 | 105 | exports[`TickMath #getSqrtRatioAtTick tick 250000 result 1`] = `"21246587762933397357449903968194344"`; 106 | 107 | exports[`TickMath #getSqrtRatioAtTick tick 500000 gas 1`] = `925`; 108 | 109 | exports[`TickMath #getSqrtRatioAtTick tick 500000 result 1`] = `"5697689776495288729098254600827762987878"`; 110 | 111 | exports[`TickMath #getSqrtRatioAtTick tick 738203 gas 1`] = `957`; 112 | 113 | exports[`TickMath #getSqrtRatioAtTick tick 738203 result 1`] = `"847134979253254120489401328389043031315994541"`; 114 | 115 | exports[`TickMath #getTickAtSqrtRatio ratio 4295128739 gas 1`] = `2365`; 116 | 117 | exports[`TickMath #getTickAtSqrtRatio ratio 4295128739 result 1`] = `-887272`; 118 | 119 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543 gas 1`] = `2350`; 120 | 121 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543 result 1`] = `-276325`; 122 | 123 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950 gas 1`] = `2350`; 124 | 125 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950 result 1`] = `-138163`; 126 | 127 | exports[`TickMath #getTickAtSqrtRatio ratio 9903520314283042199192993792 gas 1`] = `1378`; 128 | 129 | exports[`TickMath #getTickAtSqrtRatio ratio 9903520314283042199192993792 result 1`] = `-41591`; 130 | 131 | exports[`TickMath #getTickAtSqrtRatio ratio 28011385487393069959365969113 gas 1`] = `2323`; 132 | 133 | exports[`TickMath #getTickAtSqrtRatio ratio 28011385487393069959365969113 result 1`] = `-20796`; 134 | 135 | exports[`TickMath #getTickAtSqrtRatio ratio 56022770974786139918731938227 gas 1`] = `2309`; 136 | 137 | exports[`TickMath #getTickAtSqrtRatio ratio 56022770974786139918731938227 result 1`] = `-6932`; 138 | 139 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950336 gas 1`] = `2229`; 140 | 141 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950336 result 1`] = `0`; 142 | 143 | exports[`TickMath #getTickAtSqrtRatio ratio 112045541949572279837463876454 gas 1`] = `2349`; 144 | 145 | exports[`TickMath #getTickAtSqrtRatio ratio 112045541949572279837463876454 result 1`] = `6931`; 146 | 147 | exports[`TickMath #getTickAtSqrtRatio ratio 224091083899144559674927752909 gas 1`] = `2363`; 148 | 149 | exports[`TickMath #getTickAtSqrtRatio ratio 224091083899144559674927752909 result 1`] = `20795`; 150 | 151 | exports[`TickMath #getTickAtSqrtRatio ratio 633825300114114700748351602688 gas 1`] = `2376`; 152 | 153 | exports[`TickMath #getTickAtSqrtRatio ratio 633825300114114700748351602688 result 1`] = `41590`; 154 | 155 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950336000 gas 1`] = `2401`; 156 | 157 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950336000 result 1`] = `138162`; 158 | 159 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950336000000 gas 1`] = `2401`; 160 | 161 | exports[`TickMath #getTickAtSqrtRatio ratio 79228162514264337593543950336000000 result 1`] = `276324`; 162 | 163 | exports[`TickMath #getTickAtSqrtRatio ratio 1461446703485210103287273052203988822378723970341 gas 1`] = `2414`; 164 | 165 | exports[`TickMath #getTickAtSqrtRatio ratio 1461446703485210103287273052203988822378723970341 result 1`] = `887271`; 166 | --------------------------------------------------------------------------------