|
| 1 | +import { multicall } from '../../utils'; |
| 2 | +import { BigNumber } from '@ethersproject/bignumber'; |
| 3 | +import { formatUnits } from '@ethersproject/units'; |
| 4 | + |
| 5 | +export const author = 'pierremarsotlyon1'; |
| 6 | +export const version = '0.0.1'; |
| 7 | + |
| 8 | +// Used ABI |
| 9 | +const abi = [ |
| 10 | + 'function balanceOf(address account) external view returns (uint256)', |
| 11 | + 'function working_supply() external view returns (uint256)', |
| 12 | + 'function totalSupply() external view returns (uint256)', |
| 13 | + 'function working_balances(address account) external view returns (uint256)', |
| 14 | + 'function balances(uint256 i) external view returns (uint256)', |
| 15 | + 'function votingPowerForAccount(address i) external view returns (uint256)' |
| 16 | +]; |
| 17 | + |
| 18 | +const MIN_BOOST = 0.4; |
| 19 | + |
| 20 | +export async function strategy( |
| 21 | + space, |
| 22 | + network, |
| 23 | + provider, |
| 24 | + addresses, |
| 25 | + options, |
| 26 | + snapshot |
| 27 | +): Promise<Record<string, number>> { |
| 28 | + // Maximum of 5 multicall! |
| 29 | + if (options.sampleStep > 5) { |
| 30 | + throw new Error('maximum of 5 call'); |
| 31 | + } |
| 32 | + |
| 33 | + // Maximum of 20 whitelisted address |
| 34 | + if (options.whiteListedAddress.length > 20) { |
| 35 | + throw new Error('maximum of 20 whitelisted address'); |
| 36 | + } |
| 37 | + |
| 38 | + // Maximum of 500 pools address |
| 39 | + if (options.pools.length > 500) { |
| 40 | + throw new Error('maximum of 500 pools'); |
| 41 | + } |
| 42 | + |
| 43 | + const calls: any[] = []; |
| 44 | + calls.push([options.sdToken, 'totalSupply', []]); |
| 45 | + |
| 46 | + for (const pool of options.pools) { |
| 47 | + calls.push([options.sdToken, 'balanceOf', [pool]]); |
| 48 | + } |
| 49 | + calls.push([ |
| 50 | + options.veToken, |
| 51 | + 'votingPowerForAccount', |
| 52 | + [options.liquidLocker] |
| 53 | + ]); |
| 54 | + |
| 55 | + // --- Create block number list for twavp |
| 56 | + // Obtain last block number |
| 57 | + // Create block tag |
| 58 | + let blockTag = 0; |
| 59 | + if (typeof snapshot === 'number') { |
| 60 | + blockTag = snapshot; |
| 61 | + } else { |
| 62 | + blockTag = await provider.getBlockNumber(); |
| 63 | + } |
| 64 | + |
| 65 | + // Create block list |
| 66 | + const blockList = getPreviousBlocks( |
| 67 | + blockTag, |
| 68 | + options.sampleStep, |
| 69 | + options.sampleSize, |
| 70 | + options.removeTwavp || false |
| 71 | + ); |
| 72 | + |
| 73 | + // Query working balance of users |
| 74 | + const workingBalanceQuery = addresses.map((address: any) => [ |
| 75 | + options.sdTokenGauge, |
| 76 | + 'working_balances', |
| 77 | + [address] |
| 78 | + ]); |
| 79 | + |
| 80 | + // Execute multicall `sampleStep` times |
| 81 | + const response: any[] = []; |
| 82 | + for (let i = 0; i < options.sampleStep; i++) { |
| 83 | + // Use good block number |
| 84 | + blockTag = blockList[i]; |
| 85 | + |
| 86 | + const loopCalls: any[] = []; |
| 87 | + |
| 88 | + // Add mutlicall response to array |
| 89 | + if (i === options.sampleStep - 1) { |
| 90 | + // End |
| 91 | + loopCalls.push([options.sdTokenGauge, 'working_supply']); |
| 92 | + loopCalls.push(...workingBalanceQuery); |
| 93 | + loopCalls.push(...calls); |
| 94 | + } else { |
| 95 | + loopCalls.push(...workingBalanceQuery); |
| 96 | + } |
| 97 | + |
| 98 | + response.push( |
| 99 | + await multicall(network, provider, abi, loopCalls, { blockTag }) |
| 100 | + ); |
| 101 | + } |
| 102 | + |
| 103 | + const workingSupply = parseFloat( |
| 104 | + formatUnits(response[response.length - 1].shift()[0], 18) |
| 105 | + ); |
| 106 | + const lockerVotingPower = parseFloat( |
| 107 | + formatUnits(response[response.length - 1].pop()[0], 18) |
| 108 | + ); |
| 109 | + |
| 110 | + const poolsBalances = options.pools.map( |
| 111 | + () => response[response.length - 1].pop()[0] |
| 112 | + ); |
| 113 | + const sumPoolsBalance = parseFloat( |
| 114 | + formatUnits( |
| 115 | + poolsBalances.reduce( |
| 116 | + (acc, balance) => acc.add(balance), |
| 117 | + BigNumber.from(0) |
| 118 | + ), |
| 119 | + 18 |
| 120 | + ) |
| 121 | + ); |
| 122 | + |
| 123 | + const sdTknSupply = parseFloat( |
| 124 | + formatUnits(response[response.length - 1].pop()[0], 18) |
| 125 | + ); |
| 126 | + |
| 127 | + const liquidityVoteFee = |
| 128 | + (MIN_BOOST * sumPoolsBalance * lockerVotingPower) / sdTknSupply; |
| 129 | + |
| 130 | + const totalUserVotes = lockerVotingPower - liquidityVoteFee; |
| 131 | + |
| 132 | + return Object.fromEntries( |
| 133 | + Array(addresses.length) |
| 134 | + .fill('x') |
| 135 | + .map((_, i) => { |
| 136 | + // Init array of working balances for user |
| 137 | + const userWorkingBalances: BigNumber[] = []; |
| 138 | + |
| 139 | + for (let j = 0; j < options.sampleStep; j++) { |
| 140 | + // Add working balance to array. |
| 141 | + userWorkingBalances.push(response[j].shift()[0]); |
| 142 | + } |
| 143 | + |
| 144 | + if (addresses[i].toLowerCase() === options.botAddress.toLowerCase()) { |
| 145 | + return [addresses[i], Number(liquidityVoteFee)]; |
| 146 | + } else { |
| 147 | + // Get average working balance. |
| 148 | + const averageWorkingBalance = parseFloat( |
| 149 | + formatUnits( |
| 150 | + average( |
| 151 | + userWorkingBalances, |
| 152 | + addresses[i], |
| 153 | + options.whiteListedAddress |
| 154 | + ), |
| 155 | + 18 |
| 156 | + ) |
| 157 | + ); |
| 158 | + |
| 159 | + // Calculate voting power. |
| 160 | + const userVote = |
| 161 | + workingSupply != 0 |
| 162 | + ? (averageWorkingBalance / workingSupply) * totalUserVotes |
| 163 | + : 0; |
| 164 | + |
| 165 | + // Return address and voting power |
| 166 | + return [addresses[i], Number(userVote)]; |
| 167 | + } |
| 168 | + }) |
| 169 | + ); |
| 170 | +} |
| 171 | + |
| 172 | +function getPreviousBlocks( |
| 173 | + currentBlockNumber: number, |
| 174 | + numberOfBlocks: number, |
| 175 | + daysInterval: number, |
| 176 | + removeTwavp: boolean |
| 177 | +): number[] { |
| 178 | + if (removeTwavp) { |
| 179 | + return [currentBlockNumber]; |
| 180 | + } |
| 181 | + |
| 182 | + // Estimate number of blocks per day |
| 183 | + const blocksPerDay = 86400 / 12; |
| 184 | + // Calculate total blocks interval |
| 185 | + const totalBlocksInterval = blocksPerDay * daysInterval; |
| 186 | + // Calculate block interval |
| 187 | + const blockInterval = totalBlocksInterval / (numberOfBlocks - 1); |
| 188 | + |
| 189 | + // Init array of block numbers |
| 190 | + const blockNumbers: number[] = []; |
| 191 | + |
| 192 | + for (let i = 0; i < numberOfBlocks; i++) { |
| 193 | + // Calculate block number |
| 194 | + const blockNumber = |
| 195 | + currentBlockNumber - totalBlocksInterval + blockInterval * i; |
| 196 | + // Add block number to array |
| 197 | + blockNumbers.push(Math.round(blockNumber)); |
| 198 | + } |
| 199 | + |
| 200 | + // Return array of block numbers |
| 201 | + return blockNumbers; |
| 202 | +} |
| 203 | + |
| 204 | +function average( |
| 205 | + numbers: BigNumber[], |
| 206 | + address: string, |
| 207 | + whiteListedAddress: string[] |
| 208 | +): BigNumber { |
| 209 | + // If no numbers, return 0 to avoid division by 0. |
| 210 | + if (numbers.length === 0) return BigNumber.from(0); |
| 211 | + |
| 212 | + // If address is whitelisted, return most recent working balance. i.e. no twavp applied. |
| 213 | + if (whiteListedAddress.includes(address)) return numbers[numbers.length - 1]; |
| 214 | + |
| 215 | + // Init sum |
| 216 | + let sum = BigNumber.from(0); |
| 217 | + // Loop through all elements and add them to sum |
| 218 | + for (let i = 0; i < numbers.length; i++) { |
| 219 | + sum = sum.add(numbers[i]); |
| 220 | + } |
| 221 | + |
| 222 | + // Return sum divided by array length to get mean |
| 223 | + return sum.div(numbers.length); |
| 224 | +} |
0 commit comments