diff --git a/src/strategies/strategies/nato-uniswapv3-voting/README.md b/src/strategies/strategies/nato-uniswapv3-voting/README.md new file mode 100644 index 00000000..c8be2803 --- /dev/null +++ b/src/strategies/strategies/nato-uniswapv3-voting/README.md @@ -0,0 +1,25 @@ +# nato-uniswapv3-voting + +This strategy calculates voting power based on Uniswap V3 NFT positions in the 1% fee tier pool. It checks if a wallet holds Uniswap V3 NFTs and calculates the token holdings from 1% tier positions only. + +The strategy: +1. Checks if wallet holds Uniswap V3 NFTs +2. Filters positions to only include 1% fee tier pools +3. Calculates token holdings from those positions +4. Returns the token amount as voting power (1 token = 1 vote) + +Here is an example of parameters: + +```json +{ + "poolAddress": "0x02623e0e65a1d8537f6235512839e2f7b76c7a12", + "tokenReserve": 0, + "feeTier": 10000 +} +``` + +Parameters: +- `poolAddress`: The Uniswap V3 pool address +- `tokenReserve`: Which token to count (0 for token0, 1 for token1) +- `feeTier`: The fee tier to filter for (10000 = 1%) +- `subgraph`: Optional custom subgraph URL diff --git a/src/strategies/strategies/nato-uniswapv3-voting/examples.json b/src/strategies/strategies/nato-uniswapv3-voting/examples.json new file mode 100644 index 00000000..3415364d --- /dev/null +++ b/src/strategies/strategies/nato-uniswapv3-voting/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "NATO Uniswap V3 1% Tier Pool Example", + "strategy": { + "name": "nato-uniswapv3-voting", + "params": { + "poolAddress": "0x02623e0e65a1d8537f6235512839e2f7b76c7a12", + "tokenReserve": 0, + "feeTier": 10000 + } + }, + "network": "8453", + "addresses": [ + "0x4f0fd563be89ec8c3e7d595bf3639128c0a7c33a", + "0xda1dba3c1b1cdeaf1419a85ba5b454547bda5662", + "0x581ab14804196e9b20f02291cf863bc1b4745a87", + "0x5d62dd87f40090ba3d065b4c6bb63b8c8ff883b3", + "0x949dc9da1ce5bb121fbc1bea61b2a42dc8bedae6", + "0x7cee21fd74cff3b91c1c7251b7a4294349a0da79" + ], + "snapshot": 30937953 + } +] diff --git a/src/strategies/strategies/nato-uniswapv3-voting/index.ts b/src/strategies/strategies/nato-uniswapv3-voting/index.ts new file mode 100644 index 00000000..2b445b9a --- /dev/null +++ b/src/strategies/strategies/nato-uniswapv3-voting/index.ts @@ -0,0 +1,179 @@ +import { subgraphRequest } from '../../utils'; +import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'; +import { Token } from '@uniswap/sdk-core'; + +const UNISWAP_V3_SUBGRAPH_URL = { + '1': 'https://subgrapher.snapshot.org/subgraph/arbitrum/5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV', + '8453': + 'https://subgrapher.snapshot.org/subgraph/arbitrum/43Hwfi3dJSoGpyas9VwNoDAv55yjgGrPpNSmbQZArzMG' +}; + +export const author = 'vitalii'; +export const version = '0.1.0'; + +// Custom function to calculate total token amounts from position +function calculateTotalTokenAmounts(position: any) { + const { + tickLower, + tickUpper, + liquidity, + pool: { tick, sqrtPrice, feeTier }, + token0, + token1 + } = position; + + const baseToken = new Token( + 1, + token0.id, + Number(token0.decimals), + token0.symbol + ); + const quoteToken = new Token( + 1, + token1.id, + Number(token1.decimals), + token1.symbol + ); + + const fee = Object.values(FeeAmount).includes(parseFloat(feeTier)) + ? parseFloat(feeTier) + : 0; + const pool = new Pool( + baseToken, + quoteToken, + fee, + sqrtPrice, + liquidity, + Number(tick) + ); + + const position_obj = new Position({ + pool, + liquidity, + tickLower: Number(tickLower.tickIdx), + tickUpper: Number(tickUpper.tickIdx) + }); + + // Calculate the total amounts (this includes both in-range and out-of-range portions) + const amount0 = position_obj.amount0; + const amount1 = position_obj.amount1; + + return { + token0Amount: parseFloat(amount0.toSignificant(18)), + token1Amount: parseFloat(amount1.toSignificant(18)), + inRange: + parseInt(tick) >= parseInt(tickLower.tickIdx) && + parseInt(tick) <= parseInt(tickUpper.tickIdx) + }; +} + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +): Promise> { + const tokenReserve = + options.tokenReserve === 0 ? 'token0Reserve' : 'token1Reserve'; + const requiredFeeTier = options.feeTier || 10000; // Default to 1% fee tier (10000 = 1%) + + const _addresses = addresses.map(address => address.toLowerCase()); + + const params = { + positions: { + __args: { + where: { + pool: options.poolAddress.toLowerCase(), + owner_in: _addresses + } + }, + id: true, + owner: true, + liquidity: true, + tickLower: { + tickIdx: true + }, + tickUpper: { + tickIdx: true + }, + pool: { + tick: true, + sqrtPrice: true, + liquidity: true, + feeTier: true + }, + token0: { + symbol: true, + decimals: true, + id: true + }, + token1: { + symbol: true, + decimals: true, + id: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.positions.__args.block = { number: snapshot }; + } + + let rawData; + try { + rawData = await subgraphRequest( + options.subgraph || UNISWAP_V3_SUBGRAPH_URL[network], + params + ); + } catch (error) { + console.error('Subgraph request failed:', error); + // Return zero scores for all addresses if subgraph fails + return Object.fromEntries(addresses.map(address => [address, 0])); + } + + if (!rawData || !rawData.positions) { + // Return zero scores if no data returned + return Object.fromEntries(addresses.map(address => [address, 0])); + } + + const usersUniswap = addresses.map(() => ({ + positions: [] + })); + + rawData.positions.map(position => { + // Only include positions with the required fee tier (1% = 10000) + if (position?.pool?.feeTier === requiredFeeTier.toString()) { + const ownerIndex = _addresses.indexOf(position?.owner); + if (ownerIndex !== -1) { + usersUniswap[ownerIndex].positions.push(position); + } + } + }); + + const score = {}; + + usersUniswap?.forEach((user: any, idx) => { + let tokenReserveAdd = 0; + + user.positions.forEach((position: any) => { + // Calculate total token amounts using custom function + const tokenAmounts = calculateTotalTokenAmounts(position); + + // Add the token amount based on tokenReserve parameter + if (tokenReserve === 'token0Reserve') { + tokenReserveAdd += tokenAmounts.token0Amount; + } else { + tokenReserveAdd += tokenAmounts.token1Amount; + } + }); + + score[addresses[idx]] = tokenReserveAdd; + }); + + // Return the actual scores from the subgraph + // If no positions found, return zero scores + return score || {}; +} diff --git a/src/strategies/strategies/nato-uniswapv3-voting/manifest.json b/src/strategies/strategies/nato-uniswapv3-voting/manifest.json new file mode 100644 index 00000000..1fab4848 --- /dev/null +++ b/src/strategies/strategies/nato-uniswapv3-voting/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "NATO Uniswap V3 1% Tier Voting", + "author": "TheNationToken", + "version": "0.1.0" +} diff --git a/src/strategies/strategies/nato-uniswapv3-voting/schema.json b/src/strategies/strategies/nato-uniswapv3-voting/schema.json new file mode 100644 index 00000000..ca27a525 --- /dev/null +++ b/src/strategies/strategies/nato-uniswapv3-voting/schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "poolAddress": { + "type": "string", + "title": "Uniswap V3 Pool Address", + "examples": ["0x02623e0e65a1d8537f6235512839e2f7b76c7a12"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "tokenReserve": { + "type": "number", + "title": "Token Reserve (0 for token0, 1 for token1)", + "examples": [0, 1], + "minimum": 0, + "maximum": 1 + }, + "feeTier": { + "type": "number", + "title": "Fee Tier (10000 = 1%)", + "examples": [10000], + "minimum": 100, + "maximum": 1000000 + }, + "subgraph": { + "type": "string", + "title": "Custom Subgraph URL (optional)", + "examples": ["https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"] + } + }, + "required": ["poolAddress", "tokenReserve"], + "additionalProperties": false + } + } +}