-
-
Save Picodes/0b738ec92f7bd72ec6e77ffdf5d1c5e2 to your computer and use it in GitHub Desktop.
Uniswap V3 Reward Computation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const multicallABI = [ | |
{ inputs: [], name: 'SubcallFailed', type: 'error' }, | |
{ | |
inputs: [ | |
{ | |
components: [ | |
{ internalType: 'address', name: 'target', type: 'address' }, | |
{ internalType: 'bytes', name: 'data', type: 'bytes' }, | |
{ internalType: 'bool', name: 'canFail', type: 'bool' }, | |
], | |
internalType: 'struct MultiCallWithFailure.Call[]', | |
name: 'calls', | |
type: 'tuple[]', | |
}, | |
], | |
name: 'multiCall', | |
outputs: [{ internalType: 'bytes[]', name: '', type: 'bytes[]' }], | |
stateMutability: 'view', | |
type: 'function', | |
}, | |
]; | |
const uniswapV3PoolABI = [ | |
'function positions(uint256) external 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)', | |
'function ownerOf(uint256) external view returns(address)', | |
'function token0() external view returns(address)', | |
'function token1() external view returns(address)', | |
]; | |
const arrakisABI = [ | |
'function getUnderlyingBalances() external view returns(uint256 amount0Current, uint256 amount1Current)', | |
'function positions(bytes32) external view returns(uint128, uint256, uint256, uint128, uint128)', | |
'function lowerTick() external view returns(int24)', | |
'function upperTick() external view returns(int24)', | |
'function balanceOf(address) external view returns(uint256)', | |
]; | |
const gammaABI = [ | |
'function getBasePosition() external view returns(uint128 liquidity, uint256 amount0, uint256 amount1)', | |
'function getLimitPosition() external view returns(uint128 liquidity, uint256 amount0, uint256 amount1)', | |
'function baseLower() external view returns(int24)', | |
'function baseUpper() external view returns(int24)', | |
'function limitLower() external view returns(int24)', | |
'function limitUpper() external view returns(int24)', | |
]; | |
const uniswapV3Interface = new utils.Interface(uniswapV3PoolABI); | |
const arrakisInterface = new utils.Interface(arrakisABI); | |
const gammaInterface = new utils.Interface(gammaABI); | |
async function fetchWeeklyTokenHolder(token: string, week: number) { | |
// Graph made to easily track holders of a token during a given week | |
// See https://github.com/AngleProtocol/weekly_holder_subgraph | |
const urlTG = 'https://api.thegraph.com/subgraphs/name/picodes/weekly-token-holders'; | |
const query = gql` | |
query Holders($where: [Int!], $token: String!) { | |
holders(where: { week_in: $where, token: $token }) { | |
holder | |
} | |
} | |
`; | |
const data = await request<{ | |
holders: { holder: string }[]; | |
}>(urlTG as string, query, { | |
token: token, | |
where: [0, week], | |
}); | |
return data.holders?.map((e) => e.holder); | |
} | |
async function fetchPositionsAndSwaps(pool: string, week: number) { | |
const tg_uniswap = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'; | |
const positionQuery = gql` | |
query getPositions($pool: String!, $timestamp: Int!) { | |
positionSnapshots(where: { pool_: { id: $pool } }) { | |
position { | |
id | |
} | |
} | |
swaps(where: { pool: $pool, timestamp_gt: $timestamp }, orderBy: timestamp, orderDirection: asc) { | |
timestamp | |
amountUSD | |
tick | |
sqrtPriceX96 | |
transaction { | |
blockNumber | |
} | |
} | |
} | |
`; | |
const data = await request<{ | |
positionSnapshots: { position: { id: string } }[]; | |
swaps: { amountUSD: string; tick: string; sqrtPriceX96: string; timestamp: string; transaction: { blockNumber: string } }[]; | |
}>(tg_uniswap as string, positionQuery, { | |
pool: pool, | |
timestamp: week * (7 * 24 * 3600), | |
}); | |
const positions = data?.positionSnapshots?.map((e) => e?.position.id); | |
const swaps = data.swaps; | |
return { positions, swaps }; | |
} | |
// ========== PARAMETERS ================= | |
const weeklyRewards = BigNumber.from(parseEther('1')); | |
const weights = { fees: 0.4, token0: 0.4, token1: 0.2 }; | |
const previousMerkleTree = null; | |
// Addresses | |
const uniswapV3poolAddress = '0x8db1b906d47dfc1d84a87fc49bd0522e285b98b9'; | |
const arrakisPoolAddress = '0x857E0B2eD0E82D5cDEB015E77ebB873C47F99575'; | |
const arrakisGaugeAddress = '0x3785ce82be62a342052b9e5431e9d3a839cfb581'; | |
const gammaPoolAddress = '0xf6eeCA73646ea6A5c878814e6508e87facC7927C'; // Example | |
const agEUR = CONTRACTS_ADDRESSES[ChainId.MAINNET]?.agEUR?.AgToken; | |
const multicallAddress = CONTRACTS_ADDRESSES[ChainId.MAINNET]?.MulticallWithFailure; | |
// Constants | |
const provider = providers[ChainId.MAINNET]; // ethers.provider | |
const week = Math.floor(moment().unix() / (7 * 24 * 3600)); | |
// ========== LOGIC ================= | |
// Data object that we'll fill | |
const data: { [holder: string]: { fees: number; token0: number; token1: number } } = {}; | |
if (!!agEUR && !!multicallAddress) { | |
const secondsInWeek = 7 * 24 * 3600; | |
// Uses a custom multicall contract that accept reverts | |
const multicall = new Contract(multicallAddress, multicallABI, provider); | |
try { | |
const tempData: { [holder: string]: { fees: number; token0: number; token1: number } } = {}; | |
// Fetch Uniswap V3 positions and swaps | |
const { positions, swaps } = await fetchPositionsAndSwaps(uniswapV3poolAddress?.toLowerCase(), week); | |
let totalAmountUSD = 0; | |
swaps.forEach((s) => (totalAmountUSD += parseInt(s.amountUSD))); | |
// Fetch Arrakis Holders over the week | |
const arrakisHolders = await fetchWeeklyTokenHolder(arrakisPoolAddress?.toLowerCase(), week); | |
const arrakisGaugeHolders = await fetchWeeklyTokenHolder(arrakisGaugeAddress?.toLowerCase(), week); | |
// Fetch Gamma Holders over the week | |
const gammaHolders = await fetchWeeklyTokenHolder(gammaPoolAddress?.toLowerCase(), week); | |
// Loop through each swap of the week | |
let previousTimestamp = week * secondsInWeek; | |
for (const swap of swaps) { | |
// ======================== Uniswap V3 NFTs ========================= | |
let calls = []; | |
for (const id of positions) { | |
calls.push({ | |
canFail: true, | |
data: uniswapV3Interface.encodeFunctionData('positions', [BigNumber.from(id)]), | |
target: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88', | |
}); | |
calls.push({ | |
canFail: true, | |
data: uniswapV3Interface.encodeFunctionData('ownerOf', [BigNumber.from(id)]), | |
target: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88', | |
}); | |
} | |
// Launch the multicall with possible failure | |
const fetchedData = multicall.interface.decodeFunctionResult( | |
'multiCall', | |
await provider.call( | |
{ data: multicall.interface.encodeFunctionData('multiCall', [calls]), to: multicall.address }, | |
parseInt(swap.transaction.blockNumber) | |
) | |
)[0]; | |
let j = 0; | |
// Compute liquidity and fees | |
for (const id of positions) { | |
try { | |
const posLiquidity = uniswapV3Interface.decodeFunctionResult('positions', fetchedData[j]).liquidity; | |
const lowerTick = parseFloat(uniswapV3Interface.decodeFunctionResult('positions', fetchedData[j]).tickLower.toString()); | |
const upperTick = parseFloat(uniswapV3Interface.decodeFunctionResult('positions', fetchedData[j++]).tickUpper.toString()); | |
const owner = uniswapV3Interface.decodeFunctionResult('ownerOf', fetchedData[j++])[0]; | |
const [amount0, amount1] = getAmountsForLiquidity(parseFloat(swap.tick), lowerTick, upperTick, posLiquidity); | |
if (!tempData[owner]) tempData[owner] = { fees: 0, token0: 0, token1: 0 }; | |
tempData[owner].fees += parseFloat(swap.amountUSD) * BN2Number(posLiquidity); | |
tempData[owner].token0 += (BN2Number(amount0) * parseInt(swap.amountUSD)) / totalAmountUSD; | |
tempData[owner].token1 += (BN2Number(amount1) * parseInt(swap.amountUSD)) / totalAmountUSD; | |
} catch { | |
j += 2; | |
} | |
} | |
// ======================== Arrakis ========================= | |
calls = []; | |
j = 0; | |
if (arrakisPoolAddress !== null) { | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('getUnderlyingBalances'), | |
target: arrakisPoolAddress, | |
}); | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('lowerTick'), | |
target: arrakisPoolAddress, | |
}); | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('upperTick'), | |
target: arrakisPoolAddress, | |
}); | |
for (const account of arrakisHolders) { | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('balanceOf', [account]), | |
target: arrakisPoolAddress, | |
}); | |
} | |
for (const account of arrakisGaugeHolders) { | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('balanceOf', [account]), | |
target: arrakisGaugeAddress, | |
}); | |
} | |
// Launch the multicall with possible failure | |
const fetchedData = multicall.interface.decodeFunctionResult( | |
'multiCall', | |
await provider.call( | |
{ data: multicall.interface.encodeFunctionData('multiCall', [calls]), to: multicall.address }, | |
parseInt(swap.transaction.blockNumber) | |
) | |
)[0]; | |
try { | |
const amount0 = arrakisInterface.decodeFunctionResult('getUnderlyingBalances', fetchedData[j])[0]; | |
const amount1 = arrakisInterface.decodeFunctionResult('getUnderlyingBalances', fetchedData[j++])[1]; | |
const token0 = (BN2Number(amount0) * parseInt(swap.amountUSD)) / totalAmountUSD; | |
const token1 = (BN2Number(amount1) * parseInt(swap.amountUSD)) / totalAmountUSD; | |
const lowerTick = arrakisInterface.decodeFunctionResult('lowerTick', fetchedData[j++])[0]; | |
const upperTick = arrakisInterface.decodeFunctionResult('upperTick', fetchedData[j++])[0]; | |
const posLiquidity = getLiquidityForAmounts(parseFloat(swap.tick), lowerTick, upperTick, amount0, amount1); | |
const fees = parseFloat(swap.amountUSD) * BN2Number(posLiquidity); | |
// Split the result among holders | |
let supply = BigNumber.from(0); | |
for (const holder of arrakisHolders) { | |
const arrakisBalance = arrakisInterface.decodeFunctionResult('balanceOf', fetchedData[j++])[0]; | |
supply = supply.add(arrakisBalance); | |
} | |
let gaugeFactor = 0; | |
j = j - arrakisHolders.length; | |
for (const holder of arrakisHolders) { | |
const ratio = BN2Number(arrakisInterface.decodeFunctionResult('balanceOf', fetchedData[j++])[0]) / BN2Number(supply); | |
if (utils.getAddress(holder) === utils.getAddress(arrakisGaugeAddress)) { | |
gaugeFactor = ratio; | |
} else { | |
if (!data[holder]) data[holder] = { fees: 0, token0: 0, token1: 0 }; | |
tempData[holder].fees += fees * ratio; | |
tempData[holder].token0 += token0 * ratio; | |
tempData[holder].token1 += token1 * ratio; | |
} | |
} | |
// Split the result among stakers | |
supply = BigNumber.from(0); | |
for (const holder of arrakisGaugeHolders) { | |
const gaugeTokenBalance = arrakisInterface.decodeFunctionResult('balanceOf', fetchedData[j++])[0]; | |
supply = supply.add(gaugeTokenBalance); | |
} | |
j = j - arrakisGaugeHolders.length; | |
for (const holder of arrakisGaugeHolders) { | |
const ratio = BN2Number(arrakisInterface.decodeFunctionResult('balanceOf', fetchedData[j++])[0]) / BN2Number(supply); | |
if (!tempData[holder]) tempData[holder] = { fees: 0, token0: 0, token1: 0 }; | |
tempData[holder].fees += fees * ratio * gaugeFactor; | |
tempData[holder].token0 += token0 * ratio * gaugeFactor; | |
tempData[holder].token1 += token1 * ratio * gaugeFactor; | |
} | |
} catch { | |
j += 2; | |
} | |
} | |
// ======================== Gamma ========================= | |
calls = []; | |
j = 0; | |
if (gammaPoolAddress !== null) { | |
calls.push({ | |
canFail: true, | |
data: gammaInterface.encodeFunctionData('getBasePosition'), | |
target: gammaPoolAddress, | |
}); | |
calls.push({ | |
canFail: true, | |
data: gammaInterface.encodeFunctionData('getLimitPosition'), | |
target: gammaPoolAddress, | |
}); | |
for (const holder of gammaHolders) { | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('balanceOf', [holder]), | |
target: gammaPoolAddress, | |
}); | |
} | |
// Launch the multicall with possible failure | |
const fetchedData = multicall.interface.decodeFunctionResult( | |
'multiCall', | |
await provider.call( | |
{ data: multicall.interface.encodeFunctionData('multiCall', [calls]), to: multicall.address }, | |
parseInt(swap.transaction.blockNumber) | |
) | |
)[0]; | |
let liquidity = gammaInterface.decodeFunctionResult('getBasePosition', fetchedData[j])[0]; | |
let amount0 = gammaInterface.decodeFunctionResult('getBasePosition', fetchedData[j])[1]; | |
let amount1 = gammaInterface.decodeFunctionResult('getBasePosition', fetchedData[j++])[2]; | |
liquidity = liquidity.add(gammaInterface.decodeFunctionResult('getLimitPosition', fetchedData[j])[0]); | |
amount0 = amount0.add(gammaInterface.decodeFunctionResult('getLimitPosition', fetchedData[j])[1]); | |
amount1 = amount1.add(gammaInterface.decodeFunctionResult('getLimitPosition', fetchedData[j++])[2]); | |
const token0 = (BN2Number(amount0) * parseInt(swap.amountUSD)) / totalAmountUSD; | |
const token1 = (BN2Number(amount1) * parseInt(swap.amountUSD)) / totalAmountUSD; | |
const fees = parseFloat(swap.amountUSD) * BN2Number(liquidity); | |
// Split the result among holders | |
let supply = BigNumber.from(0); | |
for (const holder of gammaHolders) { | |
const gammaBalance = arrakisInterface.decodeFunctionResult('balanceOf', fetchedData[j++])[0]; | |
supply = supply.add(gammaBalance); | |
} | |
j = j - gammaHolders.length; | |
for (const holder of gammaHolders) { | |
const ratio = BN2Number(arrakisInterface.decodeFunctionResult('balanceOf', fetchedData[j++])[0]) / BN2Number(supply); | |
if (!tempData[holder]) tempData[holder] = { fees: 0, token0: 0, token1: 0 }; | |
tempData[holder].fees += fees * ratio; | |
tempData[holder].token0 += token0 * ratio; | |
tempData[holder].token1 += token1 * ratio; | |
} | |
} | |
// ======================== veANGLE Boosting ========================= | |
let totalToken0 = 0; | |
let totalToken1 = 0; | |
let totalFees = 0; | |
Object.values(tempData).forEach((p) => { | |
totalToken0 += p.token0; | |
totalToken1 += p.token1; | |
totalFees += p.fees; | |
}); | |
calls = []; | |
j = 0; | |
for (const h of Object.keys(tempData)) { | |
calls.push({ | |
canFail: true, | |
data: arrakisInterface.encodeFunctionData('balanceOf', [h]), | |
target: CONTRACTS_ADDRESSES[ChainId.MAINNET].veANGLE, | |
}); | |
} | |
const fetchedVeAngleData = multicall.interface.decodeFunctionResult( | |
'multiCall', | |
await provider.call( | |
{ data: multicall.interface.encodeFunctionData('multiCall', [calls]), to: multicall.address }, | |
parseInt(swap.transaction.blockNumber) | |
) | |
)[0]; | |
let supply = BigNumber.from(0); | |
for (const holder of Object.keys(tempData)) { | |
const veANGLEBalance = arrakisInterface.decodeFunctionResult('balanceOf', fetchedVeAngleData[j++])[0]; | |
supply = supply.add(veANGLEBalance); | |
} | |
j = j - Object.keys(tempData).length; | |
for (const holder of Object.keys(tempData)) { | |
const veANGLEBalance = arrakisInterface.decodeFunctionResult('balanceOf', fetchedVeAngleData[j++])[0]; | |
const total = weights.fees * totalFees + weights.token0 * totalToken0 + weights.token1 * totalToken1; | |
const w = | |
weights.fees * tempData[holder].fees + weights.token0 * tempData[holder].token0 + weights.token1 * tempData[holder].token1; | |
const boost = 1 + (((1.5 * BN2Number(veANGLEBalance)) / BN2Number(supply)) * w) / total; | |
// Eventually change the previous results based on the veANGLE balance | |
tempData[holder].fees = boost * tempData[holder].fees; | |
tempData[holder].token0 = boost * tempData[holder].token0; | |
tempData[holder].token1 = boost * tempData[holder].token1; | |
} | |
Object.keys(tempData).forEach((h) => { | |
if (!data[h]) data[h] = { fees: 0, token0: 0, token1: 0 }; | |
data[h].fees += tempData[h].fees; | |
data[h].token0 += tempData[h].token0; | |
data[h].token1 += tempData[h].token1; | |
}); | |
previousTimestamp = parseInt(swap.timestamp); | |
console.log(data); | |
} | |
} catch (e) { | |
console.log(e); | |
} | |
} | |
// Now we assume the data array is filled | |
let totalToken0 = 0; | |
let totalToken1 = 0; | |
let totalFees = 0; | |
Object.values(data).forEach((p) => { | |
totalToken0 += p.token0; | |
totalToken1 += p.token1; | |
totalFees += p.fees; | |
}); | |
const rewards = previousMerkleTree ? require(previousMerkleTree) : {}; | |
for (const holder of Object.keys(data)) { | |
const ratio = | |
(weights.fees * data[holder].fees) / totalFees + | |
(weights.token0 * data[holder].token0) / totalToken0 + | |
(weights.token1 * data[holder].token1) / totalToken1; | |
rewards[holder] = weeklyRewards | |
.mul(BigNumber.from(Math.round(ratio * 10 * 10))) | |
.div(BigNumber.from(10 ** 10)) | |
?.toString(); | |
} | |
fs.writeFile(`${week}.json`, JSON.stringify(rewards), function (e) { | |
if (e) { | |
console.log(e); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment