Skip to content

Commit

Permalink
WIP: Add support for SlipStream.
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarlycow committed Jul 26, 2024
1 parent e4e9bcf commit c207a54
Show file tree
Hide file tree
Showing 16 changed files with 1,682 additions and 112 deletions.
14 changes: 2 additions & 12 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ on:

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

jobs:
Expand Down Expand Up @@ -42,16 +41,7 @@ jobs:
forge build
id: build

- name: Run Forge tests on Ethereum mainnet
- name: Run Forge tests on Base
run: |
forge test -vvv
env:
CHAIN_ID: 1
id: testMainnet

- name: Run Forge tests on BNB mainnet
run: |
forge test -vvv
env:
CHAIN_ID: 56
id: testBNB
id: testBase
14 changes: 8 additions & 6 deletions contracts/EphemeralAllPositionsByOwner.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {NPMCaller} from "@aperture_finance/uni-v3-lib/src/NPMCaller.sol";
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);
constructor(DEX dex, address npm, address owner) payable {
PositionState[] memory positions = allPositions(dex, 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.
Expand All @@ -18,18 +19,19 @@ contract EphemeralAllPositionsByOwner is PositionUtils {
}

/// @dev Public function to expose the abi for easier decoding using TypeChain
/// @param dex DEX
/// @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);
function allPositions(DEX dex, address npm, address owner) public payable returns (PositionState[] memory positions) {
uint256 balance = NPMCaller.balanceOf(INPM(npm), owner);
positions = new PositionState[](balance);
unchecked {
for (uint256 i; i < balance; ++i) {
uint256 tokenId = NPMCaller.tokenOfOwnerByIndex(npm, owner, i);
uint256 tokenId = NPMCaller.tokenOfOwnerByIndex(INPM(npm), owner, i);
PositionState memory state = positions[i];
state.owner = owner;
positionInPlace(npm, tokenId, state.position);
peek(npm, tokenId, state);
peek(dex, npm, tokenId, state);
}
}
}
Expand Down
12 changes: 7 additions & 5 deletions contracts/EphemeralGetPosition.sol
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {NPMCaller} from "@aperture_finance/uni-v3-lib/src/NPMCaller.sol";
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);
constructor(DEX dex, address npm, uint256 tokenId) payable {
PositionState memory pos = getPosition(dex, 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 dex DEX
/// @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);
function getPosition(DEX dex, address npm, uint256 tokenId) public payable returns (PositionState memory state) {
state.owner = NPMCaller.ownerOf(INPM(npm), tokenId);
positionInPlace(npm, tokenId, state.position);
peek(npm, tokenId, state);
peek(dex, npm, tokenId, state);
}
}
13 changes: 8 additions & 5 deletions contracts/EphemeralGetPositions.sol
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {NPMCaller} from "@aperture_finance/uni-v3-lib/src/NPMCaller.sol";
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);
constructor(DEX dex, address npm, uint256[] memory tokenIds) payable {
PositionState[] memory positions = getPositions(dex, 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 dex DEX
/// @param npm Nonfungible position manager
/// @param tokenIds Token IDs of the positions
function getPositions(
INPM npm,
DEX dex,
address npm,
uint256[] memory tokenIds
) public payable returns (PositionState[] memory positions) {
unchecked {
Expand All @@ -32,8 +35,8 @@ contract EphemeralGetPositions is PositionUtils {
PositionState memory state = positions[i];
if (positionInPlace(npm, tokenId, state.position)) {
++i;
state.owner = NPMCaller.ownerOf(npm, tokenId);
peek(npm, tokenId, state);
state.owner = NPMCaller.ownerOf(INPM(npm), tokenId);
peek(dex, npm, tokenId, state);
}
}
assembly ("memory-safe") {
Expand Down
93 changes: 82 additions & 11 deletions contracts/PositionUtils.sol
Original file line number Diff line number Diff line change
@@ -1,35 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {INonfungiblePositionManager as INPM, IPCSV3NonfungiblePositionManager as IPCSV3NPM} from "@aperture_finance/uni-v3-lib/src/interfaces/INonfungiblePositionManager.sol";
import {NPMCaller, PositionFull} from "@aperture_finance/uni-v3-lib/src/NPMCaller.sol";
import {ICommonNonfungiblePositionManager as INPM, IUniswapV3NonfungiblePositionManager as IUniV3NPM} from "@aperture_finance/uni-v3-lib/src/interfaces/IUniswapV3NonfungiblePositionManager.sol";
import {IPCSV3NonfungiblePositionManager as IPCSV3NPM} from "@aperture_finance/uni-v3-lib/src/interfaces/IPCSV3NonfungiblePositionManager.sol";
import {ISlipStreamNonfungiblePositionManager as ISlipStreamNPM} from "@aperture_finance/uni-v3-lib/src/interfaces/ISlipStreamNonfungiblePositionManager.sol";
import {PoolAddress} from "@aperture_finance/uni-v3-lib/src/PoolAddress.sol";
import {PoolAddressPancakeSwapV3} from "@aperture_finance/uni-v3-lib/src/PoolAddressPancakeSwapV3.sol";
import {IUniswapV3PoolState, V3PoolCallee} from "@aperture_finance/uni-v3-lib/src/PoolCaller.sol";
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import {IPancakeV3Factory} from "@pancakeswap/v3-core/contracts/interfaces/IPancakeV3Factory.sol";
import {ERC20Callee} from "./libraries/ERC20Caller.sol";
import {PoolUtils} from "./PoolUtils.sol";

enum DEX {
UniswapV3,
PancakeSwapV3,
SlipStream
}

struct Slot0 {
uint160 sqrtPriceX96;
int24 tick;
uint16 observationIndex;
uint16 observationCardinality;
uint16 observationCardinalityNext;
// `feeProtocol` is of type uint8 in Uniswap V3, and uint32 in PancakeSwap V3.
// We use uint32 here as this can hold both uint8 and uint32.
// `feeProtocol` is of type uint8 in Uniswap V3, and uint32 in PancakeSwap V3. We use uint32 here as this can hold both uint8 and uint32.
// `feeProtocol` doesn't exist on SlipStream so this is manually set to 0.
uint32 feeProtocol;
// This should always be true because it is only temporarily set to false as re-entrancy guard during a transaction.
bool unlocked;
}

// The length of the struct is 25 words.
struct PositionFull {
// the nonce for permits
uint96 nonce;
// the address that is approved for spending this token
address operator;
address token0;
address token1;
// The pool's fee or tickSpacing. This depends on the DEX's NPM.positions() implementation. This is `fee` for Uniswap V3 and PancakeSwap V3, and `tickSpacing` for SlipStream.
uint24 feeOrTickSpacing;
// the tick range of the position
int24 tickLower;
int24 tickUpper;
// the liquidity of the position
uint128 liquidity;
// the fee growth of the aggregate position as of the last action on the individual position
uint256 feeGrowthInside0LastX128;
uint256 feeGrowthInside1LastX128;
// how many uncollected tokens are owed to the position, as of the last computation
uint128 tokensOwed0;
uint128 tokensOwed1;
}

struct PositionState {
// token ID of the position
uint256 tokenId;
// position's owner
address owner;
// nonfungible position manager's position struct with real-time tokensOwed
PositionFull position;
// The pool address.
address pool;
// The pool's fee in hundredths of a bip, i.e. 1e-6
uint24 poolFee;
// The pool's tick spacing.
int24 poolTickSpacing;
// pool's slot0 struct
Slot0 slot0;
// pool's active liquidity
Expand All @@ -40,21 +76,56 @@ struct PositionState {
uint8 decimals1;
}

// Partial interface for the SlipStream factory. SlipStream factory is named "CLFactory" and "CL" presumably stands for concentrated liquidity.
// https://github.com/velodrome-finance/slipstream/blob/main/contracts/core/interfaces/ICLFactory.sol
interface ISlipStreamCLFactory {
/// @notice Returns the pool address for a given pair of tokens and a tick spacing, or address 0 if it does not exist
/// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order
/// @param tokenA The contract address of either token0 or token1
/// @param tokenB The contract address of the other token
/// @param tickSpacing The tick spacing of the pool
/// @return pool The pool address
function getPool(address tokenA, address tokenB, int24 tickSpacing) external view returns (address pool);
}

/// @title Position utility contract
/// @author Aperture Finance
/// @notice Base contract for Uniswap v3 that peeks into the current state of position and pool info
abstract contract PositionUtils is PoolUtils {
/// @dev Peek a position and calculate the fee growth inside the position
/// state.position must be populated before calling this function
/// @param dex DEX type
/// @param npm Nonfungible position manager
/// @param tokenId Token ID of the position
/// @param state Position state pointer to be updated in place
function peek(INPM npm, uint256 tokenId, PositionState memory state) internal view {
function peek(DEX dex, address npm, uint256 tokenId, PositionState memory state) internal view {
state.tokenId = tokenId;
PositionFull memory position = state.position;
V3PoolCallee pool = V3PoolCallee.wrap(
IUniswapV3Factory(NPMCaller.factory(npm)).getPool(position.token0, position.token1, position.fee)
);
if (dex == DEX.UniswapV3) {
state.poolFee = position.feeOrTickSpacing;
state.pool = IUniswapV3Factory(IUniV3NPM(npm).factory()).getPool(
position.token0,
position.token1,
state.poolFee
);
} else if (dex == DEX.PancakeSwapV3) {
state.poolFee = position.feeOrTickSpacing;
state.pool = IPancakeV3Factory(IUniV3NPM(npm).factory()).getPool(
position.token0,
position.token1,
state.poolFee
);
} else if (dex == DEX.SlipStream) {
state.poolTickSpacing = int24(position.feeOrTickSpacing);
state.pool = ISlipStreamCLFactory(ISlipStreamNPM(npm).factory()).getPool(
position.token0,
position.token1,
state.poolTickSpacing
);
}
V3PoolCallee pool = V3PoolCallee.wrap(state.pool);
state.poolFee = pool.fee();
state.poolTickSpacing = pool.tickSpacing();
state.activeLiquidity = pool.liquidity();
slot0InPlace(pool, state.slot0);
if (position.liquidity != 0) {
Expand Down Expand Up @@ -83,8 +154,8 @@ abstract contract PositionUtils is PoolUtils {
/// @param tokenId The ID of the token that represents the position
/// @param pos The position pointer to be updated in place
/// @return exists Whether the position exists
function positionInPlace(INPM npm, uint256 tokenId, PositionFull memory pos) internal view returns (bool exists) {
bytes4 selector = INPM.positions.selector;
function positionInPlace(address npm, uint256 tokenId, PositionFull memory pos) internal view returns (bool exists) {
bytes4 selector = IUniV3NPM(npm).positions.selector;
assembly ("memory-safe") {
// Write the abi-encoded calldata into memory.
mstore(0, selector)
Expand Down
3 changes: 1 addition & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ optimizer_runs = 200
runs = 16

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
bnb_smart_chain = "${BNB_RPC_URL}"
base = "${BASE_RPC_URL}"

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
"typechain": "hardhat typechain"
},
"dependencies": {
"@aperture_finance/uni-v3-lib": "^2.0.1",
"@aperture_finance/uni-v3-lib": "^3.0.0-alpha",
"@openzeppelin/contracts": "^5.0.2",
"ethers": "5.7.2",
"viem": "^2.8.4",
"zod":"^3.23.8"
"zod": "^3.23.8"
},
"devDependencies": {
"@ethersproject/abi": "5.7.0",
Expand All @@ -66,6 +66,7 @@
"@types/node": "^20.11.30",
"@uniswap/v3-sdk": "^3.11.0",
"chai": "^4.4.1",
"dotenv": "^16.4.5",
"hardhat": "^2.22.2",
"mocha": "^10.4.0",
"prettier": "^3.2.5",
Expand Down
Loading

0 comments on commit c207a54

Please sign in to comment.