Skip to content

Commit

Permalink
Add lens contracts and Foundry tests
Browse files Browse the repository at this point in the history
  • Loading branch information
shuhuiluo committed Dec 4, 2023
1 parent 01314cb commit 1c862c0
Show file tree
Hide file tree
Showing 29 changed files with 4,507 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
PoolLensTest:testFuzz_GetPositions(int24,int24) (runs: 16, μ: 971375, ~: 998911)
PoolLensTest:test_GetPopulatedTicksInRange() (gas: 4109615)
PoolLensTest:test_GetPositions() (gas: 712285)
PoolLensTest:test_GetSlots() (gas: 3380493)
PoolLensTest:test_GetTickBitmap() (gas: 3092280)
PositionLensTest:testFuzz_GetPosition(uint256) (runs: 16, μ: 155236, ~: 155452)
PositionLensTest:test_AllPositions() (gas: 874498)
PositionLensTest:test_GetFeesOwed() (gas: 809236)
PositionLensTest:test_GetPositions() (gas: 1015301)
PositionLensTest:test_GetTotalAmounts() (gas: 825164)
TickLensTest:test_GetPopulatedTicksInRange() (gas: 10239511)
47 changes: 47 additions & 0 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Foundry

on:
push:
branches:
- main
pull_request:

env:
FOUNDRY_PROFILE: ci
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}

jobs:
check:
name: Forge Tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: "yarn"
cache-dependency-path: "yarn.lock"

- name: Install dependencies 📦
run: yarn install --frozen-lockfile

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build
run: |
forge --version
forge build
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: test
44 changes: 44 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Node.js

on:
push:
branches:
- main
pull_request:

env:
INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }}

jobs: # each workflow consists of 1+ jobs; by default, all jobs run in parallel
test: # Run tests.
runs-on: ubuntu-latest # host's operating system
steps: # each job consists of 1+ steps
- name: Checkout commit # download the code from triggering commit
uses: actions/checkout@v3

- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: "yarn"
cache-dependency-path: "yarn.lock"

- name: Install dependencies 📦
run: yarn install --frozen-lockfile

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build
run: |
forge --version
forge build
id: build

- name: Generate Typechain types
run: yarn typechain

- name: Run hardhat tests
run: yarn test:hardhat
19 changes: 19 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Publish package
on:
release:
types: [published]
jobs:
npm_publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'
scope: '@aperture_finance'
- run: yarn install --frozen-lockfile
- run: yarn build
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,16 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# IDEs and editors
.DS_Store
.idea

# Foundry cache
cache
dry-run

# Hardhat cache
artifacts
cache_hardhat
typechain
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
36 changes: 36 additions & 0 deletions contracts/EphemeralAllPositionsByOwner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./PositionUtils.sol";

/// @notice A lens for Uniswap v3 that peeks into the current state of all positions by an owner without deployment
/// @author Aperture Finance
/// @dev The return data can be accessed externally by `eth_call` without a `to` address or internally by catching the
/// revert data, and decoded by `abi.decode(data, (PositionState[]))`
contract EphemeralAllPositionsByOwner is PositionUtils {
constructor(INPM npm, address owner) payable {
PositionState[] memory positions = allPositions(npm, owner);
bytes memory returnData = abi.encode(positions);
assembly ("memory-safe") {
// The return data in a constructor will be written to code, which may exceed the contract size limit.
revert(add(returnData, 0x20), mload(returnData))
}
}

/// @dev Public function to expose the abi for easier decoding using TypeChain
/// @param npm Nonfungible position manager
/// @param owner The address that owns the NFTs
function allPositions(INPM npm, address owner) public payable returns (PositionState[] memory positions) {
uint256 balance = NPMCaller.balanceOf(npm, owner);
positions = new PositionState[](balance);
unchecked {
for (uint256 i; i < balance; ++i) {
uint256 tokenId = NPMCaller.tokenOfOwnerByIndex(npm, owner, i);
PositionState memory state = positions[i];
state.owner = owner;
positionInPlace(npm, tokenId, state.position);
peek(npm, tokenId, state);
}
}
}
}
83 changes: 83 additions & 0 deletions contracts/EphemeralGetPopulatedTicksInRange.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "./PoolUtils.sol";

