@dydxfoundation/governance
Version:
dYdX governance smart contracts
262 lines (261 loc) • 12 kB
JavaScript
"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;