Skip to content
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

[moxie] Adding Strategy For Moxie Voting Power Calculation #1604

Merged
merged 12 commits into from
Oct 15, 2024
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
SNAPSHOT_API_STRATEGY_SALT=12345 # Salt for the snapshot API strategy to send a key in header (optional)
PASSPORT_API_KEY= # API key for the passport API (optional for other strategies)
PASSPORT_SCORER_ID= # Scorer ID for the passport API (optional for other strategies)
MOXIE_API_KEY= # API key for the moxie APIs (optional for other strategies)
MOXIE_PROTOCOL_ID= #Protocol ID for the moxie API (optional for other strategies)
MOXIE_VESTING_ID= # Vesting ID for the moxie API (optional for other strategies)
MOXIE_LIQUDITY_ID= # Liquidity ID for the moxie API (optional for other strategies)
2 changes: 2 additions & 0 deletions src/strategies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ import * as superboring from './superboring';
import * as erableGovernanceV1 from './erable-governance-v1';
import * as worldLibertyFinancial from './world-liberty-financial-erc20-balance-of-votes';
import * as snxMultichain from './snx-multichain';
import * as moxie from './moxie'
import * as stakingAmountDurationLinear from './staking-amount-duration-linear';
import * as stakingAmountDurationExponential from './staking-amount-duration-exponential';

Expand Down Expand Up @@ -927,6 +928,7 @@ const strategies = {
'erable-governance-v1': erableGovernanceV1,
'world-liberty-financial-erc20-balance-of-votes': worldLibertyFinancial,
'snx-multichain': snxMultichain,
'moxie': moxie,
'staking-amount-duration-linear': stakingAmountDurationLinear,
'staking-amount-duration-exponential': stakingAmountDurationExponential
};
Expand Down
13 changes: 13 additions & 0 deletions src/strategies/moxie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# moxie

This strategy returns the voting power of users. This takes into consideration
1. Vesting Contract Moxie Balance
2. Wallet Moxie Balance
3. Vesting Contract Fan Token Balance
4. Wallet Fan Token Balance
5. Liquidity Pool Contributions
6. Staked fan tokens for Wallet & Vesting Contracts

There are special multipliers provided for Fan Token Balances and Liquidity Pool Contributions


14 changes: 14 additions & 0 deletions src/strategies/moxie/examples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"name": "Example query",
"strategy": {
"name": "moxie"
},
"network": "8453",
"addresses": [
"0xb59aa5bb9270d44be3fa9b6d67520a2d28cf80ab"
],
"snapshot": 20631331
}
]

213 changes: 213 additions & 0 deletions src/strategies/moxie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { BigNumberish } from '@ethersproject/bignumber';
import { formatUnits } from '@ethersproject/units';
import { Multicaller, subgraphRequest } from '../../utils';
import { getAddress } from '@ethersproject/address';

export const author = 'Hrishikesh-Thakkar';
export const version = '0.0.1';

const abi = [
'function balanceOf(address account) external view returns (uint256)'
];

const QUERY_LIMIT = 1000;
const UNSTAKED_FAN_TOKEN_MULTIPLIER = 2;
const STAKED_FAN_TOKEN_MULTIPLIER = 3;
const MOXIE_LIQUIDITY_MULTIPLIER = 2;
const MOXIE_CONTRACT_ADDRESS = "0x8C9037D1Ef5c6D1f6816278C7AAF5491d24CD527";
const MOXIE_DECIMALS = 18;
const PORTFOLIO_SUBGRAPH_PAGE_LIMIT = 2;
const LIQUIDITY_POOL_SUBGRAPH_PAGE_LIMIT = 1;