/// @notice A lens that fetches chunks of tick data in a range for a Uniswap v3 pool without deployment
/// @author Aperture Finance
/// @dev The return data can be accessed externally by `eth_call` without a `to` address or internally by catching the
/// revert data, and decoded by `abi.decode(data, (PopulatedTick[]))`
contract EphemeralGetPopulatedTicksInRange is PoolUtils {
constructor(V3PoolCallee pool, int24 tickLower, int24 tickUpper) payable {
PopulatedTick[] memory populatedTicks = getPopulatedTicksInRange(pool, tickLower, tickUpper);
bytes memory returnData = abi.encode(populatedTicks);
assembly ("memory-safe") {
revert(add(returnData, 0x20), mload(returnData))
}
}

/// @notice Get all the tick data for the populated ticks from tickLower to tickUpper
/// @param pool The address of the pool for which to fetch populated tick data
/// @param tickLower The lower tick boundary of the populated ticks to fetch
/// @param tickUpper The upper tick boundary of the populated ticks to fetch
/// @return populatedTicks An array of tick data for the given word in the tick bitmap
function getPopulatedTicksInRange(
V3PoolCallee pool,
int24 tickLower,
int24 tickUpper
) public payable returns (PopulatedTick[] memory populatedTicks) {
require(tickLower <= tickUpper);
// checks that the pool exists
int24 tickSpacing = IUniswapV3Pool(V3PoolCallee.unwrap(pool)).tickSpacing();
(int16 wordPosLower, int16 wordPosUpper) = getWordPositions(tickLower, tickUpper, tickSpacing);
unchecked {
(uint256[] memory tickBitmap, uint256 count) = getTickBitmapAndCount(pool, wordPosLower, wordPosUpper);
// fetch populated tick data
populatedTicks = new PopulatedTick[](count);
uint256 idx;
for (int16 wordPos = wordPosLower; wordPos <= wordPosUpper; ++wordPos) {
idx = populateTicksInWord(
pool,
wordPos,
tickSpacing,
tickBitmap[uint16(wordPos - wordPosLower)],
populatedTicks,
idx
);
}
}
}

/// @notice Get the tick data for all populated ticks in a word of the tick bitmap
function populateTicksInWord(
V3PoolCallee pool,
int16 wordPos,
int24 tickSpacing,
uint256 bitmap,
PopulatedTick[] memory populatedTicks,
uint256 idx
) internal view returns (uint256) {
unchecked {
for (uint256 bitPos; bitPos < 256; ++bitPos) {
//slither-disable-next-line incorrect-shift
if (bitmap & (1 << bitPos) != 0) {
int24 tick;
assembly {
tick := mul(tickSpacing, add(shl(8, wordPos), bitPos))
}
populateTick(pool, tick, populatedTicks[idx++]);
}
}
return idx;
}
}

function populateTick(V3PoolCallee pool, int24 tick, PopulatedTick memory populatedTick) internal view {
PoolCaller.TickInfo memory info = pool.ticks(tick);
populatedTick.tick = tick;
populatedTick.liquidityNet = info.liquidityNet;
populatedTick.liquidityGross = info.liquidityGross;
populatedTick.feeGrowthOutside0X128 = info.feeGrowthOutside0X128;
populatedTick.feeGrowthOutside1X128 = info.feeGrowthOutside1X128;
}
}
27 changes: 27 additions & 0 deletions contracts/EphemeralGetPosition.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./PositionUtils.sol";

/// @notice A lens for Uniswap v3 that peeks into the current state of position and pool info without deployment
/// @author Aperture Finance
/// @dev The return data can be accessed externally by `eth_call` without a `to` address or internally by catching the
/// revert data, and decoded by `abi.decode(data, (PositionState))`
contract EphemeralGetPosition is PositionUtils {
constructor(INPM npm, uint256 tokenId) payable {
PositionState memory pos = getPosition(npm, tokenId);
bytes memory returnData = abi.encode(pos);
assembly ("memory-safe") {
revert(add(returnData, 0x20), mload(returnData))
}
}

/// @dev Public function to expose the abi for easier decoding using TypeChain
/// @param npm Nonfungible position manager
/// @param tokenId Token ID of the position
function getPosition(INPM npm, uint256 tokenId) public payable returns (PositionState memory state) {
state.owner = NPMCaller.ownerOf(npm, tokenId);
positionInPlace(npm, tokenId, state.position);
peek(npm, tokenId, state);
}
}
44 changes: 44 additions & 0 deletions contracts/EphemeralGetPositions.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./PositionUtils.sol";

/// @notice A lens for Uniswap v3 that peeks into the current state of positions and pool info without deployment
/// @author Aperture Finance
/// @dev The return data can be accessed externally by `eth_call` without a `to` address or internally by catching the
/// revert data, and decoded by `abi.decode(data, (PositionState[]))`
contract EphemeralGetPositions is PositionUtils {
constructor(INPM npm, uint256[] memory tokenIds) payable {
PositionState[] memory positions = getPositions(npm, tokenIds);
bytes memory returnData = abi.encode(positions);
assembly ("memory-safe") {
revert(add(returnData, 0x20), mload(returnData))
}
}

/// @dev Public function to expose the abi for easier decoding using TypeChain
/// @param npm Nonfungible position manager
/// @param tokenIds Token IDs of the positions
function getPositions(
INPM npm,
uint256[] memory tokenIds
) public payable returns (PositionState[] memory positions) {
unchecked {
uint256 length = tokenIds.length;
positions = new PositionState[](length);
uint256 i;
for (uint256 j; j < length; ++j) {
uint256 tokenId = tokenIds[j];
PositionState memory state = positions[i];
if (positionInPlace(npm, tokenId, state.position)) {
++i;
state.owner = NPMCaller.ownerOf(npm, tokenId);
peek(npm, tokenId, state);
}
}
assembly ("memory-safe") {
mstore(positions, i)
}
}
}
}
52 changes: 52 additions & 0 deletions contracts/EphemeralPoolPositions.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./PoolUtils.sol";

/// @notice A lens that batches fetching of the `positions` mapping for a Uniswap v3 pool without deployment
/// @author Aperture Finance
/// @dev The return data can be accessed externally by `eth_call` without a `to` address or internally by catching the
/// revert data, and decoded by `abi.decode(data, (Slot[]))`
contract EphemeralPoolPositions is PoolUtils {
constructor(V3PoolCallee pool, PositionKey[] memory keys) payable {
Slot[] memory slots = getPositions(pool, keys);
bytes memory returnData = abi.encode(slots);
assembly ("memory-safe") {
revert(add(returnData, 0x20), mload(returnData))
}
}

/// @notice Get liquidity positions in a pool
/// @dev Public function to expose the abi for easier decoding using TypeChain
/// @param pool The address of the pool for which to fetch the tick bitmap
/// @param keys The position keys to fetch
/// @return slots An array of storage slots and their raw data
function getPositions(V3PoolCallee pool, PositionKey[] memory keys) public payable returns (Slot[] memory slots) {
unchecked {
uint256 length = keys.length;
// each position occupies 4 storage slots
slots = new Slot[](length << 2);
uint256 j;
for (uint256 i; i < length; ++i) {
// calculate the storage slot corresponding to the position key
// the slot of positions[key] is keccak256(abi.encode(key, positions.slot))
bytes32 key = getPositionKey(keys[i]);
uint256 slot;
assembly ("memory-safe") {
mstore(0, key)
mstore(0x20, POSITIONS_SLOT)
slot := keccak256(0, 0x40)
}
PoolCaller.PositionInfo memory info = pool.positions(key);
slots[j++] = Slot(slot++, info.liquidity);
slots[j++] = Slot(slot++, info.feeGrowthInside0LastX128);
slots[j++] = Slot(slot++, info.feeGrowthInside1LastX128);
uint256 data;
assembly {
data := or(shl(128, mload(add(info, 0x80))), mload(add(info, 0x60)))
}
slots[j++] = Slot(slot, data);
}
}
}
}
Loading

0 comments on commit 1c862c0

Please sign in to comment.