-
Notifications
You must be signed in to change notification settings - Fork 64
[nato-uniswap-v3] Add NATO Uniswap V3 strategy #1353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 19 commits
aa07d2e
445ab34
4aaea2c
da5e6a4
e611696
efbfaff
85caeb9
072aa05
47157ac
44d1ba0
017a4e8
05f53a3
92cba05
e79aa3d
dfb0842
b59088b
6ef6d26
89df396
e023ad6
171c32f
2404807
9d300ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # NATO Uniswap V3 Strategy | ||
|
|
||
| This strategy calculates voting power based on Uniswap V3 LP positions that include the NATO token. | ||
|
|
||
| ## Parameters | ||
|
|
||
| - **tokenAddress**: NATO token contract address | ||
| - **poolAddress**: Uniswap V3 pool address (NATO/WETH) | ||
| - **feeTier**: Uniswap V3 pool fee tier (e.g. `10000` for 1%) | ||
| - **positionManager**: Uniswap V3 NFT Position Manager contract address | ||
| - **network**: Target chain (e.g. `"base"`) | ||
|
|
||
| ## Example | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "name": "Nato example", | ||
| "strategy": { | ||
| "name": "nato-uniswap-v3", | ||
| "params": { | ||
| "tokenAddress": "0xd968196fa6977c4e58f2af5ac01c655ea8332d22", | ||
| "poolAddress": "0x02623e0e65a1d8537f6235512839e2f7b76c7a12", | ||
| "feeTier": 10000, | ||
| "positionManager": "0x03a520b32c04bf3beef7beb72e919cf822ed34f1", | ||
| "network": "base" | ||
| } | ||
| }, | ||
| "network": "base", | ||
| "addresses": [ | ||
| "0x4019fc024a385c6eb2ba17a43b286615f11c4ef4", | ||
| "0x07a1f6fc8923c5ebd4e4ddae89ac97629856a0f", | ||
| "0x38c0039247a31f399bae65e953612125cb88268" | ||
| ], | ||
| "snapshot": 12222222 | ||
| } | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| [ | ||
| { | ||
| "name": "Nato example", | ||
| "strategy": { | ||
| "name": "nato-uniswap-v3", | ||
| "params": { | ||
| "tokenAddress": "0xd968196fa6977c4e58f2af5ac01c655ea8332d22", | ||
| "poolAddress": "0x02623e0e65A1D8537f6235512839E2f7b76C7A12", | ||
| "feeTier": 10000, | ||
| "positionManager": "0x03a520b32c04bf3beef7beb72e919cf822ed34f1" | ||
| } | ||
| }, | ||
| "network": "base", | ||
| "addresses": [ | ||
| "0x4019FC024a385c6Eb2bA17a43B286615f11C4EF4", | ||
| "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", | ||
| "0x38C0039247A31F3939baE65e953612125cB88268" | ||
| ], | ||
| "snapshot": 12222222 | ||
| } | ||
|
Comment on lines
+10
to
+18
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
src/strategies/strategies/nato-uniswap-v3/index.ts |
||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| import { getAddress } from '@ethersproject/address'; | ||
| import { formatUnits } from '@ethersproject/units'; | ||
| import { Multicaller } from '../../utils'; | ||
|
|
||
| const POSITION_MANAGER_ABI = [ | ||
| 'function balanceOf(address owner) view returns (uint256)', | ||
| 'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)', | ||
| 'function positions(uint256 tokenId) view returns (uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)' | ||
| ]; | ||
|
|
||
| const POOL_ABI = [ | ||
| 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)' | ||
| ]; | ||
|
|
||
| function getAmount1ForLiquidity( | ||
| sqrtLower: number, | ||
| sqrtUpper: number, | ||
| sqrtPriceCurrent: bigint, | ||
| liquidity: bigint | ||
| ): bigint { | ||
| const lower = BigInt(sqrtLower); | ||
| const upper = BigInt(sqrtUpper); | ||
| const current = BigInt(sqrtPriceCurrent); | ||
| const L = BigInt(liquidity); | ||
|
|
||
| if (current <= lower) return 0n; | ||
| if (current < upper) { | ||
| return (L * (current - lower)) / 2n ** 96n; | ||
| } | ||
| return (L * (upper - lower)) / 2n ** 96n; | ||
| } | ||
|
|
||
| export const strategy = async ( | ||
| _space: string, | ||
| _network: string, | ||
| _provider: any, | ||
| addresses: string[], | ||
| options: any, | ||
| snapshot: number | 'latest' | ||
| ) => { | ||
| const results: Record<string, number> = {}; | ||
| const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; | ||
|
|
||
| // slot0 | ||
| const poolCaller = new Multicaller(_network, _provider, POOL_ABI, { | ||
| blockTag | ||
| }); | ||
| poolCaller.call('slot0', options.poolAddress, 'slot0', []); | ||
| const poolRes = await poolCaller.execute(); | ||
| const slot0Arr = poolRes.slot0 as any[]; | ||
| if (!slot0Arr || !slot0Arr[0]) | ||
| throw new Error('❌ Failed to fetch slot0() from pool'); | ||
| const sqrtPriceX96 = BigInt(slot0Arr[0]); | ||
|
|
||
| // balanceOf | ||
| const balancesCaller = new Multicaller( | ||
| _network, | ||
| _provider, | ||
| POSITION_MANAGER_ABI, | ||
| { blockTag } | ||
| ); | ||
| addresses.forEach(addr => { | ||
| const addrChecksum = getAddress(addr); | ||
| balancesCaller.call( | ||
| `${addrChecksum}.balance`, | ||
| options.positionManager, | ||
| 'balanceOf', | ||
| [addrChecksum] | ||
| ); | ||
| }); | ||
| const balancesRes = (await balancesCaller.execute()) as Record< | ||
| string, | ||
| { balance: any } | ||
| >; | ||
|
|
||
| // tokenOfOwnerByIndex | ||
| const tokensCaller = new Multicaller( | ||
| _network, | ||
| _provider, | ||
| POSITION_MANAGER_ABI, | ||
| { blockTag } | ||
| ); | ||
| Object.entries(balancesRes).forEach(([addrChecksum, { balance }]) => { | ||
| const count = balance ? parseInt(balance.toString()) : 0; | ||
| for (let i = 0; i < count; i++) { | ||
| tokensCaller.call( | ||
| `${addrChecksum}.tokenIds.${i}`, | ||
| options.positionManager, | ||
| 'tokenOfOwnerByIndex', | ||
| [addrChecksum, i] | ||
| ); | ||
| } | ||
| }); | ||
| const tokensRes = (await tokensCaller.execute()) as Record< | ||
| string, | ||
| { tokenIds: Record<string, any> } | ||
| >; | ||
|
|
||
| // positions | ||
| const positionsCaller = new Multicaller( | ||
| _network, | ||
| _provider, | ||
| POSITION_MANAGER_ABI, | ||
| { blockTag } | ||
| ); | ||
| Object.values(tokensRes).forEach((entry: any) => { | ||
| if (!entry?.tokenIds) return; | ||
| Object.values(entry.tokenIds).forEach((tokenId: any) => { | ||
| if (tokenId !== undefined) { | ||
| positionsCaller.call( | ||
| `${tokenId}`, | ||
| options.positionManager, | ||
| 'positions', | ||
| [tokenId.toString()] | ||
| ); | ||
| } | ||
| }); | ||
| }); | ||
| const positionsRes = (await positionsCaller.execute()) as Record< | ||
| string, | ||
| any[] | ||
| >; | ||
|
|
||
| // aggregate | ||
| for (const addrRaw of addresses) { | ||
| const addrChecksum = getAddress(addrRaw); | ||
| const balBN = balancesRes[addrChecksum]?.balance; | ||
| const bal = balBN ? parseInt(balBN.toString()) : 0; | ||
| if (!bal) { | ||
| results[addrChecksum] = 0; | ||
| continue; | ||
| } | ||
|
|
||
| const tokenIdsObj = (tokensRes[addrChecksum]?.tokenIds || {}) as Record< | ||
| string, | ||
| any | ||
| >; | ||
| const tokenIds = Object.values(tokenIdsObj) | ||
| .map((v: any) => v?.toString()) | ||
| .filter(Boolean); | ||
|
|
||
| let total = 0n; | ||
| for (const tokenId of tokenIds) { | ||
| const pos = positionsRes[tokenId]; | ||
| if (!pos) continue; | ||
|
|
||
| const [ | ||
| , | ||
| , | ||
| , | ||
| token1, | ||
| fee, | ||
| tickLower, | ||
| tickUpper, | ||
| liquidity, | ||
| , | ||
| , | ||
| , | ||
| tokensOwed1 | ||
| ] = pos; | ||
|
|
||
| if ( | ||
| String(token1).toLowerCase() !== | ||
| String(options.tokenAddress).toLowerCase() || | ||
| Number(fee) !== Number(options.feeTier) | ||
| ) | ||
| continue; | ||
|
|
||
| const sqrtLower = Math.floor( | ||
| Math.sqrt(Math.pow(1.0001, Number(tickLower))) * 2 ** 96 | ||
| ); | ||
| const sqrtUpper = Math.floor( | ||
| Math.sqrt(Math.pow(1.0001, Number(tickUpper))) * 2 ** 96 | ||
| ); | ||
|
|
||
| const amount1 = getAmount1ForLiquidity( | ||
| sqrtLower, | ||
| sqrtUpper, | ||
| sqrtPriceX96, | ||
| BigInt(liquidity) | ||
| ); | ||
| total += amount1 + BigInt(tokensOwed1); | ||
| } | ||
|
|
||
| results[addrChecksum] = parseFloat(formatUnits(total.toString(), 18)); | ||
| } | ||
|
|
||
| return results; | ||
| }; |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Schema doesn't look correct |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "title": "NATO Uniswap V3 Strategy", | ||
| "type": "object", | ||
| "properties": { | ||
| "tokenAddress": { | ||
| "type": "string", | ||
| "title": "NATO Token Address", | ||
| "default": "0xd968196fa6977c4e58f2af5ac01c655ea8332d22" | ||
| }, | ||
| "poolAddress": { | ||
| "type": "string", | ||
| "title": "Uniswap V3 Pool Address", | ||
| "default": "0x02623e0e65a1d8537f6235512839e2f7b76c7a12" | ||
| }, | ||
| "feeTier": { | ||
| "type": "number", | ||
| "title": "Fee Tier (Uniswap V3)", | ||
| "default": 10000 | ||
| }, | ||
| "positionManager": { | ||
| "type": "string", | ||
| "title": "Uniswap V3 Position Manager Address", | ||
| "default": "0x03a520b32c04bf3beef7beb72e919cf822ed34f1" | ||
| }, | ||
| "network": { | ||
| "type": "string", | ||
| "title": "Network", | ||
| "default": "base" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "tokenAddress", | ||
| "poolAddress", | ||
| "feeTier", | ||
| "positionManager", | ||
| "network" | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still same, we need to update this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to update it to be like this:
[
{
"strategy": {
"name": "nato-uniswap-v3",
"params": {
"tokenAddress": "0xd968196fa6977c4e58f2af5ac01c655ea8332d22",
"poolAddress": "0x02623e0e65A1D8537f6235512839E2f7b76C7A12",
"feeTier": 10000,
"network": "base"
}
},
"addresses": [
"0x1234567890abcdef1234567890abcdef12345678",
"0xaabbccddeeff00112233445566778899aabbccdd",
"0x07a1f6fc89223c5ebd4e4ddae89ac97629856a0f"
],
"snapshot": 12222222
}
]
like this would be fine?