Skip to content

Instantly share code, notes, and snippets.

@Picodes
Last active January 23, 2024 02:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Picodes/0b738ec92f7bd72ec6e77ffdf5d1c5e2 to your computer and use it in GitHub Desktop.
Save Picodes/0b738ec92f7bd72ec6e77ffdf5d1c5e2 to your computer and use it in GitHub Desktop.
Uniswap V3 Reward Computation
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