UNPKG

@dydxfoundation/governance

Version:
262 lines (261 loc) 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const bignumber_js_1 = __importDefault(require("bignumber.js")); const ethers_1 = require("ethers"); const utils_1 = require("ethers/lib/utils"); const types_1 = require("../../../types"); const balance_tree_1 = __importDefault(require("../../merkle-tree-helpers/balance-tree")); const config_1 = require("../config"); const types_2 = require("../types"); const ipfs_1 = require("../utils/ipfs"); const BaseService_1 = __importDefault(require("./BaseService")); class MerkleDistributor extends BaseService_1.default { constructor(config, erc20Service, hardhatMerkleDistributorAddresses) { super(config, types_1.MerkleDistributorV1__factory); this.erc20Service = erc20Service; this._rewardToken = null; this._activeRootDataAndHistory = { userBalancesPerEpoch: {}, activeMerkleTree: null, }; this._proposedRootMetadata = { balances: {}, ipfsCid: '', }; this._transfersRestrictedBefore = null; const { network } = this.config; const isHardhatNetwork = network === types_2.Network.hardhat; if (isHardhatNetwork && !hardhatMerkleDistributorAddresses) { throw new Error('Must specify merkle distributor addresses when on hardhat network'); } this.merkleDistributorAddress = isHardhatNetwork ? hardhatMerkleDistributorAddresses.MERKLE_DISTRIBUTOR_ADDRESS : config_1.merkleDistributorAddresses[network].MERKLE_DISTRIBUTOR_ADDRESS; } get contract() { return this.getContractInstance(this.merkleDistributorAddress); } async getActiveRootData() { const rootUpdatedFilter = this.contract.filters.RootUpdated(null, null, null); const rootUpdatedEvents = await this.contract.queryFilter(rootUpdatedFilter); if (!rootUpdatedEvents.length) { // if no root has been set to active on the contract yet, show 0 rewards return this._activeRootDataAndHistory; } await Promise.all(rootUpdatedEvents.map(async (rootUpdatedEvent) => { const eventArgs = rootUpdatedEvent.args; const epoch = eventArgs.epoch.toNumber(); const ipfsCid = eventArgs.ipfsCid; if (!(epoch in this._activeRootDataAndHistory.userBalancesPerEpoch)) { // fetch user balances from IPFS for this epoch const balances = await (0, ipfs_1.getMerkleTreeBalancesFromIpfs)(ipfsCid, this.config.ipfsTimeoutMs); this._activeRootDataAndHistory.userBalancesPerEpoch[epoch] = balances; } })); const lastRootUpdatedEpoch = rootUpdatedEvents.length - 1; // active merkle tree must exist if there was a root update event, update if needed this._activeRootDataAndHistory.activeMerkleTree = this.getActiveMerkleTree(this._activeRootDataAndHistory.userBalancesPerEpoch[lastRootUpdatedEpoch], lastRootUpdatedEpoch); return this._activeRootDataAndHistory; } getActiveMerkleTree(activeRootEpochBalances, activeRootEpoch) { let currentMerkleTreeData = this._activeRootDataAndHistory.activeMerkleTree; if (!currentMerkleTreeData || activeRootEpoch > currentMerkleTreeData.epoch) { const merkleTree = new balance_tree_1.default(activeRootEpochBalances); currentMerkleTreeData = { epoch: activeRootEpoch, merkleTree, }; } return currentMerkleTreeData; } // this function should only be called if the merkle distributor has a pending root async getProposedRootBalances() { const proposedRootData = await this.contract.getProposedRoot(); const ipfsCid = proposedRootData.ipfsCid; if (ipfsCid === '0x') { throw new Error('Proposed root IPFS CID is unset'); } if (ipfsCid !== this._proposedRootMetadata.ipfsCid) { // IPFS CID is different than previously stored IPFS CID, update proposed root metadata const balances = await (0, ipfs_1.getMerkleTreeBalancesFromIpfs)(ipfsCid, this.config.ipfsTimeoutMs); this._proposedRootMetadata = { balances, ipfsCid, }; } return this._proposedRootMetadata; } async hasPendingRoot() { return this.contract.hasPendingRoot(); } async getRewardToken() { if (!this._rewardToken) { this._rewardToken = await this.contract.REWARDS_TOKEN(); } return this._rewardToken; } async getTransfersRestrictedBefore() { if (!this._transfersRestrictedBefore) { const rewardsTokenAddress = await this.getRewardToken(); const { provider } = this.config; const dydxToken = types_1.DydxToken__factory.connect(rewardsTokenAddress, provider); const transfersRestrictedBefore = await dydxToken._transfersRestrictedBefore(); this._transfersRestrictedBefore = transfersRestrictedBefore.toNumber(); } return this._transfersRestrictedBefore; } async getActiveRootMerkleProof(user) { const checksummedAddress = ethers_1.utils.getAddress(user); const activeRootDataAndHistory = await this.getActiveRootData(); const activeMerkleTree = activeRootDataAndHistory.activeMerkleTree; if (!activeMerkleTree) { // no root has been promoted to active on merkle distributor return { cumulativeAmount: ethers_1.BigNumber.from(0), merkleProof: [], }; } const userBalancesPerEpoch = activeRootDataAndHistory.userBalancesPerEpoch; const activeRootEpoch = activeMerkleTree.epoch; if (!(activeRootEpoch in userBalancesPerEpoch)) { throw Error(`Balances were not found for epoch ${activeRootEpoch}`); } if (!(checksummedAddress in userBalancesPerEpoch[activeRootEpoch])) { // user has no trading rewards return { cumulativeAmount: ethers_1.BigNumber.from(0), merkleProof: [], }; } const cumulativeAmount = userBalancesPerEpoch[activeRootEpoch][checksummedAddress]; const merkleProof = activeMerkleTree.merkleTree.getProof(checksummedAddress, cumulativeAmount); return { cumulativeAmount, merkleProof, }; } async claimRewards(user) { const checksummedAddress = ethers_1.utils.getAddress(user); const merkleProof = await this.getActiveRootMerkleProof(checksummedAddress); if (!merkleProof.merkleProof.length) { throw new Error('User not found in the Merkle tree'); } const txCallback = this.generateTxCallback({ rawTxMethod: () => this.contract.populateTransaction.claimRewards(merkleProof.cumulativeAmount, merkleProof.merkleProof), from: user, gasSurplus: 20, }); return [ { tx: txCallback, txType: types_2.eEthereumTxType.MERKLE_DISTRIBUTOR_ACTION, gas: this.generateTxPriceEstimation([], txCallback), }, ]; } async getRootUpdatedMetadata() { const rootUpdatedEventFilter = this.contract.filters.RootUpdated(null, null, null); const rootUpdatedEvents = await this.contract.queryFilter(rootUpdatedEventFilter); if (rootUpdatedEvents.length === 0) { // root hasn't been updated yet return { lastRootUpdatedTimestamp: 0, numRootUpdates: 0, }; } const lastRootUpdatedEvent = rootUpdatedEvents[rootUpdatedEvents.length - 1]; const rootUpdatedBlock = await lastRootUpdatedEvent.getBlock(); return { lastRootUpdatedTimestamp: rootUpdatedBlock.timestamp, numRootUpdates: rootUpdatedEvents.length, }; } async getUserRewardsData(user) { const checksummedAddress = ethers_1.utils.getAddress(user); const [claimedRewardsWei, hasPendingRoot, { userBalancesPerEpoch }, epochData,] = await Promise.all([ this.contract.getClaimed(checksummedAddress), this.hasPendingRoot(), this.getActiveRootData(), this.getEpochData(), ]); const rewardsPerEpoch = {}; let currentActiveRootRewards = ethers_1.BigNumber.from(0); // Should have a rewards balance entry for each active root update const numRootUpdates = Object.keys(userBalancesPerEpoch).length; const lastEpoch = numRootUpdates - 1; for (let i = 0; i < numRootUpdates; i++) { if (!(i in userBalancesPerEpoch)) { console.error(`Data for epoch ${i} not found`); } const epochRewards = userBalancesPerEpoch[i]; const userEpochRewards = checksummedAddress in epochRewards ? epochRewards[checksummedAddress] : ethers_1.BigNumber.from(0); rewardsPerEpoch[i] = (0, utils_1.formatEther)(userEpochRewards); if (i === lastEpoch) { // on last root update, record current active root rewards to calculate new pending root rewards currentActiveRootRewards = userEpochRewards; } } let newPendingRootRewards = '0.0'; let waitingPeriodEnd = 0; if (hasPendingRoot) { const [waitingPeriodEndBN, proposedRootMetadata,] = await Promise.all([ this.contract.getWaitingPeriodEnd(), this.getProposedRootBalances(), ]); waitingPeriodEnd = waitingPeriodEndBN.toNumber(); const pendingRootBalances = proposedRootMetadata.balances; if (checksummedAddress in pendingRootBalances) { newPendingRootRewards = (0, utils_1.formatEther)(pendingRootBalances[checksummedAddress].sub(currentActiveRootRewards)); } } return { rewardsPerEpoch, claimedRewards: (0, utils_1.formatEther)(claimedRewardsWei), pendingRootData: { hasPendingRoot, waitingPeriodEnd, }, newPendingRootRewards, epochData, }; } // Meant to be used for testing purposes only clearCachedRewardsData() { this._activeRootDataAndHistory = { userBalancesPerEpoch: {}, activeMerkleTree: null, }; this._proposedRootMetadata = { balances: {}, ipfsCid: '', }; } async getEpochData() { const [epochParams, waitingPeriodLength, currentBlocktime,] = await Promise.all([ this.contract.getEpochParameters(), this.contract.WAITING_PERIOD(), this.timeLatest(), ]); const epochZeroStart = epochParams.offset; const epochLength = epochParams.interval.toNumber(); const secondsSinceEpochZero = new bignumber_js_1.default(currentBlocktime).minus(epochZeroStart.toNumber()); const currentEpoch = secondsSinceEpochZero .dividedToIntegerBy(epochLength) .toNumber(); const startOfEpochTimestamp = epochZeroStart.add(epochParams.interval.mul(currentEpoch)).toNumber(); const endOfEpochTimestamp = epochZeroStart.add(epochParams.interval.mul(currentEpoch + 1)).toNumber(); return { epochLength, currentEpoch, startOfEpochTimestamp, endOfEpochTimestamp, waitingPeriodLength: waitingPeriodLength.toNumber(), }; } } exports.default = MerkleDistributor;