//Strategy to Compute Voting Power for MoxieDAO
export async function strategy(
space,
network,
provider,
addresses,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw a error if addresses contain more than 1 address (just not to confuse) we can ignore errors in tests related to this change (need to modify examples.json to pass only one address)

options,
snapshot
) {
//Check if the addresses array has length not equal to 1
if(addresses.length != 1) {
throw new Error("This strategy expects a single address");
};
const MOXIE_API_KEY = process.env.MOXIE_API_KEY || "";
const MOXIE_PROTOCOL_ID = process.env.MOXIE_PROTOCOL_ID || "";
const MOXIE_VESTING_ID = process.env.MOXIE_VESTING_ID || "";
const MOXIE_LIQUIDITY_ID = process.env.MOXIE_LIQUIDITY_ID || "";

//SETTING DEFAULT SUBGRAPH URLS
let MOXIE_PROTOCOL_SUBGRAPH_URL = "https://api.studio.thegraph.com/query/88457/moxie-protocol/version/latest";
let MOXIE_VESTING_SUBGRAPH_URL = "https://api.studio.thegraph.com/query/88457/moxie-vesting/version/latest";
let MOXIE_LIQUIDITY_POOL_SUBGRAPH_URL = "https://api.studio.thegraph.com/query/88457/moxie-liquidity/version/latest";

if (MOXIE_API_KEY !== "" && MOXIE_PROTOCOL_ID !== "") {
MOXIE_PROTOCOL_SUBGRAPH_URL = `https://gateway.thegraph.com/api/${MOXIE_API_KEY}/subgraphs/id/${MOXIE_PROTOCOL_ID}`;
}
if (MOXIE_API_KEY !== "" && MOXIE_LIQUIDITY_ID !== "") {
MOXIE_LIQUIDITY_POOL_SUBGRAPH_URL = `https://gateway.thegraph.com/api/${MOXIE_API_KEY}/subgraphs/id/${MOXIE_LIQUIDITY_ID}`;
}
if (MOXIE_API_KEY !== "" && MOXIE_VESTING_ID !== "") {
MOXIE_VESTING_SUBGRAPH_URL = `https://gateway.thegraph.com/api/${MOXIE_API_KEY}/subgraphs/id/${MOXIE_VESTING_ID}`;
}

//Check if the snapshot is for a specific block number or it's latest
const blockTag = typeof snapshot === 'number' ? snapshot : 'latest';
const addressesMap = addresses.reduce((map, address) => {
map[getAddress(address)] = 0;
return map;
}, {});

const lowercaseAddresses = Object.keys(addressesMap).map((address) =>
address.toLowerCase()
);

// Once we have the addresses we need to query the subgraphs to get the vesting contract addresses
const vestingSubgraphQuery = {
tokenLockWallets: {
__args: {
where: {
beneficiary_in: lowercaseAddresses
}
},
id: true,
beneficiary: true,
}
};
//Adding block to the query if the snapshot is not latest
if (snapshot !== 'latest') {
vestingSubgraphQuery.tokenLockWallets.__args['block'] = { number: snapshot };
}

//Query the vesting subgraph to get the vesting contract addresses
const vestingContractResponse = await subgraphRequest(MOXIE_VESTING_SUBGRAPH_URL, vestingSubgraphQuery);

// Generate a map of vesting contract addresses to beneficiaries
const addressToBeneficiaryMap = vestingContractResponse.tokenLockWallets.reduce((map, wallet) => {
map[wallet.id.toLowerCase()] = wallet.beneficiary.toLowerCase();
return map;
}, {});

// Add vesting contract addresses to the list of addresses to query
const allAddresses = [
...Object.keys(addressToBeneficiaryMap),
...lowercaseAddresses
];

// Initialise all the addresses with a score of 0
const allAddressesScoreMap = allAddresses.reduce((map, address) => {
map[getAddress(address)] = 0;
return map;
}, {});

const protocolSubgraphQuery = {
portfolios: {
__args: {
where: {
user_in: allAddresses,
balance_gt: 0
},
first: QUERY_LIMIT,
skip: 0
},
unstakedBalance: true,
stakedBalance: true,
subjectToken: {
currentPriceInMoxie: true
},
user: {
id: true
}
}
};

const liquidityPoolSubgraphQuery = {
userPools: {
__args: {
where: {
user_in: allAddresses,
},
first: QUERY_LIMIT,
skip: 0
},
totalLPAmount: true,
pool: {
totalSupply: true,
moxieReserve: true,
},
user: {
id: true
}
}
};

//Adding block to the queries if the snapshot is not latest
if (snapshot !== 'latest') {
protocolSubgraphQuery.portfolios.__args['block'] = { number: snapshot };
liquidityPoolSubgraphQuery.userPools.__args['block'] = { number: snapshot };
}

const fetchSubgraphData = async (subgraphUrl, query, processFunction, key, pageLimit) => {
let next_page = 0;
while (next_page < pageLimit) {
query[key].__args.skip = next_page * QUERY_LIMIT;
const response = await subgraphRequest(subgraphUrl, query);
const data = response[Object.keys(response)[0]];

processFunction(data);

if (data.length < QUERY_LIMIT) break;
next_page++;
}
};

const processProtocolData = (portfolios) => {
portfolios.forEach((portfolio) => {
const userAddress = getAddress(portfolio.user.id);
allAddressesScoreMap[userAddress] += parseFloat(formatUnits(portfolio.unstakedBalance, MOXIE_DECIMALS)) * UNSTAKED_FAN_TOKEN_MULTIPLIER * portfolio.subjectToken.currentPriceInMoxie +
parseFloat(formatUnits(portfolio.stakedBalance, MOXIE_DECIMALS)) * STAKED_FAN_TOKEN_MULTIPLIER * portfolio.subjectToken.currentPriceInMoxie;
});
};

const processLiquidityPoolData = (userPools) => {
userPools.forEach((userPool) => {
const userAddress = getAddress(userPool.user.id);
allAddressesScoreMap[userAddress] += MOXIE_LIQUIDITY_MULTIPLIER * parseFloat(formatUnits(userPool.totalLPAmount, MOXIE_DECIMALS)) *
parseFloat(formatUnits(userPool.pool.moxieReserve, MOXIE_DECIMALS)) /
parseFloat(formatUnits(userPool.pool.totalSupply, MOXIE_DECIMALS));
});
};

// RPC Call to get balance of Moxie at a block for users
const fetchBalances = async () => {
const multi = new Multicaller(network, provider, abi, { blockTag });
allAddresses.forEach((address) =>
multi.call(address, MOXIE_CONTRACT_ADDRESS, 'balanceOf', [address])
);
const result: Record<string, BigNumberish> = await multi.execute();
Object.entries(result).forEach(([address, balance]) => {
let formattedBalance = parseFloat(formatUnits(balance, MOXIE_DECIMALS));
allAddressesScoreMap[getAddress(address)] += formattedBalance;
});
};

// Fetch data from both subgraphs in parallel
await Promise.all([
fetchSubgraphData(MOXIE_PROTOCOL_SUBGRAPH_URL, protocolSubgraphQuery, processProtocolData, "portfolios", PORTFOLIO_SUBGRAPH_PAGE_LIMIT),
fetchSubgraphData(MOXIE_LIQUIDITY_POOL_SUBGRAPH_URL, liquidityPoolSubgraphQuery, processLiquidityPoolData, "userPools", LIQUIDITY_POOL_SUBGRAPH_PAGE_LIMIT),
fetchBalances(),
]);

// Now we have the score for each address we need to ensure it is added to the beneficiary address if it exists
Object.keys(allAddressesScoreMap).forEach((address) => {
const beneficiary = addressToBeneficiaryMap[address.toLowerCase()];
if (beneficiary) {
addressesMap[getAddress(beneficiary)] += allAddressesScoreMap[address];
} else {
addressesMap[address] += allAddressesScoreMap[address];
}
});

return addressesMap;
}
Loading